落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
232 lines
5.7 KiB
Go
232 lines
5.7 KiB
Go
// Package ask builds deterministic growth-assistant replies (rule fallback).
|
|
// Copy follows .ai/product/lexicon.md — companion for self-understanding, not fortune-telling.
|
|
package ask
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/yuxingu/digital-psychology/apps/api/internal/portrait"
|
|
)
|
|
|
|
// ReplyInput is context for a single assistant reply.
|
|
type ReplyInput struct {
|
|
DisplayName string
|
|
BirthDate time.Time
|
|
Relation string // self | other
|
|
Scene string
|
|
UserMessage string
|
|
}
|
|
|
|
// BuildReply returns a profile-aware assistant message with actionable detail.
|
|
func BuildReply(in ReplyInput) string {
|
|
name := in.DisplayName
|
|
if name == "" {
|
|
if in.Relation == "other" {
|
|
name = "TA"
|
|
} else {
|
|
name = "你"
|
|
}
|
|
}
|
|
out := portrait.Build(in.BirthDate, name)
|
|
style := str(out.Summary["style_label"])
|
|
if style == "" {
|
|
style = str(out.Summary["headline"])
|
|
}
|
|
one := str(out.Summary["one_liner"])
|
|
tip := str(out.Summary["life_tip"])
|
|
overview := str(out.Summary["overview"])
|
|
|
|
sceneHint := sceneLine(in.Scene, in.UserMessage)
|
|
focus := detectFocus(in.UserMessage)
|
|
|
|
sectionBody := sectionByFocus(out.Detail, focus)
|
|
scripts := strSlice(out.Detail["conversation_scripts"])
|
|
scriptLine := ""
|
|
if len(scripts) > 0 {
|
|
scriptLine = "可以试着说:「" + scripts[0] + "」"
|
|
}
|
|
plan := firstPlan(out.Detail["growth_plan"])
|
|
|
|
var body string
|
|
switch focus {
|
|
case "relation":
|
|
body = fmt.Sprintf(
|
|
"结合「%s」偏「%s」的档案:%s\n\n关系侧重点:%s\n\n小建议:%s\n%s\n本周可做:%s",
|
|
name, style, one, pick(sectionBody, "在关系里先复述对方感受,再表达需要。"), tip, scriptLine, pick(plan, tip),
|
|
)
|
|
case "career":
|
|
body = fmt.Sprintf(
|
|
"从「%s / %s」看职业节奏:%s\n\n%s\n\n可执行:%s\n本周:%s",
|
|
name, style, one, pick(sectionBody, overview), tip, pick(plan, "选一件能发挥你节奏优势的小事推进。"),
|
|
)
|
|
case "emotion":
|
|
body = fmt.Sprintf(
|
|
"我听到你在整理情绪。对照「%s」:%s\n\n%s\n\n调节建议:%s\n%s\n本周:%s",
|
|
style, one, pick(sectionBody, "先命名感受,再决定行动。"), tip, scriptLine, pick(plan, "给自己 10 分钟不评判地写下此刻感受。"),
|
|
)
|
|
case "life":
|
|
body = fmt.Sprintf(
|
|
"关于生活节奏(%s):%s\n\n%s\n\n今日建议:%s\n本周:%s",
|
|
style, one, pick(sectionBody, overview), tip, pick(plan, tip),
|
|
)
|
|
default:
|
|
body = fmt.Sprintf(
|
|
"我是愈心谷的成长助手,会结合档案陪你一起看。\n\n「%s」更偏「%s」:%s\n\n%s\n\n生活建议:%s\n%s\n本周可做:%s",
|
|
name, style, one, trimRunes(overview, 120), tip, scriptLine, pick(plan, tip),
|
|
)
|
|
}
|
|
|
|
disclaimer := "以上是自我探索与生活方式参考,不构成医疗或占卜预测。"
|
|
if sceneHint != "" {
|
|
return sceneHint + "\n\n" + strings.TrimSpace(body) + "\n\n" + disclaimer
|
|
}
|
|
return strings.TrimSpace(body) + "\n\n" + disclaimer
|
|
}
|
|
|
|
func sectionByFocus(detail map[string]any, focus string) string {
|
|
titleHint := map[string]string{
|
|
"relation": "关系",
|
|
"career": "事业",
|
|
"emotion": "情绪",
|
|
"life": "生活",
|
|
"self": "性格",
|
|
}[focus]
|
|
secs, ok := detail["sections"].([]map[string]any)
|
|
if !ok {
|
|
// also []any after some encodings
|
|
if raw, ok2 := detail["sections"].([]any); ok2 {
|
|
for _, item := range raw {
|
|
m, _ := item.(map[string]any)
|
|
if strings.Contains(str(m["title"]), titleHint) {
|
|
return str(m["body"])
|
|
}
|
|
}
|
|
}
|
|
switch focus {
|
|
case "relation":
|
|
return str(detail["relation_style"])
|
|
case "career", "self":
|
|
return str(detail["behavior_pattern"])
|
|
default:
|
|
return str(detail["growth_direction"])
|
|
}
|
|
}
|
|
for _, s := range secs {
|
|
if strings.Contains(str(s["title"]), titleHint) {
|
|
return str(s["body"])
|
|
}
|
|
}
|
|
if len(secs) > 0 {
|
|
return str(secs[0]["body"])
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func firstPlan(v any) string {
|
|
arr, ok := v.([]map[string]any)
|
|
if ok && len(arr) > 0 {
|
|
return str(arr[0]["focus"])
|
|
}
|
|
raw, ok := v.([]any)
|
|
if ok && len(raw) > 0 {
|
|
if m, ok := raw[0].(map[string]any); ok {
|
|
return str(m["focus"])
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func pick(a, b string) string {
|
|
if strings.TrimSpace(a) != "" {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func trimRunes(s string, n int) string {
|
|
r := []rune(s)
|
|
if len(r) <= n {
|
|
return s
|
|
}
|
|
return string(r[:n]) + "…"
|
|
}
|
|
|
|
func strSlice(v any) []string {
|
|
if arr, ok := v.([]string); ok {
|
|
return arr
|
|
}
|
|
raw, ok := v.([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(raw))
|
|
for _, x := range raw {
|
|
if s, ok := x.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sceneLine(scene, msg string) string {
|
|
s := strings.TrimSpace(scene)
|
|
if s == "" {
|
|
s = detectScene(msg)
|
|
}
|
|
switch s {
|
|
case "self":
|
|
return "场景:认识自己"
|
|
case "relation":
|
|
return "场景:理解关系"
|
|
case "career":
|
|
return "场景:职业探索"
|
|
case "emotion":
|
|
return "场景:情绪整理"
|
|
case "life":
|
|
return "场景:生活建议"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func detectScene(msg string) string {
|
|
return detectFocus(msg)
|
|
}
|
|
|
|
func detectFocus(msg string) string {
|
|
m := strings.ToLower(msg)
|
|
switch {
|
|
case containsAny(m, "关系", "相处", "伴侣", "朋友", "沟通", "TA", "他", "她"):
|
|
return "relation"
|
|
case containsAny(m, "工作", "职业", "面试", "事业", "选择"):
|
|
return "career"
|
|
case containsAny(m, "情绪", "焦虑", "难过", "压力", "心情", "害怕"):
|
|
return "emotion"
|
|
case containsAny(m, "作息", "睡眠", "习惯", "生活", "饮食", "运动"):
|
|
return "life"
|
|
default:
|
|
return "self"
|
|
}
|
|
}
|
|
|
|
func containsAny(s string, words ...string) bool {
|
|
for _, w := range words {
|
|
if strings.Contains(s, w) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func str(v any) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
return fmt.Sprint(v)
|
|
}
|