package job import ( "context" "fmt" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libviral "haixun-backend/internal/library/viral" domai "haixun-backend/internal/model/ai/domain/usecase" aiusecase "haixun-backend/internal/model/ai/usecase" missionentity "haixun-backend/internal/model/copy_mission/domain/entity" missiondomain "haixun-backend/internal/model/copy_mission/domain/usecase" jobdom "haixun-backend/internal/model/job/domain/usecase" personadomain "haixun-backend/internal/model/persona/domain/usecase" placementusecase "haixun-backend/internal/model/placement/usecase" threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase" ) type AnalyzeCopyMissionDeps struct { Jobs jobdom.UseCase CopyMission missiondomain.UseCase Persona personadomain.UseCase ThreadsAccount threadsaccountdomain.UseCase Placement placementusecase.UseCase AI aiusecase.UseCase } func RegisterAnalyzeCopyMissionHandler(runner *Runner, deps AnalyzeCopyMissionDeps) { if runner == nil { return } runner.RegisterStepHandler("copy_mission_map", func(ctx context.Context, step StepContext) error { return runAnalyzeCopyMission(ctx, step, deps) }) } func runAnalyzeCopyMission(ctx context.Context, step StepContext, deps AnalyzeCopyMissionDeps) error { payload := step.Run.Payload tenantID, ownerUID := runActorFromPayload(payload, step.Run) personaID := stringField(payload, "persona_id") missionID := stringField(payload, "copy_mission_id") if tenantID == "" || ownerUID == "" || personaID == "" || missionID == "" { return fmt.Errorf("analyze-copy-mission payload missing tenant_id, owner_uid, persona_id, or copy_mission_id") } mission, err := deps.CopyMission.Get(ctx, tenantID, ownerUID, personaID, missionID) if err != nil { return err } persona, err := deps.Persona.Get(ctx, tenantID, ownerUID, personaID) if err != nil { return err } updateProgress := func(summary string, percentage int) { _ = step.Heartbeat(ctx) _, _ = deps.Jobs.UpdateProgress(ctx, jobdom.UpdateProgressRequest{ JobID: step.JobID, WorkerID: step.WorkerID, Phase: "copy_mission_map", Summary: summary, Percentage: percentage, }) } updateProgress("產生拷貝任務研究地圖…", 15) credential, err := deps.ThreadsAccount.ResolveMemberAiCredential(ctx, tenantID, ownerUID) if err != nil { return err } providerID, err := aiusecase.MapWorkerProvider(credential.Provider) if err != nil { return err } result, err := deps.AI.GenerateText(ctx, domai.GenerateRequest{ Provider: providerID, Model: credential.Model, Credential: domai.Credential{ APIKey: credential.APIKey, }, System: libviral.BuildMissionResearchMapSystemPrompt(), Messages: []domai.Message{ { Role: "user", Content: libviral.BuildMissionResearchMapUserPrompt(libviral.CopyResearchMapInput{ Label: mission.Label, SeedQuery: mission.SeedQuery, Brief: mission.Brief, Persona: persona.Persona, StyleBenchmark: persona.StyleBenchmark, PersonaAudienceSummary: persona.CopyResearchMap.AudienceSummary, PersonaContentGoal: persona.CopyResearchMap.ContentGoal, PersonaQuestions: append([]string(nil), persona.CopyResearchMap.Questions...), PersonaPillars: append([]string(nil), persona.CopyResearchMap.Pillars...), }), }, }, }) if err != nil { return err } parsed, err := libviral.ParseMissionResearchMapOutput(result.Text) if err != nil { return app.For(code.AI).SvcThirdParty("拷貝任務研究地圖 LLM 回傳無法解析:" + err.Error()) } entityTags := make([]missionentity.SuggestedTag, 0, len(parsed.SuggestedTags)) for _, tag := range parsed.SuggestedTags { entityTags = append(entityTags, missionentity.SuggestedTag{ Tag: tag.Tag, Reason: tag.Reason, SearchIntent: tag.SearchIntent, SearchType: tag.SearchType, }) } researchMap := missionentity.ResearchMap{ AudienceSummary: parsed.AudienceSummary, ContentGoal: parsed.ContentGoal, Questions: parsed.Questions, Pillars: parsed.Pillars, Exclusions: parsed.Exclusions, BenchmarkNotes: parsed.BenchmarkNotes, } entityTags = make([]missionentity.SuggestedTag, 0, len(parsed.SuggestedTags)) for _, tag := range parsed.SuggestedTags { entityTags = append(entityTags, missionentity.SuggestedTag{ Tag: tag.Tag, Reason: tag.Reason, SearchIntent: tag.SearchIntent, SearchType: tag.SearchType, }) } researchMap.SimilarAccounts = nil researchMap.SuggestedTags = entityTags selected := libviral.PickDefaultSelectedTags(parsed.SuggestedTags) mapped := missionentity.StatusMapped updateProgress("儲存研究地圖與預設標籤…", 85) _, err = deps.CopyMission.Update(ctx, missiondomain.UpdateRequest{ TenantID: tenantID, OwnerUID: ownerUID, PersonaID: personaID, MissionID: missionID, Patch: missiondomain.MissionPatch{ ResearchMap: &researchMap, SelectedTagsSet: true, SelectedTags: selected, Status: &mapped, }, }) if err != nil { return err } handoff := map[string]any{ "flow": "copy", "persona_id": personaID, "copy_mission_id": missionID, "summary": fmt.Sprintf("研究地圖就緒,已預選 %d 個搜尋標籤", len(selected)), "next_route": fmt.Sprintf("/matrix/missions/%s", missionID), } _, err = deps.Jobs.CompleteRun(ctx, jobdom.CompleteRunRequest{ JobID: step.JobID, WorkerID: step.WorkerID, Result: map[string]any{ "tag_count": len(entityTags), "account_count": 0, "selected_count": len(selected), "handoff": handoff, }, }) return err } func copyMissionIDFromPayload(payload map[string]any) string { if id := stringField(payload, "copy_mission_id"); id != "" { return id } return strings.TrimSpace(stringField(payload, "mission_id")) }