package placement import ( "encoding/json" "fmt" "regexp" "strings" ) type ResearchMap struct { AudienceSummary string `json:"audienceSummary"` ContentGoal string `json:"contentGoal"` Questions []string `json:"questions"` Pillars []string `json:"pillars"` Exclusions []string `json:"exclusions"` } type ResearchMapInput struct { Label string SeedQuery string Brief string ProductContext string } var researchMapFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```") func BuildResearchMapSystemPrompt() string { return strings.TrimSpace(`你是 Threads(脆)產品置入研究顧問。目標是幫品牌找到「近期發文、作者有需求、現在留言還來得及自然推薦產品」的貼文。 規則: 1. questions 與 pillars 會直接拿去 Threads 搜尋——每句 5~20 字,像真人求助 2. questions 至少 5 個;pillars 至少 4 個;exclusions 至少 4 個 3. contentGoal 要寫:找到近期發文且可自然留言置入的貼文 4. 全部繁體中文,貼近台灣 Threads 5. 只回傳一個 JSON:audienceSummary, contentGoal, questions, pillars, exclusions`) } func BuildResearchMapUserPrompt(in ResearchMapInput) string { var b strings.Builder b.WriteString("【主題名稱】") b.WriteString(strings.TrimSpace(in.Label)) b.WriteString("\n【種子關鍵字】") b.WriteString(strings.TrimSpace(in.SeedQuery)) b.WriteString("\n【這個主題想做什么】\n") b.WriteString(strings.TrimSpace(in.Brief)) b.WriteString("\n【產品置入】\n") product := FormatProductContextForPrompt(in.ProductContext) if product == "" { product = "(尚未填寫)" } b.WriteString(product) b.WriteString("\n\n請產出研究地圖 JSON。") return b.String() } func ParseResearchMapOutput(raw string) (ResearchMap, error) { payload, err := extractResearchJSONObject(raw) if err != nil { return ResearchMap{}, err } var out ResearchMap if err := json.Unmarshal(payload, &out); err != nil { return ResearchMap{}, fmt.Errorf("parse research map json: %w", err) } out.AudienceSummary = strings.TrimSpace(out.AudienceSummary) out.ContentGoal = strings.TrimSpace(out.ContentGoal) out.Questions = cleanStringList(out.Questions) out.Pillars = cleanStringList(out.Pillars) out.Exclusions = cleanStringList(out.Exclusions) if out.AudienceSummary == "" && len(out.Questions) == 0 { return ResearchMap{}, fmt.Errorf("research map missing audience or questions") } return out, nil } func cleanStringList(items []string) []string { out := make([]string, 0, len(items)) seen := map[string]struct{}{} for _, item := range items { item = strings.TrimSpace(item) if item == "" { continue } if _, ok := seen[item]; ok { continue } seen[item] = struct{}{} out = append(out, item) } return out } func extractResearchJSONObject(raw string) ([]byte, error) { raw = strings.TrimSpace(raw) if m := researchMapFenceRE.FindStringSubmatch(raw); len(m) == 2 { raw = strings.TrimSpace(m[1]) } start := strings.Index(raw, "{") end := strings.LastIndex(raw, "}") if start < 0 || end <= start { return nil, fmt.Errorf("research map output missing json object") } return []byte(raw[start : end+1]), nil }