187 lines
5.9 KiB
Go
187 lines
5.9 KiB
Go
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"))
|
|
}
|