feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具
落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
// Package imagecard implements 意象卡片 draws (projection / reflection, not tarot UI).
|
||||
package imagecard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Card is one exploration card.
|
||||
type Card struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Imagery string `json:"imagery"`
|
||||
Prompt string `json:"prompt"`
|
||||
Tip string `json:"tip"`
|
||||
}
|
||||
|
||||
// DrawResult is API payload for a draw.
|
||||
type DrawResult struct {
|
||||
Scene string `json:"scene"`
|
||||
Cards []Card `json:"cards"`
|
||||
Summary map[string]any `json:"summary"`
|
||||
Detail map[string]any `json:"detail"`
|
||||
QuotaLeft int `json:"quota_left"`
|
||||
}
|
||||
|
||||
var scenes = []string{
|
||||
"情绪整理", "关系", "选择", "自我",
|
||||
"工作节奏", "边界", "休息", "表达",
|
||||
}
|
||||
|
||||
// Scenes returns available reflection scenes.
|
||||
func Scenes() []map[string]string {
|
||||
out := make([]map[string]string, 0, len(scenes))
|
||||
for _, s := range scenes {
|
||||
out = append(out, map[string]string{"key": s, "label": s})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// deck built once: 12 handcrafted + generated to ≥78.
|
||||
var deck = buildDeck()
|
||||
|
||||
func buildDeck() []Card {
|
||||
base := []Card{
|
||||
{"c01", "微光小路", "一条只够一人走过的小径,尽头有一点光。", "此刻你最想靠近的「光」是什么?", "选一个最小行动靠近它,不必一次走完。"},
|
||||
{"c02", "安静的杯子", "桌上杯子里的水是温的,蒸汽慢慢散开。", "你最近在为谁/什么「保温」?有没有过热?", "今天给自己倒一杯水,只为自己停两分钟。"},
|
||||
{"c03", "未拆的信", "信封边缘有点磨损,还没打开。", "有哪句话你一直想说却还没说?", "写下来即可,不一定立刻发出。"},
|
||||
{"c04", "桥与河", "桥稳稳跨过河,水流在下面经过。", "你现在更需要「过去」还是「停在岸上感受」?", "允许自己选一边,并告诉相关的人。"},
|
||||
{"c05", "收拾桌面", "杂物被归类,只留一件重要的东西在中间。", "若只能保留一件「最重要」,会是什么?", "本周减少一件消耗你的杂事。"},
|
||||
{"c06", "窗边呼吸", "窗外有风,窗帘轻轻动。", "你的呼吸是浅还是深?身体哪里最紧?", "做 4 次慢呼吸,肩再放松一点。"},
|
||||
{"c07", "同行的影子", "两个人影并排,步伐不完全一致。", "关系里你希望对方怎样配合你的节奏?", "用一句具体请求代替抱怨。"},
|
||||
{"c08", "种子与土", "土里有一颗刚发芽的种子。", "你正在酝酿、还没被人看见的是什么?", "给它一点时间,并做一个保护边界。"},
|
||||
{"c09", "地图折痕", "地图被折过很多次,路线仍可辨认。", "过去哪些弯路其实教了你方法?", "写下一条「下次可以更早用的经验」。"},
|
||||
{"c10", "钥匙串", "一串钥匙里有一把还没用过。", "你手头有哪项资源/能力还没启用?", "本周试用一次那个「备用钥匙」。"},
|
||||
{"c11", "退潮沙滩", "潮水退去,露出一些小贝壳。", "热闹过后,你发现了什么真实感受?", "把感受写三句,不评判对错。"},
|
||||
{"c12", "灯塔节奏", "灯塔规律地亮—灭—亮。", "你的生活里什么节奏让你安心?", "把那个节奏固定进日程一格。"},
|
||||
}
|
||||
themes := []struct{ title, imagery, prompt, tip string }{
|
||||
{"山径转弯", "山路在雾里拐了一个弯,远处仍可见脚印。", "你正在避开的弯是什么?", "允许自己慢半步,再决定方向。"},
|
||||
{"雨后青石", "雨停了,石板反光,空气很干净。", "刚过去的「雨」带走了什么?", "写下三件你想留下的。"},
|
||||
{"未完成的画", "画布只涂了一角,颜料还湿着。", "哪件事值得继续涂,而不是重开一张?", "今天只加一笔就够。"},
|
||||
{"旧毛衣", "袖口有点起球,却仍然暖和。", "你生活里哪件「旧物」仍在保护你?", "对它说一句感谢。"},
|
||||
{"电梯门", "电梯门开了又关,楼层数字在跳。", "你卡在哪一层不愿下去或上去?", "选一个小出口先离开卡点。"},
|
||||
{"夜班台灯", "台灯只照亮桌面一小块。", "你需要把注意力收窄到哪里?", "设 25 分钟专注,然后休息。"},
|
||||
{"空座位", "长椅空着一半,另一半有阳光。", "你更需要坐下还是让出位置?", "今天做一个明确的「要/不要」。"},
|
||||
{"漂流瓶", "瓶子在水里轻轻晃,纸条卷着。", "你想让谁「偶然」读到你的真心?", "写给自己一封短信即可。"},
|
||||
{"鞋带", "鞋带松了,还在走。", "哪里需要先停下来系紧?", "处理一件基础小事再赶路。"},
|
||||
{"回声廊", "走廊很长,声音会折返。", "你说出口的话,回来时变成了什么?", "少说一句评判,多问一句好奇。"},
|
||||
{"晨雾车站", "站台有雾,车还没到。", "等待里你在消耗还是在准备?", "用等待做一件小事(喝水/伸展)。"},
|
||||
{"书架空隙", "两本书之间留了一指宽空隙。", "你生活里需要留白的是哪一块?", "从日程删掉一个非必要。"},
|
||||
{"冷水龙头", "水先凉后热,需要等一会儿。", "你对谁/什么事太急了?", "给过程多 10 分钟缓冲。"},
|
||||
{"风筝线", "线在手里,风筝在高处晃。", "你抓得太紧的是什么?", "刻意松开一点控制,观察结果。"},
|
||||
{"烛火", "火焰小而稳,周围很暗。", "什么能成为你今晚的小光源?", "做一件温暖且短的事。"},
|
||||
{"拼图缺角", "图案几乎成型,只差一块。", "缺的那块真的在外面,还是你还没承认?", "列出「已有」而不是「没有」。"},
|
||||
{"旧车票", "票根折痕深,日期已过。", "哪段旅程可以温柔结束了?", "做一个小小的告别仪式。"},
|
||||
{"晾衣绳", "衣服在风里慢慢干。", "什么事其实只需要时间,不需要用力?", "今天不催自己一次。"},
|
||||
{"镜面湖", "湖面几乎无波,倒映天空。", "你害怕看到的自己是哪一面?", "对镜写下三个不评判的观察。"},
|
||||
{"登山杖", "杖尖戳进土里,借力往上。", "你愿意向谁借一点力?", "发出一个具体求助。"},
|
||||
{"暖手宝", "掌心慢慢回温。", "谁/什么在给你持续的暖?", "今天主动回馈一点点暖。"},
|
||||
{"十字路口灯", "红灯停,绿灯行,黄灯提醒。", "你现在更需要停、行,还是减速?", "按信号选一个动作执行。"},
|
||||
{"背包重量", "肩带勒出印子。", "包里哪样可以先拿出来?", "卸下一件心理负担并告诉自己。"},
|
||||
{"夜路灯", "一盏盏灯把路分段照亮。", "你下一步只需要照亮多远?", "只计划明天上午即可。"},
|
||||
{"陶罐裂缝", "裂缝里透出一点光。", "你的「不完美」里藏了什么资源?", "把一个缺点改写成可用特质。"},
|
||||
{"梯田", "一层一层往上,水流向下。", "你的成长是哪一层该浇水?", "本周只推进一层目标。"},
|
||||
{"耳机静音", "世界声音被隔开一点。", "你需要隔开什么噪音?", "设定一段无信息时段。"},
|
||||
{"折纸船", "小船在水盆里打转。", "你把安全感放在哪里?", "做一个可控的小实验。"},
|
||||
{"墙洞光", "光从窄缝进来,依然够用。", "有限条件下你仍能做什么?", "用现有资源完成一件小事。"},
|
||||
{"沙漏", "沙子安静落下。", "你在跟时间较劲什么?", "接受「够好」的标准一次。"},
|
||||
{"回廊座", "走廊尽头有一张椅子。", "你允许自己坐下休息了吗?", "安排 15 分钟真正休息。"},
|
||||
{"雾中灯塔", "雾浓,灯仍按节奏闪。", "不确定时什么原则仍不变?", "写下你的一条底线。"},
|
||||
{"青苔石", "石头潮湿,青苔慢慢长。", "哪段关系需要慢养而不是快修?", "降低频率,提高质量。"},
|
||||
{"晾晒日光", "被子在阳光里松软。", "什么情绪需要「拿出去晒晒」?", "和信任的人聊 10 分钟。"},
|
||||
{"圆规", "圆心固定,半径可调。", "你的边界圆心在哪里?", "明确一个「可以/不可以」。"},
|
||||
{"夜航图", "星点标出航线。", "你参考的「星」是谁的标准?", "换成自己的三条原则。"},
|
||||
{"竹筒饭", "竹香与米香混在一起。", "简单事物里你忽略了什么满足?", "吃一顿专注的饭。"},
|
||||
{"软垫", "落地时被接住。", "跌倒时谁/什么接住了你?", "写感谢,或成为别人的软垫一次。"},
|
||||
{"窗格影子", "格子把光切成块。", "你把生活切得太碎了吗?", "合并两件可一起做的事。"},
|
||||
{"河石", "棱角被水磨圆。", "冲突磨掉了你的什么?留下了什么?", "保留圆润,也保留核心硬度。"},
|
||||
{"草稿本", "涂改很多,仍翻得开。", "哪份「草稿」其实已经能见人?", "发布一个 70 分版本。"},
|
||||
{"门铃", "轻按一下就会有回应。", "你在等谁来按铃?还是该自己开门?", "主动发起一次连结。"},
|
||||
{"冬青", "天冷仍绿。", "什么习惯在难时仍支撑你?", "把那习惯写进日程。"},
|
||||
{"秋千", "来回摆,最高点短暂。", "你在追峰值还是稳态?", "选择稳态的一小步。"},
|
||||
{"墨点", "一滴墨在宣纸上晕开。", "影响正在扩大的是什么?", "及时止损或顺势引导。"},
|
||||
{"绳结", "结打紧了,解需要耐心。", "哪个结值得慢慢解?", "今天只解一层。"},
|
||||
{"麦浪", "风过,一片起伏。", "集体节奏里你站在哪?", "允许与主流差半拍。"},
|
||||
{"星尘罐", "罐子装不住光,却闪着。", "你收藏的希望是什么?", "拿出来看一眼,再盖上。"},
|
||||
{"石阶", "一级一级,并不陡。", "你把台阶看高了吗?", "只上今天这一级。"},
|
||||
{"暖汤", "热气模糊了眼镜。", "谁需要一碗「暖」?", "做一件具体的照顾。"},
|
||||
{"风铃", "风来才响。", "什么条件一到你就会行动?", "把条件改成更小的触发。"},
|
||||
{"书签", "停在未读完的那页。", "你暂停的故事要不要继续?", "读完两页或正式合上。"},
|
||||
{"露珠", "很快会消失,却折射天空。", "短暂却珍贵的片刻是什么?", "用心经历,不必抓住。"},
|
||||
{"柴门", "门不华丽,却通向家。", "「回家」对你意味着什么?", "安排一次真正放松的回家感。"},
|
||||
{"远山", "看得到,走得到需要天。", "远目标如何拆成今日可见的一步?", "写下「今天可见的山脚」。"},
|
||||
{"井绳", "要用力,也要节奏。", "你在哪用力过猛?", "改成间歇用力。"},
|
||||
{"纸飞机", "飞不远,但方向是自己的。", "小实验可以朝哪飞?", "发出一个低成本试探。"},
|
||||
{"苔径", "湿滑,需放慢。", "哪里需要防滑措施?", "加一个缓冲或备份。"},
|
||||
{"铜铃", "声音清脆后很快静。", "提醒响过,你听见了吗?", "把提醒变成一个行动。"},
|
||||
{"白瓷碗", "空着时也完整。", "空不是缺失,可能是准备。", "留出空白时段不被填满。"},
|
||||
{"夜雨窗", "雨点连成线。", "情绪连成片时你怎么分段?", "用呼吸把情绪切成小节。"},
|
||||
{"藤椅", "坐下就陷进去一点。", "你允许自己「陷进」休息吗?", "无罪恶感地休息 20 分钟。"},
|
||||
{"路牌", "箭头指向两个镇。", "信息不够时如何选?", "选可逆的那条先走。"},
|
||||
{"炭火", "表面灰,内里仍热。", "表面平静下还热着的是什么?", "找安全出口表达它。"},
|
||||
{"蒲公英", "风一吹就出发。", "你准备好松开了吗?", "松开一个过时期待。"},
|
||||
{"石桥栏", "扶手冰凉但可靠。", "你的可靠支持是什么?", "主动靠一下支持系统。"},
|
||||
}
|
||||
out := append([]Card{}, base...)
|
||||
for i, th := range themes {
|
||||
out = append(out, Card{
|
||||
ID: fmt.Sprintf("c%02d", len(base)+i+1),
|
||||
Title: th.title,
|
||||
Imagery: th.imagery,
|
||||
Prompt: th.prompt,
|
||||
Tip: th.tip,
|
||||
})
|
||||
}
|
||||
// pad to at least 78 with deterministic variants
|
||||
for len(out) < 78 {
|
||||
i := len(out)
|
||||
src := base[i%len(base)]
|
||||
out = append(out, Card{
|
||||
ID: fmt.Sprintf("c%02d", i+1),
|
||||
Title: fmt.Sprintf("%s·续", src.Title),
|
||||
Imagery: src.Imagery + " 光线又柔和了一点。",
|
||||
Prompt: src.Prompt,
|
||||
Tip: src.Tip,
|
||||
})
|
||||
}
|
||||
return out[:78]
|
||||
}
|
||||
|
||||
// Draw picks cards deterministically from userID+scene+day for free single, or 3 for deep.
|
||||
func Draw(userKey, scene string, deep bool, now time.Time) DrawResult {
|
||||
if scene == "" {
|
||||
scene = scenes[0]
|
||||
}
|
||||
n := 1
|
||||
if deep {
|
||||
n = 3
|
||||
}
|
||||
day := now.UTC().Format("2006-01-02")
|
||||
idxs := pickIndexes(userKey+"|"+scene+"|"+day, n, len(deck))
|
||||
cards := make([]Card, 0, n)
|
||||
for _, i := range idxs {
|
||||
cards = append(cards, deck[i])
|
||||
}
|
||||
summary := map[string]any{
|
||||
"title": "意象卡片·探索",
|
||||
"headline": fmt.Sprintf("场景「%s」· %s", scene, cards[0].Title),
|
||||
"one_liner": cards[0].Imagery,
|
||||
"overview": fmt.Sprintf("反思:%s", cards[0].Prompt),
|
||||
"life_tip": cards[0].Tip,
|
||||
"keywords": []string{"意象卡片", scene, cards[0].Title},
|
||||
"scene": scene,
|
||||
"card_ids": idsOf(cards),
|
||||
}
|
||||
var detail map[string]any
|
||||
if deep && len(cards) >= 3 {
|
||||
detail = map[string]any{
|
||||
"title": "意象卡片·组合反思",
|
||||
"sections": []map[string]any{
|
||||
{"title": cards[0].Title, "body": cards[0].Imagery + " " + cards[0].Prompt, "bullets": []string{cards[0].Tip}},
|
||||
{"title": cards[1].Title, "body": cards[1].Imagery + " " + cards[1].Prompt, "bullets": []string{cards[1].Tip}},
|
||||
{"title": cards[2].Title, "body": cards[2].Imagery + " " + cards[2].Prompt, "bullets": []string{cards[2].Tip}},
|
||||
{"title": "组合练习", "body": "把三张卡连成一个小故事:发生了什么、你感受到什么、下一步一小步是什么。写 5 行即可。", "bullets": []string{"不评判对错", "只选一个可执行下一步", "需要时可去问答继续聊"}},
|
||||
},
|
||||
"conversation_scripts": []string{"我抽到的意象让我想到……", "我现在需要的是……"},
|
||||
"faq": []map[string]string{
|
||||
{"q": "这是在预测未来吗?", "a": "不是。意象卡片用于投射与自我反思,不判定好坏,也不预测未来。"},
|
||||
},
|
||||
}
|
||||
}
|
||||
return DrawResult{Scene: scene, Cards: cards, Summary: summary, Detail: detail}
|
||||
}
|
||||
|
||||
func idsOf(cards []Card) []string {
|
||||
out := make([]string, len(cards))
|
||||
for i, c := range cards {
|
||||
out[i] = c.ID
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pickIndexes(seed string, n, mod int) []int {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(seed))
|
||||
start := int(h.Sum32() % uint32(mod))
|
||||
out := make([]int, 0, n)
|
||||
seen := map[int]bool{}
|
||||
for i := 0; len(out) < n && i < mod*2; i++ {
|
||||
idx := (start + i*3) % mod
|
||||
if seen[idx] {
|
||||
continue
|
||||
}
|
||||
seen[idx] = true
|
||||
out = append(out, idx)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package imagecard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDeckSize(t *testing.T) {
|
||||
if len(deck) < 78 {
|
||||
t.Fatalf("want ≥78 cards, got %d", len(deck))
|
||||
}
|
||||
if len(scenes) < 8 {
|
||||
t.Fatalf("want ≥8 scenes, got %d", len(scenes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrawLexicon(t *testing.T) {
|
||||
out := Draw("u1", "情绪整理", true, time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC))
|
||||
if len(out.Cards) != 3 {
|
||||
t.Fatalf("want 3 cards, got %d", len(out.Cards))
|
||||
}
|
||||
raw, _ := json.Marshal(out)
|
||||
for _, bad := range []string{"算命", "占卜"} {
|
||||
if strings.Contains(string(raw), bad) {
|
||||
t.Fatalf("forbidden %q", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScenes(t *testing.T) {
|
||||
if len(Scenes()) < 3 {
|
||||
t.Fatal("too few scenes")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user