任何運行中的實例 /docs 路徑下都有實時、可瀏覽的參考文檔(utoipa 註解生成的 Scalar UI)。
這個頁面是手寫的端點摘要。Scalar UI 是權威 spec。
每個 /comp/* 跟 /bff/v1/* 端點都需要 Authorization: Bearer <Supabase JWT>。JWT 必須是 HS256 簽名、密鑰為 SUPABASE_JWT_SECRET。sub claim 必須是個 UUID;該 UUID 即該請求的 user_id。
/healthz 跟 /docs 是公開的。
存活探針。無需鑒權。
curl http://localhost:8080/healthz{
"status": "ok",
"service": "eros-engine",
"version": "0.3.1",
"timestamp": "2026-05-05T19:06:05.309302232+00:00"
}列出所有處於 active 狀態的人格基因。需鑒權。
curl -H "Authorization: Bearer $JWT" \
http://localhost:8080/comp/personas{
"personas": [
{
"id": "11d6a45a-1fd9-4fe6-a943-3f049035eb68",
"name": "Aria",
"system_prompt": "…",
"tip_personality": "warm-but-reserved",
"avatar_url": "https://avatars.etherfun.xyz/aria.png",
"art_metadata": { "age": 27, "mbti": "INFJ", "model": "x-ai/grok-4-fast", … },
"is_active": true
}
]
}對指定人格基因開新 chat session。如果 (genome_id, jwt_user_id) 對應的 persona_instance 還不存在,服務器先建一個,然後建一個引用該 instance 的 chat_session。
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"genome_id":"11d6a45a-1fd9-4fe6-a943-3f049035eb68"}' \
http://localhost:8080/comp/chat/start{
"session_id": "5f7e…",
"persona_name": "Aria",
"is_new": true
}is_new=false 表示同一用戶用同一個 genome_id 再調 /start——引擎恢復已有 session 而不是建重複的。
流式聊天,返回 text/event-stream,使用 meta → delta* → done → final
状态机(详见 SSE streaming chat 0.2 设计文档)。
请求体必须包含 client_msg_id(26..36 个 ASCII 可打印字符,任意 UUID 或
ULID)。24 小时内同一对 (session_id, client_msg_id) 的重复请求将从
数据库重放历史帧,不会再次调用 OpenRouter。
curl -N -X POST \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"content":"hi","client_msg_id":"01J3333333333333333333333A"}' \
http://localhost:8080/comp/chat/<session_id>/message/stream示例帧(每行 data: 后跟一个 JSON 对象):
data: {"type":"meta","message_id":"01J...","action_type":"reply","model":"x-ai/grok-4-fast"}
data: {"type":"delta","message_id":"01J...","content":"你好"}
data: {"type":"done","message_id":"01J...","truncated":false,"usage":{"prompt_tokens":12,"completion_tokens":4,"total_tokens":16},"generation_id":"gen-abc"}
data: {"type":"final","lead_score":0.42,"should_show_cta":false,"agent_training_level":0.18}
每个用户最多 3 条并发活跃流。保活心跳(: ping)每 15 秒发一次,
防止反向代理因空闲超时断开连接。
流前错误(第一个 SSE 字节写出之前的 HTTP 4xx/5xx)携带含 code、
message、user_message 字段的 JSON 响应体;409 duplicate_in_progress
时还会带 original_user_message_id。完整错误码表见
设计文档。
一旦第一个 SSE 字节写出,终端错误以带内 error 帧的形式到达并关闭流;
此时 HTTP 响应已提交 200 OK。
可选:tier 选择。 请求体可附加 tier 字符串 ——
类型 String,正则 ^[a-z0-9_]{1,32}$(格式错返回 400)。
从 model_config.toml 中选择对应 tier 的模型和 allow_traits
([tasks.chat_companion.tiers.<tier>])。tier 未知或缺省时
回退到任务默认块(会记录一条 warn 日志)。示例:
curl -N -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"content": "hi",
"client_msg_id": "01J3333333333333333333333A",
"tier": "gold"
}' \
http://localhost:8080/comp/chat/<session_id>/message/stream可选:单轮 prompt traits。 请求体可附加 prompt_traits 数组 ——
详见 prompt-traits.zh.md。示例:
curl -N -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"content": "hi",
"client_msg_id": "01J3333333333333333333333A",
"prompt_traits": [
{"tag": "nsfw_boost", "text": "<your injection text>"}
]
}' \
http://localhost:8080/comp/chat/<session_id>/message/stream限制:最多 8 条,tag 满足 [a-z0-9_]{1,32},text ≤ 2000 字符
(trim 后非空)。违反作为 pre-stream 错误返回 400 BadRequest。
可选:记忆注入范围。 请求体可附加 memory_scope 字符串,控制哪些
记忆层会被注入到 prompt 中。接受值:
| 值 | 注入内容 |
|---|---|
full |
完整用户画像(含亲密字段)+ 关系记忆 |
neutral_and_relationship |
中性画像(仅城市/职业/MBTI)+ 关系记忆 (默认) |
relationship_only |
仅关系记忆;不含画像 |
neutral_only |
仅中性画像;不含关系记忆 |
insights_only |
仅完整用户画像(含亲密字段);不含关系记忆 |
none |
不注入任何记忆 |
重要(#40 缓解措施): 默认的
neutral_and_relationship有意比 #40 之前的行为更窄(旧行为注入全部内容)。省略memory_scope并不 等同于旧行为——会应用收窄后的默认值。如需完整注入,请显式指定full。
可选:好感度注入范围。 请求体可附加 affinity_scope 值,控制六个
好感度轴中哪些会被注入到 prompt 中。接受值:
- 具名预设:
"bond"(默认) — warmth + intimacy + tension;"chemistry"— trust + intrigue + patience;"bond_and_chemistry"/"full"— 全部六轴;"none"— 不注入好感度。 - 轴数组:
["warmth", "trust", "intrigue", "intimacy", "patience", "tension"]的任意子集。
重要(#40 缓解措施): 默认的
bond(3 轴)有意比 #40 之前的行为 更窄(旧行为注入全部六轴)。省略affinity_scope并不等同于旧行为。 如需全轴注入,请显式指定"bond_and_chemistry"或"full"。
同时使用两个字段的示例:
curl -N -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"content": "hi",
"client_msg_id": "01J3333333333333333333333A",
"memory_scope": "full",
"affinity_scope": "bond_and_chemistry"
}' \
http://localhost:8080/comp/chat/<session_id>/message/stream可选:OpenRouter audit 透传。 请求体可附加 audit 对象,
原样作为 wire 级别的 user / session_id / metadata 发送给
OpenRouter —— 详见 llm-audit.zh.md。示例:
curl -N -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"content": "hi",
"client_msg_id": "01J3333333333333333333333A",
"audit": {
"user": "u_<hash>",
"session_id": "conv_xyz",
"metadata": { "feature": "chat", "plan": "pro" }
}
}' \
http://localhost:8080/comp/chat/<session_id>/message/stream限制:audit.user 与 audit.session_id ≤ 256 字符;audit.metadata
≤ 16 个 key,key 满足 [A-Za-z0-9_.-]{1,64},value 必须是 string
且 ≤ 512 字符。违反作为 pre-stream 错误返回 400 BadRequest。
分頁讀消息歷史,最新在前。
{
"messages": [
{ "id": "…", "role": "assistant", "content": "Bishop。", "sent_at": "…" },
{ "id": "…", "role": "user", "content": "嗨…", "sent_at": "…" }
]
}role ∈ user | assistant | gift_user | system_error。
該 user_id 名下的所有 chat sessions。路徑裡的 user_id 必須 等於 JWT 裡的 user_id;否則 403。
當前的 companion_insights JSONB 加上加權的 training_level。同樣的 user_id 等值檢查。
{
"insights": {
"city": "Hong Kong",
"occupation": "graphic designer",
"interests": ["jazz", "long walks"],
"mbti_guess": "INFP"
},
"training_level": 0.42
}training_level 是九個字段加權後的分數(city 0.05、occupation 0.05、interests 0.10、mbti_guess 0.15、love_values 0.15、emotional_needs 0.15、life_rhythm 0.10、personality_traits 0.15、matching_preferences 0.10)。權重總和為 1.0。
把外部事件帶來的好感度 deltas 應用上去(虛擬禮物、表情反應、任何你想建模成「用戶剛做了件好事」的事)。路由會寫一條 chat_messages(role='gift_user')並通過好感度持久化路徑應用 deltas。
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{
"deltas": {"warmth": 0.05, "intimacy": 0.03, "tension": -0.02},
"label": "玫瑰",
"metadata": {"source": "frontend-shop", "amount": 100}
}' \
http://localhost:8080/comp/chat/<session_id>/event/giftv0.1 的禮物路由 不會 觸發 LLM 反應(reply 為 null)。人格在用戶下一輪消息裡承認這份禮物,那時新的好感度狀態塑造回覆。同步反應變體是後續增強。
列出該 session 的所有禮物事件,分頁。
實時 6 維向量 + ghost 統計 + 關係標籤。受 EXPOSE_AFFINITY_DEBUG=true 環境變量控制;關閉時返 404。
{
"warmth": 0.42,
"trust": 0.28,
"intrigue": 0.61,
"intimacy": 0.15,
"patience": 0.55,
"tension": 0.18,
"ghost_streak": 0,
"total_ghosts": 0,
"relationship_label": "stranger",
"updated_at": "2026-05-05T19:42:00.000000Z"
}生產部署通常關著(好感度向量是魔法的一部份——把它暴露出來會破壞錯覺)。如果你的前端想實時畫好感度雷達圖,再把它打開。
面向第一方前端、把部分 /comp/* 路由重塑成前端形狀的鏡像層。鑒權與
canonical 路由完全相同(同樣的 Supabase JWT、同樣的 per-user ownership
檢查),只有 響應形狀 不同(更精簡的 DTO、打包好的 payload)。
canonical /comp/* 路由永遠不會為了遷就前端而被改形狀——而是在旁邊
新增一條 BFF 路由。目前有三條。
冷啟動打包:一個 round-trip 內既解析(或創建)session,又返回它最近的
歷史,把前端原本分開的 start + history 兩個調用合成一個。同一用戶 +
同一輸入,會解析到與 canonical POST /comp/chat/start 完全相同的 session。
請求體 = canonical start 請求體,外加一個 BFF-only 字段:
genome_id/instance_id—— 標識人格(同 canonical)。is_demo—— 可選,同 canonical。history_limit—— 可選,打包歷史的頁大小;默認 50,上限 50。
{
"session_id": "5f7e…",
"instance_id": "…",
"persona_name": "Aria",
"is_new": false,
"history": [
{ "id": "3cc06c53-…", "client_msg_id": "c_abc", "role": "user", "content": "hello", "sent_at": "…" },
{ "id": "9f2e7a10-…", "client_msg_id": null, "role": "assistant", "content": "hi back", "sent_at": "…" }
]
}這裡 不會 打包 affinity——前端單獨讀取(見下面的 affinity event
路由),這樣 bootstrap 就與 EXPOSE_AFFINITY_DEBUG 解耦。
給聊天屏用的精簡歷史投影:id / client_msg_id / role / content /
sent_at(不含 extracted_facts)。id 是 chat_messages 行的主鍵(UUID);
client_msg_id 是前端串流時帶上的 id(沒帶的行為 null,例如 assistant 回合)。
鑒權、ownership 檢查、limit ∈ [1, 50] 夾取
都與 canonical history 路由相同。刻意差異: 默認 limit 是 50
(canonical 默認 20),因為 BFF 是為「冷啟動一次拉一整屏 backscroll」設計的。
{
"session_id": "…",
"messages": [
{ "id": "3cc06c53-…", "client_msg_id": "c_abc", "role": "user", "content": "alpha", "sent_at": "…" },
{ "id": "9f2e7a10-…", "client_msg_id": null, "role": "assistant", "content": "beta", "sent_at": "…" }
],
"total": 2
}total 是 本次 響應裡 messages 的條數(== messages.len()),
不是該 session 的總行數。
最近一次用戶輪次的好感度 delta(post-EMA),供前端做逐輪觀測。與
canonical 的 /comp/affinity/{session_id} debug 路由不同,它 不受
EXPOSE_AFFINITY_DEBUG 控制(這塊歸前端所有)——但仍做 JWT + ownership 檢查。
{
"session_id": "…",
"event": {
"event_id": "…",
"event_type": "message",
"effective_deltas": {
"warmth": 0.03, "trust": 0.01, "intrigue": 0.0,
"intimacy": 0.0, "patience": 0.0, "tension": -0.01
},
"created_at": "…"
}
}event 為 null 的情況:還沒有任何用戶輪次事件(全新 session,或只有
time-decay),或最近一次事件早於 affinity migration 0014。event_type
∈ message | gift | proactive | ghost;ghost 輪次的 effective_deltas
全為零。
所有錯誤都是 JSON 形狀 {"error": "<code>", "message": "<人類可讀>"}:
| 狀態碼 | code | 何時 |
|---|---|---|
| 400 | bad_request |
請求體格式錯、UUID 無效、缺必填字段 |
| 401 | unauthorized |
JWT 缺失 / 格式錯 / 過期 / 密鑰不符 |
| 403 | forbidden |
路徑 user 跟 JWT user 不匹配,或想讀別人的 session |
| 404 | not_found |
session / 人格 / 消息 id 不存在 |
| 500 | internal |
其餘一切(DB 錯、LLM API 錯等) |
crates/eros-engine-server/src/routes/companion.rs——handler 實現crates/eros-engine-server/src/routes/bff/companion.rs——BFF/bff/v1/comp/chat/*crates/eros-engine-server/src/routes/bff/affinity.rs——BFF/bff/v1/comp/affinity/*crates/eros-engine-server/src/routes/debug.rs——好感度 debug 路由crates/eros-engine-server/src/routes/health.rs——/healthzcrates/eros-engine-server/src/openapi.rs——Scalar UI spec 元數據