thread-master/apps/backend/internal/module/radar/usecase/reply.go

126 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase
import (
"context"
"fmt"
"strings"
"apps/backend/internal/module/radar/domain"
usageDomain "apps/backend/internal/module/usage/domain"
)
func (s *Service) ListReplies(ctx context.Context, ownerUID int64, opportunityID string) ([]*domain.ReplyVariant, error) {
if _, err := s.GetOpportunity(ctx, ownerUID, opportunityID); err != nil {
return nil, err
}
return s.Repo.ListReplies(ctx, ownerUID, opportunityID)
}
/*
GenerateReply creates one reply variant with forbidden-word filter.
DM variants are copy-only: never claim auto-send (spec 送出閘).
*/
func (s *Service) GenerateReply(ctx context.Context, ownerUID int64, opportunityID, variant string) (_ *domain.ReplyVariant, err error) {
if !domain.IsReplyVariant(variant) {
return nil, fmt.Errorf("%w: unknown variant %q", domain.ErrValidation, variant)
}
o, err := s.GetOpportunity(ctx, ownerUID, opportunityID)
if err != nil {
return nil, err
}
profile, _ := s.Repo.GetServiceProfile(ctx, ownerUID)
charge, err := s.bill(ctx, ownerUID, usageDomain.MeterAICopy, "雷達回覆生成", "radar.reply")
if err != nil {
return nil, err
}
defer charge.Settle(ctx, &err)
text, err := s.completeAI(ctx, ownerUID, replyPrompt(profile, o, variant))
if err != nil {
// heuristic fallback
text = heuristicReply(profile, o, variant)
}
text = strings.TrimSpace(text)
if profile != nil {
text = filterForbidden(text, profile.Forbidden)
}
if text == "" {
return nil, fmt.Errorf("%w: empty reply after forbidden filter", domain.ErrValidation)
}
r := &domain.ReplyVariant{
ID: domain.NewID(),
OwnerUID: ownerUID,
OpportunityID: opportunityID,
Variant: variant,
Text: text,
CreatedAt: domain.NowNano(),
}
if err := r.Normalize(); err != nil {
return nil, err
}
if err := s.Repo.SaveReply(ctx, r); err != nil {
return nil, err
}
return r, nil
}
func replyPrompt(profile *domain.ServiceProfile, o *domain.Opportunity, variant string) string {
var b strings.Builder
b.WriteString("為以下商機寫一則繁中台灣語氣的回覆草稿,只輸出正文。\n")
b.WriteString("版本:" + variant + "\n")
if variant == domain.ReplyDM {
b.WriteString("這是私訊草稿,使用者會手動複製送出,不要寫「已自動送出」。\n")
}
if profile != nil {
if len(profile.Forbidden) > 0 {
b.WriteString("禁止使用:" + strings.Join(profile.Forbidden, "、") + "\n")
}
if profile.ToneNote != "" {
b.WriteString("語氣:" + profile.ToneNote + "\n")
}
}
b.WriteString("貼文:\n" + o.Text + "\n")
return b.String()
}
func heuristicReply(profile *domain.ServiceProfile, o *domain.Opportunity, variant string) string {
svc := "我們的服務"
if profile != nil && len(profile.Services) > 0 {
svc = profile.Services[0].Name
}
switch variant {
case domain.ReplyNoSales:
return "看到你的需求了,先分享一點實務上常見的做法給你參考,有問題再問我。"
case domain.ReplyProfessional:
return fmt.Sprintf("您好,關於「%s」我這邊有%s的經驗若方便可再補充你的時間與預算範圍。", trimRunes(o.Text, 40), svc)
case domain.ReplyHumorous:
return "懂你的痛苦!這題我遇過幾次,願意的話我可以幫你釐清怎麼選比較不踩雷。"
case domain.ReplyDM:
return fmt.Sprintf("你好,我做%s看到你的貼文想私訊給你更完整的說明請手動貼上傳送。", svc)
default:
return fmt.Sprintf("嗨,看到你在找相關協助。我這邊有%s可以幫忙若還在比較歡迎問我細節。", svc)
}
}
func filterForbidden(text string, forbidden []string) string {
out := text
for _, f := range forbidden {
f = strings.TrimSpace(f)
if f == "" {
continue
}
out = strings.ReplaceAll(out, f, "…")
}
return out
}
func trimRunes(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "…"
}