Icebreaker API
Prefix: /api/icebreaker
New-user onboarding ("icebreaker") endpoints. The icebreaker is an onboarding conversation, not a mini-game: it keeps its own lifecycle state, separate from the game-route lifecycle. It can append context and speak fixed onboarding lines, but it never makes /api/game/route/active report an open mini-game window.
INFO
All mutating endpoints (/route/start, /route/end, /context, /choice, /speak) are local-mutation endpoints guarded by the same CSRF / local-request validation as the rest of the backend. A failed check returns { "ok": false, "reason": "csrf_validation_failed" }.
INFO
lanlan_name identifies the active character and is required on the mutating POST endpoints. /route/start, /route/end, and /speak resolve it from the body and otherwise fall back to the currently selected character (当前猫娘); if no character can be resolved they return { "ok": false, "reason": "missing_lanlan_name" }. /context is stricter: it requires a non-empty lanlan_name in the body (no fallback) and returns { "ok": false, "reason": "missing_lanlan_name" } when it is missing or empty.
Route lifecycle
POST /api/icebreaker/route/start
Activate the icebreaker route for a character and bind it to a session.
Body:
{
"lanlan_name": "character_name",
"session_id": "session-uuid"
}session_id is required; a missing value returns { "ok": false, "reason": "missing_session_id" }.
Response:
{
"ok": true,
"state": {
"icebreaker_active": true,
"lanlan_name": "character_name",
"session_id": "session-uuid",
"started_at": 0.0,
"last_activity": 0.0,
"source": "new_user_icebreaker"
}
}POST /api/icebreaker/route/end
Finalize the active icebreaker route.
Body:
{
"lanlan_name": "character_name",
"session_id": "session-uuid",
"reason": "icebreaker_end"
}reason is optional and defaults to icebreaker_end.
Response: { "ok": true, "state": <route state> }.
INFO
If session_id is supplied but does not match the active route's session (e.g. a second tab opened a newer session), the call is rejected without ending the route:
{
"ok": false,
"reason": "session_id_mismatch",
"handled": false,
"lanlan_name": "character_name",
"method": "route_end",
"state": "<route state>"
}GET /api/icebreaker/route/state
Read the current icebreaker route state for a character.
Query: lanlan_name — Character name (optional; falls back to the selected character).
Response: The public route state. When no route is active, state is { "icebreaker_active": false }.
{ "ok": true, "state": { "icebreaker_active": false } }Onboarding interactions
POST /api/icebreaker/context
Append a line of onboarding context (user or assistant) to the project session history. The text is also cached for the memory system. Requires an active route bound to the same session.
Body:
{
"lanlan_name": "character_name",
"session_id": "session-uuid",
"role": "assistant",
"text": "Welcome! Let's get you set up."
}rolemust beassistantoruser; otherwise{ "ok": false, "reason": "invalid_role" }.textis required (missing_text) and capped at 2000 characters (invalid_text_length).- An optional
request_id(also accepted asevent.request_id) is used for deduplication.
Response:
{
"ok": true,
"method": "project_session_history",
"lanlan_name": "character_name",
"source": "new_user_icebreaker",
"session_id": "session-uuid",
"memory_cached": true
}A duplicate append returns the same envelope with "deduped": true.
If no route is active, the call returns:
{
"ok": false,
"reason": "route_not_active",
"lanlan_name": "character_name",
"source": "new_user_icebreaker",
"method": "project_session_history"
}If a route is active but the supplied session_id does not match it (a stale or superseded session), the append is skipped:
{
"ok": true,
"skipped": "stale_session",
"reason": "session_id_mismatch",
"handled": false,
"lanlan_name": "character_name",
"method": "project_session_history",
"state": "<route state>"
}POST /api/icebreaker/choice
Persist a single effective tutorial choice into the durable choices pool, so it survives across sessions. Requires an active route bound to the same session.
INFO
This endpoint is write-only for now: the recorded choice does not enter the memory system and does not influence the model. It is kept separate from /context (which feeds transient session history) so the pool stays an independent signal that can be consumed later.
Body:
{
"lanlan_name": "character_name",
"session_id": "session-uuid",
"day": 1,
"node_id": "intro",
"choice": "option_a",
"label": "Tell me more",
"handoff": false,
"completed": false,
"seq": 0
}Response: The persistence result from the choices pool, with source set to new_user_icebreaker. If no route is active, the call returns { "ok": false, "reason": "route_not_active", ... }.
POST /api/icebreaker/speak
Speak a fixed onboarding line through the project TTS pipeline (and mirror it into the chat surface). Requires an active route bound to the same session.
Body:
{
"lanlan_name": "character_name",
"session_id": "session-uuid",
"line": "Nice to meet you!",
"mirror_text": true,
"emit_turn_end": true,
"interrupt_audio": false
}lineis required (missing_line); SSML-like tags are stripped and the line is truncated to 240 characters.mirror_textandemit_turn_enddefault totrue;interrupt_audiodefaults tofalse.
Response: The TTS result envelope, including method: "project_tts" and a voice_source block:
{
"ok": true,
"lanlan_name": "character_name",
"method": "project_tts",
"voice_source": { "provider": "project_tts", "method": "project_tts" }
}If no route is active, the call returns:
{
"ok": false,
"reason": "route_not_active",
"lanlan_name": "character_name",
"source": "new_user_icebreaker",
"method": "project_tts",
"audio_sent": false
}If a route is active but the supplied session_id does not match it (a stale or superseded session), the line is not spoken:
{
"ok": true,
"skipped": "stale_session",
"reason": "session_id_mismatch",
"handled": false,
"lanlan_name": "character_name",
"method": "project_tts",
"state": "<route state>",
"audio_sent": false,
"audio_committed": false,
"voice_source": {
"provider": "project_tts",
"method": "project_tts",
"skipped": "stale_session"
}
}