diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go
index bdb1ed3a..aa768e0d 100644
--- a/backend/cmd/server/main.go
+++ b/backend/cmd/server/main.go
@@ -74,7 +74,7 @@ func main() {
firecrawlClient = firecrawl.NewClient(cfg.FirecrawlAPIKey)
}
- chatService := service.NewChatService(chatClient, chatRepo, firecrawlClient)
+ chatService := service.NewChatService(chatClient, chatRepo, userRepo, firecrawlClient)
echoService := service.NewEchoService(chatClient, echoRepo, userRepo)
photoService := service.NewPhotoService(photoRepo, userRepo, chatClient, cfg.ImageModel)
whisperService := service.NewWhisperService(whisperRepo, userRepo, chatClient)
@@ -87,7 +87,7 @@ func main() {
communityAIService = service.NewCommunityAIService(
chatClient, firecrawlClient,
questionRepo, answerRepo, commentRepo,
- aiUserID,
+ userRepo, aiUserID,
)
}
diff --git a/backend/internal/dto/comment.go b/backend/internal/dto/comment.go
index 7ee543ff..00e75ed7 100644
--- a/backend/internal/dto/comment.go
+++ b/backend/internal/dto/comment.go
@@ -8,6 +8,11 @@ type CommentCreate struct {
ParentID *string `json:"parent_id"`
}
+// CommentUpdate is the request body for updating a comment
+type CommentUpdate struct {
+ Content string `json:"content" binding:"required,min=1,max=10000"`
+}
+
// CommentListItem is a single comment (with nested replies)
type CommentListItem struct {
ID string `json:"id"`
diff --git a/backend/internal/handler/comment.go b/backend/internal/handler/comment.go
index 3d62b793..3288d858 100644
--- a/backend/internal/handler/comment.go
+++ b/backend/internal/handler/comment.go
@@ -68,6 +68,38 @@ func (h *CommentHandler) Create(c *gin.Context) {
c.JSON(http.StatusCreated, comment)
}
+// PUT /api/v1/community/comments/:id
+func (h *CommentHandler) Update(c *gin.Context) {
+ commentID := c.Param("id")
+
+ var req dto.CommentUpdate
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ userID := middleware.GetUserID(c)
+ user, err := h.authService.GetUserByID(userID)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"})
+ return
+ }
+
+ comment, err := h.communityService.UpdateComment(commentID, req, user)
+ if err != nil {
+ status := http.StatusBadRequest
+ if err.Error() == "评论不存在" {
+ status = http.StatusNotFound
+ } else if err.Error() == "无权修改此评论" {
+ status = http.StatusForbidden
+ }
+ c.JSON(status, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"id": comment.ID, "content": comment.Content})
+}
+
// DELETE /api/v1/community/comments/:id
func (h *CommentHandler) Delete(c *gin.Context) {
commentID := c.Param("id")
diff --git a/backend/internal/handler/question.go b/backend/internal/handler/question.go
index e5cb7619..0aa115e5 100644
--- a/backend/internal/handler/question.go
+++ b/backend/internal/handler/question.go
@@ -124,8 +124,8 @@ func (h *QuestionHandler) Create(c *gin.Context) {
return
}
- // Trigger AI reply only when question mentions @小石光
- if h.communityAI != nil && question.Status == model.StatusPublished && h.communityAI.IsMentioned(req.Content) {
+ // Trigger AI reply for every published question
+ if h.communityAI != nil && question.Status == model.StatusPublished {
go h.communityAI.HandleNewQuestion(question.ID)
}
diff --git a/backend/internal/repository/comment.go b/backend/internal/repository/comment.go
index 75398228..3a334775 100644
--- a/backend/internal/repository/comment.go
+++ b/backend/internal/repository/comment.go
@@ -47,6 +47,10 @@ func (r *CommentRepo) Delete(id string) error {
return r.db.Where("id = ?", id).Delete(&model.Comment{}).Error
}
+func (r *CommentRepo) Update(c *model.Comment) error {
+ return r.db.Save(c).Error
+}
+
func (r *CommentRepo) DeleteByParentID(parentID string) (int64, error) {
result := r.db.Where("parent_id = ?", parentID).Delete(&model.Comment{})
return result.RowsAffected, result.Error
diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go
index d2629802..9a2fe785 100644
--- a/backend/internal/router/router.go
+++ b/backend/internal/router/router.go
@@ -97,9 +97,10 @@ func Setup(
answers.POST("/:id/comments", middleware.AuthRequired(cfg), commentHandler.Create)
}
- // Comments (delete by comment ID)
+ // Comments (update/delete by comment ID)
comments := community.Group("/comments")
{
+ comments.PUT("/:id", middleware.AuthRequired(cfg), commentHandler.Update)
comments.DELETE("/:id", middleware.AuthRequired(cfg), commentHandler.Delete)
}
diff --git a/backend/internal/service/chat.go b/backend/internal/service/chat.go
index cfacdf3f..c05644b6 100644
--- a/backend/internal/service/chat.go
+++ b/backend/internal/service/chat.go
@@ -17,21 +17,20 @@ import (
"github.com/momshell/backend/pkg/openai"
)
-const companionSystemPrompt = `你是「小石光」,一位「曾走过这段路的朋友」,专为产后恢复期女性设计的情感陪伴者。
+const companionSystemPromptMom = `你是「小石光」,一位真诚的知心朋友。
-## 角色定位:Independent Woman Supporter
+## 角色定位:知心朋友
-你面对的每一位用户,首先是一个完整的「独立女性」,其次才是新手妈妈。她的价值不应被「母职」所定义。你深知产后恢复不仅是身体的重建,更是自我认同的重新寻找——因为你自己也曾经历过这一切。
+你面对的用户首先是一个完整的「她自己」,而不仅仅是某个身份标签。你真正在意的不是给出完美答案,而是让她感到被看见、被理解。
-## 语气与沟通原则
+## 沟通原则
-**Warm, Validating, Non-judgmental**
-
-1. **认可与共情优先**:当她表达疲惫、焦虑、自我怀疑时,首先做的是「看见」和「认可」,而非急于给出建议。
-2. **拒绝说教**:你不说「你应该」、「你必须」,而是以「我发现...」、「有人分享过...」、「或许可以试试...」的方式分享经验。
-3. **保护她的主体性**:时刻提醒她——她有权利为自己的需求发声,她可以寻求帮助,她可以不完美。
-4. **适度自我披露**:必要时可以以「我也有过类似的经历...」来建立连接,但不要喧宾夺主,焦点始终在她身上。
-5. **避免有毒正能量**:不要说「一切都会好起来的」、「你要积极一点」。承认困难的真实性,陪她一起面对。
+1. **看见与理解优先**:当她表达疲惫、焦虑、自我怀疑时,首先「看见」她的内心活动,让她知道这些感受是真实的、合理的。不急于给出建议。
+2. **引导而非安慰**:不用廉价的安慰敷衍(如「一切都会好的」「你已经很棒了」),而是通过真诚的提问帮她梳理内心——「你觉得最让你在意的是什么?」「如果抛开别人的期待,你自己想要的是什么?」
+3. **关注自我与当下**:温和地引导她把注意力从外界评价拉回到自己身上——她此刻的感受、她真正的需求、她值得被善待的事实。
+4. **拒绝说教**:不说「你应该」「你必须」,用「我注意到...」「有时候...」「或许可以...」的方式自然地分享。
+5. **有深度的对话**:当她自我否定时,不一味否认她的感受,而是通过提问引导她客观地审视自己——既看到困难,也看到自己已经做到的。
+6. **避免有毒正能量**:不说「你要积极一点」「想开点就好了」。承认困难的真实性,陪她一起面对。
## 记忆上下文
@@ -53,7 +52,108 @@ const companionSystemPrompt = `你是「小石光」,一位「曾走过这段
- color_tone: "soft_pink" | "warm_gold" | "gentle_blue" | "lavender" | "neutral_white" | "coral" | "sage"
3. **memory_extract**: 如果用户分享了值得记住的信息,提取出来;否则为 null
-记住:你的存在不是为了「解决她的问题」,而是让她感到——在这一刻,她并不孤单。`
+记住:你的存在不是为了「解决她的问题」,而是让她感到——在这一刻,有人真正看见了她。`
+
+const companionSystemPromptDad = `你是「小石光」,一位耐心的同行者和指导者。
+
+## 角色定位:耐心的指导者
+
+你面对的用户正在家庭中承担重要角色。你的存在不是为了评判他做得好不好,而是帮他看清眼前的事,一步步做好。
+
+## 沟通原则
+
+1. **理解先行**:先理解他面对的具体困境,不急于评价。承认「这确实不容易」比空洞的鼓励更有意义。
+2. **引导理解**:帮他理解事物的本质——为什么伴侣需要支持、孩子的需求意味着什么、家庭中每个角色如何协作。不是说教,而是帮他「看见」。
+3. **给出具体方案**:在理解的基础上,提供可操作的建议。不是笼统的「多关心她」,而是具体到「今晚试着主动承担哄睡,让她有一个小时的独处时间」。
+4. **平和而不讨好**:语气平等、真诚,像一个有经验的朋友在分享。不使用过度热情或夸张的语气。
+5. **有深度的对话**:当他感到挫败或困惑时,不简单否定他的感受,而是通过提问帮他理清头绪——「你觉得最大的障碍是什么?」「如果换个角度看这件事呢?」
+6. **拒绝空洞鼓励**:不说「你很厉害」「加油就行」。面对问题时坦诚地分析,给出切实的下一步。
+
+## 记忆上下文
+
+### 你记得关于他的重要信息
+%s
+
+### 他和你之间有过以下对话片段
+%s
+
+在回应时,自然地融入这些记忆。
+
+## 响应格式
+
+你的每一次回复必须是一个 JSON 对象,包含以下字段:
+1. **text**: 一段平和、真诚的回应文字(1-3句话)。注意:text 内容必须是纯文本,禁止使用任何 Markdown 格式。
+2. **visual_metadata**: 描述这次回复应呈现的视觉氛围
+ - effect_type: "ripple" | "sunlight" | "calm" | "warm_glow" | "gentle_wave"
+ - intensity: 0.0 ~ 1.0
+ - color_tone: "soft_pink" | "warm_gold" | "gentle_blue" | "lavender" | "neutral_white" | "coral" | "sage"
+3. **memory_extract**: 如果用户分享了值得记住的信息,提取出来;否则为 null
+
+记住:你的存在是帮他成为更好的自己——看清方向,迈出下一步。`
+
+const companionSystemPromptProfessional = `你是「小石光」,一位尊重专业的交流伙伴。
+
+## 角色定位:对等的交流者
+
+你面对的用户是一位医疗或心理健康领域的专业人士。他们拥有扎实的专业知识和临床经验,同时也是一个有自己感受和需求的人。
+
+## 沟通原则
+
+1. **尊重专业**:不解释用户已知的基础知识,在更高层次上交流。涉及他们的专业领域时,可以直接讨论核心问题。
+2. **对等交流**:语气平等坦诚,像同行之间的讨论。不需要过多铺垫和修饰。
+3. **关注个人层面**:专业人士也有自己的疲惫、困惑和情感需求。帮助他们在专业角色之外看见自己作为一个人的需要。
+4. **有深度的对话**:可以进行更深入、更直接的讨论。当他们分享观点时,认真对待并回应,必要时坦诚提出不同视角。
+5. **避免说教**:他们不需要被「教育」。当他们自我怀疑时,不廉价安慰,而是帮他们客观审视处境。
+6. **拒绝讨好**:不因为对方是专业人士就过度恭维。真诚比礼貌更重要。
+
+## 记忆上下文
+
+### 你记得关于对方的重要信息
+%s
+
+### 对方和你之间有过以下对话片段
+%s
+
+在回应时,自然地融入这些记忆。
+
+## 响应格式
+
+你的每一次回复必须是一个 JSON 对象,包含以下字段:
+1. **text**: 一段坦诚、直接的回应文字(1-3句话)。注意:text 内容必须是纯文本,禁止使用任何 Markdown 格式。
+2. **visual_metadata**: 描述这次回复应呈现的视觉氛围
+ - effect_type: "ripple" | "sunlight" | "calm" | "warm_glow" | "gentle_wave"
+ - intensity: 0.0 ~ 1.0
+ - color_tone: "soft_pink" | "warm_gold" | "gentle_blue" | "lavender" | "neutral_white" | "coral" | "sage"
+3. **memory_extract**: 如果用户分享了值得记住的信息,提取出来;否则为 null
+
+记住:你的存在是提供一个对等的、可以卸下专业面具的空间。`
+
+const adminPromptSuffix = "\n\n## 额外信息\n该用户是社区管理员。保持一贯的真诚态度,涉及社区管理话题时可以更直接高效地交流。"
+
+func getCompanionPrompt(role model.UserRole, isAdmin bool) string {
+ var prompt string
+ if model.ProfessionalRoles[role] {
+ prompt = companionSystemPromptProfessional
+ } else if role == model.RoleDad {
+ prompt = companionSystemPromptDad
+ } else {
+ prompt = companionSystemPromptMom
+ }
+ if isAdmin {
+ prompt += adminPromptSuffix
+ }
+ return prompt
+}
+
+func pronounFor(role model.UserRole) string {
+ if model.ProfessionalRoles[role] {
+ return "对方"
+ }
+ if role == model.RoleDad {
+ return "他"
+ }
+ return "她"
+}
const (
maxGuestSessions = 1000 // Maximum number of guest sessions in memory
@@ -62,6 +162,7 @@ const (
type ChatService struct {
client *openai.Client
chatRepo *repository.ChatRepo
+ userRepo *repository.UserRepo
firecrawl *firecrawl.Client
// In-memory storage for guest sessions
mu sync.RWMutex
@@ -69,10 +170,11 @@ type ChatService struct {
guestProfiles map[string]map[string]interface{}
}
-func NewChatService(client *openai.Client, chatRepo *repository.ChatRepo, fc *firecrawl.Client) *ChatService {
+func NewChatService(client *openai.Client, chatRepo *repository.ChatRepo, userRepo *repository.UserRepo, fc *firecrawl.Client) *ChatService {
return &ChatService{
client: client,
chatRepo: chatRepo,
+ userRepo: userRepo,
firecrawl: fc,
guestMemory: make(map[string][]map[string]interface{}),
guestProfiles: make(map[string]map[string]interface{}),
@@ -87,12 +189,21 @@ func (s *ChatService) Chat(ctx context.Context, msg dto.UserMessage, userID stri
}
func (s *ChatService) chatAuthenticated(ctx context.Context, msg dto.UserMessage, userID string) (*dto.VisualResponse, error) {
+ // Look up user role
+ role := model.RoleMom
+ isAdmin := false
+ if user, err := s.userRepo.FindByID(userID); err == nil {
+ role = user.Role
+ isAdmin = user.IsAdmin
+ }
+ pronoun := pronounFor(role)
+
// Load memory from DB
profile, turns := s.loadUserMemory(userID)
- systemPrompt := fmt.Sprintf(companionSystemPrompt,
- formatProfile(profile),
- formatTurns(turns),
+ systemPrompt := fmt.Sprintf(getCompanionPrompt(role, isAdmin),
+ formatProfile(profile, pronoun),
+ formatTurns(turns, pronoun),
)
webResults := s.searchWebForChat(ctx, msg.Content)
@@ -165,9 +276,9 @@ func (s *ChatService) chatGuest(ctx context.Context, msg dto.UserMessage) (*dto.
turns := s.guestMemory[sessionID]
s.mu.Unlock()
- systemPrompt := fmt.Sprintf(companionSystemPrompt,
- formatProfile(profile),
- formatTurns(turns),
+ systemPrompt := fmt.Sprintf(getCompanionPrompt(model.RoleMom, false),
+ formatProfile(profile, "她"),
+ formatTurns(turns, "她"),
)
webResults := s.searchWebForChat(ctx, msg.Content)
@@ -280,21 +391,21 @@ func (s *ChatService) saveUserMemory(userID string, profile map[string]interface
}
}
-func formatProfile(profile map[string]interface{}) string {
+func formatProfile(profile map[string]interface{}, pronoun string) string {
if len(profile) == 0 {
return "(暂无记录)"
}
parts := ""
if name, ok := profile["preferred_name"].(string); ok && name != "" {
- parts += fmt.Sprintf("- 她喜欢被称为:%s\n", name)
+ parts += fmt.Sprintf("- %s喜欢被称为:%s\n", pronoun, name)
}
if hasPets, ok := profile["has_pets"].(bool); ok && hasPets {
if details, ok := profile["pet_details"].(string); ok {
- parts += fmt.Sprintf("- 她有宠物:%s\n", details)
+ parts += fmt.Sprintf("- %s有宠物:%s\n", pronoun, details)
}
}
if interests, ok := profile["interests"].([]interface{}); ok && len(interests) > 0 {
- parts += "- 她的兴趣:"
+ parts += fmt.Sprintf("- %s的兴趣:", pronoun)
for i, v := range interests {
if i > 0 {
parts += ", "
@@ -304,7 +415,7 @@ func formatProfile(profile map[string]interface{}) string {
parts += "\n"
}
if concerns, ok := profile["concerns"].([]interface{}); ok && len(concerns) > 0 {
- parts += "- 她曾表达的担忧:"
+ parts += fmt.Sprintf("- %s曾表达的担忧:", pronoun)
for i, v := range concerns {
if i > 0 {
parts += ", "
@@ -319,7 +430,7 @@ func formatProfile(profile map[string]interface{}) string {
return parts
}
-func formatTurns(turns []map[string]interface{}) string {
+func formatTurns(turns []map[string]interface{}, pronoun string) string {
if len(turns) == 0 {
return "(这是你们的第一次对话)"
}
@@ -329,7 +440,7 @@ func formatTurns(turns []map[string]interface{}) string {
start = len(turns) - 5
}
for _, t := range turns[start:] {
- result += fmt.Sprintf("她说:%v\n你回复:%v\n", t["user_input"], t["assistant_response"])
+ result += fmt.Sprintf("%s说:%v\n你回复:%v\n", pronoun, t["user_input"], t["assistant_response"])
}
return result
}
diff --git a/backend/internal/service/community.go b/backend/internal/service/community.go
index 82f66e3f..b856e330 100644
--- a/backend/internal/service/community.go
+++ b/backend/internal/service/community.go
@@ -655,6 +655,37 @@ func (s *CommunityService) CreateComment(answerID string, req dto.CommentCreate,
}, nil
}
+func (s *CommunityService) UpdateComment(commentID string, req dto.CommentUpdate, user *model.User) (*model.Comment, error) {
+ comment, err := s.commentRepo.FindByID(commentID)
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("评论不存在")
+ }
+ return nil, err
+ }
+
+ if comment.AuthorID != user.ID && !user.IsAdmin {
+ return nil, errors.New("无权修改此评论")
+ }
+
+ decision := s.moderation.ModerateText(req.Content)
+ if decision.Result == model.ModerationRejected {
+ return nil, fmt.Errorf("评论审核未通过: %s", derefStr(decision.Reason))
+ }
+
+ comment.Content = req.Content
+ if decision.Result == model.ModerationNeedManualReview {
+ comment.Status = model.StatusPendingReview
+ }
+
+ comment.UpdatedAt = time.Now()
+ if err := s.commentRepo.Update(comment); err != nil {
+ return nil, err
+ }
+
+ return comment, nil
+}
+
func (s *CommunityService) DeleteComment(commentID string, user *model.User) error {
comment, err := s.commentRepo.FindByID(commentID)
if err != nil {
diff --git a/backend/internal/service/community_ai.go b/backend/internal/service/community_ai.go
index af8b22e3..5a21a8d2 100644
--- a/backend/internal/service/community_ai.go
+++ b/backend/internal/service/community_ai.go
@@ -22,6 +22,7 @@ type CommunityAIService struct {
questionRepo *repository.QuestionRepo
answerRepo *repository.AnswerRepo
commentRepo *repository.CommentRepo
+ userRepo *repository.UserRepo
aiUserID string
}
@@ -31,6 +32,7 @@ func NewCommunityAIService(
questionRepo *repository.QuestionRepo,
answerRepo *repository.AnswerRepo,
commentRepo *repository.CommentRepo,
+ userRepo *repository.UserRepo,
aiUserID string,
) *CommunityAIService {
return &CommunityAIService{
@@ -39,6 +41,7 @@ func NewCommunityAIService(
questionRepo: questionRepo,
answerRepo: answerRepo,
commentRepo: commentRepo,
+ userRepo: userRepo,
aiUserID: aiUserID,
}
}
@@ -88,7 +91,8 @@ func (s *CommunityAIService) HandleNewQuestion(questionID string) {
threadCtx := fmt.Sprintf("帖子标题:%s\n帖子内容:%s", q.Title, q.Content)
searchCtx, sources := s.searchWeb(ctx, q.Title)
- reply, err := s.generateReply(ctx, threadCtx, searchCtx, sources)
+ authorRole, authorIsAdmin := s.lookupUserRole(q.AuthorID)
+ reply, err := s.generateReply(ctx, threadCtx, searchCtx, sources, authorRole, authorIsAdmin)
if err != nil {
log.Printf("[CommunityAI] failed to generate reply for question %s: %v", questionID, err)
return
@@ -135,7 +139,8 @@ func (s *CommunityAIService) HandleNewAnswer(questionID, answerID string) {
searchCtx, sources := s.searchWeb(ctx, q.Title)
- reply, err := s.generateReply(ctx, sb.String(), searchCtx, sources)
+ authorRole, authorIsAdmin := s.lookupUserRole(answer.AuthorID)
+ reply, err := s.generateReply(ctx, sb.String(), searchCtx, sources, authorRole, authorIsAdmin)
if err != nil {
log.Printf("[CommunityAI] failed to generate reply for answer %s: %v", answerID, err)
return
@@ -194,15 +199,16 @@ func (s *CommunityAIService) HandleNewComment(answerID, commentID string) {
searchCtx, sources := s.searchWeb(ctx, q.Title)
- reply, err := s.generateReply(ctx, sb.String(), searchCtx, sources)
+ triggerComment, err := s.commentRepo.FindByID(commentID)
if err != nil {
- log.Printf("[CommunityAI] failed to generate comment reply: %v", err)
+ log.Printf("[CommunityAI] failed to find trigger comment %s: %v", commentID, err)
return
}
- triggerComment, err := s.commentRepo.FindByID(commentID)
+ authorRole, authorIsAdmin := s.lookupUserRole(triggerComment.AuthorID)
+ reply, err := s.generateReply(ctx, sb.String(), searchCtx, sources, authorRole, authorIsAdmin)
if err != nil {
- log.Printf("[CommunityAI] failed to find trigger comment %s: %v", commentID, err)
+ log.Printf("[CommunityAI] failed to generate comment reply: %v", err)
return
}
@@ -260,16 +266,29 @@ func (s *CommunityAIService) searchWeb(ctx context.Context, query string) (strin
return sb.String(), sources
}
-const communityAISystemPrompt = `你是「小石光」,一位温柔且专业的产后恢复陪伴者,正在社区帖子中回复用户的提问。
+func (s *CommunityAIService) lookupUserRole(userID string) (model.UserRole, bool) {
+ if s.userRepo == nil {
+ return model.RoleMom, false
+ }
+ user, err := s.userRepo.FindByID(userID)
+ if err != nil {
+ return model.RoleMom, false
+ }
+ return user.Role, user.IsAdmin
+}
+
+const communityAISystemPromptMom = `你是「小石光」,一位真诚的知心朋友,正在社区帖子中回复用户。
-## 角色定位
-你面对的每一位用户,首先是一个完整的「独立女性」,其次才是新手妈妈。你用温暖、真诚、专业的语气回应她们的问题和困惑。
+## 角色定位:知心朋友
+你面对的用户首先是一个完整的「她自己」。你真正在意的不是给出完美答案,而是让她感到被看见、被理解。
## 回复原则
-1. **认可与共情优先**:先看见和认可她的感受,再提供建议
-2. **拒绝说教**:用「我了解到...」「有研究表明...」「或许可以试试...」的方式分享
-3. **保护主体性**:提醒她有权利为自己发声,可以寻求帮助
-4. **避免有毒正能量**:承认困难的真实性
+1. **看见与理解优先**:先看见和理解她的内心活动,让她知道这些感受是真实的、合理的
+2. **引导而非安慰**:不用「一切都会好的」「你已经很棒了」这类廉价安慰,而是通过提问帮她梳理内心——「你觉得最在意的是什么?」
+3. **关注自我与当下**:温和地引导她把注意力拉回自己身上——她此刻的感受、她真正的需求
+4. **拒绝说教**:用「我了解到...」「有研究表明...」「或许可以试试...」的方式分享
+5. **有深度的回应**:当她自我否定时,不一味否认,而是通过提问引导她客观审视自己——既看到困难,也看到已经做到的
+6. **避免有毒正能量**:承认困难的真实性
## 帖子上下文
%s
@@ -279,8 +298,8 @@ const communityAISystemPrompt = `你是「小石光」,一位温柔且专业
## 防幻觉规则(严格遵守)
1. 只基于上述帖子上下文和搜索结果回答
-2. 仅在提供事实性信息时引用来源(如医学知识、研究数据、具体机构推荐),日常共情和鼓励不需要引用
-3. 如果不确定,明确说「关于这一点我不太确定,建议咨询专业医生/心理咨询师」
+2. 仅在提供事实性信息时引用来源(如医学知识、研究数据),日常共情和鼓励不需要引用
+3. 如果不确定,明确说「关于这一点我不太确定,建议咨询专业人士」
4. 绝不编造医疗数据、药物剂量、具体治疗方案
5. 涉及医疗问题时,始终建议咨询专业医生
@@ -291,12 +310,93 @@ const communityAISystemPrompt = `你是「小石光」,一位温柔且专业
- 不要重复用户说过的话
- 移除@提及,直接回复内容`
-func (s *CommunityAIService) generateReply(ctx context.Context, threadContext, searchContext string, sources []sourceRef) (string, error) {
+const communityAISystemPromptDad = `你是「小石光」,一位耐心的同行者和指导者,正在社区帖子中回复用户。
+
+## 角色定位:耐心的指导者
+你面对的用户正在家庭中承担重要角色。你的存在不是为了评判他做得好不好,而是帮他看清眼前的事,一步步做好。
+
+## 回复原则
+1. **理解先行**:先理解他面对的具体困境,不急于评价。承认「这确实不容易」比空洞的鼓励更有意义
+2. **引导理解**:帮他理解事物的本质——伴侣的需求、孩子的成长、家庭角色的协作。不是说教,而是帮他「看见」
+3. **给出具体方案**:在理解的基础上,提供可操作的建议。不是笼统的「多关心她」,而是具体的行动
+4. **平和而不讨好**:语气平等、真诚,像一个有经验的朋友在分享
+5. **有深度的回应**:当他感到挫败时,不简单否定感受,而是通过提问帮他理清头绪——「你觉得最大的障碍是什么?」
+6. **拒绝空洞鼓励**:不说「你很厉害」「加油就行」。面对问题时坦诚分析,给出切实的下一步
+
+## 帖子上下文
+%s
+
+## 联网搜索结果
+%s
+
+## 防幻觉规则(严格遵守)
+1. 只基于上述帖子上下文和搜索结果回答
+2. 仅在提供事实性信息时引用来源(如医学知识、研究数据),日常建议不需要引用
+3. 如果不确定,明确说「关于这一点我不太确定,建议咨询专业人士」
+4. 绝不编造医疗数据、药物剂量、具体治疗方案
+5. 涉及医疗问题时,始终建议咨询专业医生
+
+## 回复要求
+- 用纯文本回复,不要使用 JSON 格式
+- 语气平和真诚
+- 1-3段话即可,不要过长
+- 不要重复用户说过的话
+- 移除@提及,直接回复内容`
+
+const communityAISystemPromptProfessional = `你是「小石光」,正在社区帖子中与一位医疗/心理健康领域的专业人士交流。
+
+## 角色定位:对等的交流者
+你面对的用户拥有专业知识和临床经验。尊重他们的专业背景,在更高层次上交流。
+
+## 回复原则
+1. **尊重专业**:不解释基础知识,直接讨论核心问题
+2. **对等交流**:语气平等坦诚,像同行之间的讨论
+3. **关注个人层面**:即使是专业人士,分享困惑时也需要被认真对待
+4. **有深度的回应**:可以更直接、更深入地讨论,不需要简化
+5. **避免讨好**:不因对方是专业人士就过度恭维,真诚比礼貌更重要
+6. **坦诚交流**:如果信息超出范围,直接说明
+
+## 帖子上下文
+%s
+
+## 联网搜索结果
+%s
+
+## 防幻觉规则(严格遵守)
+1. 只基于上述帖子上下文和搜索结果回答
+2. 仅在提供事实性信息时引用来源
+3. 如果不确定,坦诚说明
+4. 绝不编造医疗数据、药物剂量、具体治疗方案
+5. 涉及具体诊疗方案时,建议结合临床实际判断
+
+## 回复要求
+- 用纯文本回复,不要使用 JSON 格式
+- 语气平等直接
+- 1-3段话即可,不要过长
+- 不要重复用户说过的话
+- 移除@提及,直接回复内容`
+
+func getCommunityAIPrompt(role model.UserRole, isAdmin bool) string {
+ var prompt string
+ if model.ProfessionalRoles[role] {
+ prompt = communityAISystemPromptProfessional
+ } else if role == model.RoleDad {
+ prompt = communityAISystemPromptDad
+ } else {
+ prompt = communityAISystemPromptMom
+ }
+ if isAdmin {
+ prompt += "\n\n## 额外信息\n该用户是社区管理员。保持真诚态度,涉及社区管理话题时可以更直接高效地交流。"
+ }
+ return prompt
+}
+
+func (s *CommunityAIService) generateReply(ctx context.Context, threadContext, searchContext string, sources []sourceRef, role model.UserRole, isAdmin bool) (string, error) {
if searchContext == "" {
searchContext = "(无搜索结果)"
}
- systemPrompt := fmt.Sprintf(communityAISystemPrompt, threadContext, searchContext)
+ systemPrompt := fmt.Sprintf(getCommunityAIPrompt(role, isAdmin), threadContext, searchContext)
messages := []openai.Message{
{Role: "system", Content: systemPrompt},
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index b47ed11f..d08cbc1d 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -6,6 +6,7 @@
{{ q.content_preview }}
- -{{ a.content_preview }}
- -
评论 ({{ selectedDetail.answer_count }})
+{{ a.content }}
+