b9617ae152
- 后端 Schema 将 interface_language 和 ai_language 合并为 language - 前端设置界面只保留一个语言选项 - AI 回复语言统一使用 language 设置 - 更新协议文档 - 新增数据库迁移脚本
454 lines
14 KiB
Markdown
454 lines
14 KiB
Markdown
# Divination Run Protocol (Frontend <-> Backend)
|
|
|
|
This document defines the structured contract for divination run input, backend hexagram derivation, and run event output.
|
|
|
|
Protocol verification status:
|
|
|
|
- Backend route source: `backend/src/v1/agent/router.py`
|
|
- Backend derivation source: `backend/src/core/divination/derivation.py`
|
|
- Runtime payload schema source: `backend/src/schemas/domain/divination.py`
|
|
|
|
## Compatibility strategy
|
|
|
|
- Run/events contract: `backward-compatible` additive evolution.
|
|
- History contract (`GET /agent/history`) currently `requires-migration` (see migration notes in this document).
|
|
- Existing required fields cannot be removed or renamed without migration notes.
|
|
- Canonical divination terminology values must remain Chinese.
|
|
|
|
## Route overview
|
|
|
|
- Submit run: `POST /api/v1/agent/runs`
|
|
- Cancel run: `POST /api/v1/agent/runs/{threadId}/cancel?runId=...`
|
|
- Stream events: `GET /api/v1/agent/runs/{threadId}/events?runId=...`
|
|
- History snapshot: `GET /api/v1/agent/history`
|
|
- Delete session: `DELETE /api/v1/agent/sessions/{threadId}`
|
|
- Upload attachment: `POST /api/v1/agent/attachments`
|
|
- Get attachment signed URL: `GET /api/v1/agent/attachments/signed-url?bucket=...&path=...`
|
|
- Audio transcribe: `POST /api/v1/agent/transcribe`
|
|
|
|
## Run request contract
|
|
|
|
`RunAgentInput` uses AG-UI shape. This protocol constrains two sections:
|
|
|
|
1) `messages[0].content` (question text)
|
|
2) `forwardedProps.divinationPayload` (structured divination input)
|
|
|
|
### Required request shape
|
|
|
|
```json
|
|
{
|
|
"threadId": "uuid",
|
|
"runId": "run_20260403_xxx",
|
|
"state": {},
|
|
"messages": [
|
|
{
|
|
"id": "msg_run_20260403_xxx_user_0",
|
|
"role": "user",
|
|
"content": "我最近换工作是否合适?"
|
|
}
|
|
],
|
|
"tools": [],
|
|
"context": [],
|
|
"forwardedProps": {
|
|
"runtime_mode": "chat",
|
|
"client_time": {
|
|
"device_timezone": "Asia/Shanghai",
|
|
"client_now_iso": "2026-04-03T20:30:00+08:00",
|
|
"client_epoch_ms": 1775219400000
|
|
},
|
|
"divinationPayload": {
|
|
"divinationMethod": "手动起卦",
|
|
"questionType": "事业",
|
|
"question": "我最近换工作是否合适?",
|
|
"divinationTimeIso": "2026-04-03T20:30:00+08:00",
|
|
"yaoLines": [
|
|
"少阳",
|
|
"少阴",
|
|
"老阳",
|
|
"少阴",
|
|
"少阳",
|
|
"老阴"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### AG-UI required base fields
|
|
|
|
- `state`: required object, frontend sends `{}` by default.
|
|
- `messages[0].id`: required string id for user message.
|
|
- `tools`: required array, frontend sends empty array when no tools requested.
|
|
- `context`: required array, frontend sends empty array when no extra context.
|
|
|
|
### `divinationPayload` strict rules
|
|
|
|
- `divinationMethod`: enum, allowed values `手动起卦 | 自动起卦`
|
|
- `questionType`: non-empty string, max 32 chars, recommended Chinese category labels
|
|
- `question`: non-empty string, max 300 chars
|
|
- `divinationTimeIso`: RFC3339 datetime with timezone offset
|
|
- `yaoLines`: exactly 6 items, order is `初爻 -> 上爻`
|
|
- `yaoLines` item enum: `少阳 | 少阴 | 老阳 | 老阴`
|
|
- Additional fields are forbidden.
|
|
|
|
### Frontend coin-face to `yaoLines` derivation rules
|
|
|
|
This section is normative for frontend collection flows (`手动起卦` and `自动起卦`).
|
|
|
|
- Both manual and auto flows MUST use the same canonical conversion logic.
|
|
- Conversion baseline is manual flow semantics (`huaCount` baseline).
|
|
- Auto flow (`ziCount` baseline) MUST be converted to `huaCount` before mapping.
|
|
- Do not maintain separate mapping tables per page/screen.
|
|
|
|
Canonical mapping (`huaCount` -> `yaoType`):
|
|
|
|
- `0` -> `老阴`
|
|
- `1` -> `少阳`
|
|
- `2` -> `少阴`
|
|
- `3` -> `老阳`
|
|
|
|
Equivalent auto mapping (`ziCount` -> `yaoType`):
|
|
|
|
- `0` -> `老阳`
|
|
- `1` -> `少阴`
|
|
- `2` -> `少阳`
|
|
- `3` -> `老阴`
|
|
|
|
Implementation requirement:
|
|
|
|
- Frontend should centralize this conversion in one reusable converter and use it in both manual and auto screens.
|
|
- `yaoLines` sent to backend MUST always be derived from this canonical mapping and keep order `初爻 -> 上爻`.
|
|
|
|
### `runtime_mode` rules
|
|
|
|
- Allowed values: `chat | follow_up`.
|
|
- `chat`: first run for a session.
|
|
- `follow_up`: follow-up run in existing session.
|
|
- Missing or invalid `runtime_mode` MUST return `422` with code `AGENT_RUNTIME_MODE_INVALID`.
|
|
- Current backend behavior still requires `forwardedProps.divinationPayload` in both modes.
|
|
|
|
### Follow-up request note
|
|
|
|
- Follow-up submit still uses `POST /api/v1/agent/runs`.
|
|
- Required differences from first run:
|
|
- `threadId` must be existing session id.
|
|
- `forwardedProps.runtime_mode` must be `follow_up`.
|
|
- `messages[0].content` is follow-up question text.
|
|
- If `threadId` does not exist, backend returns `404` (`AGENT_SESSION_NOT_FOUND`).
|
|
|
|
## Event output contract
|
|
|
|
During run streaming, backend emits standard AG-UI lifecycle events and two divination-relevant payload events:
|
|
|
|
- Lifecycle: `RUN_STARTED`, `RUN_FINISHED` or `RUN_ERROR`.
|
|
- Step events: `STEP_STARTED`, `STEP_FINISHED` with `stepName` (for example: `worker`).
|
|
- Payload events: `DIVINATION_DERIVED`, `TEXT_MESSAGE_END`.
|
|
|
|
### 1) `DIVINATION_DERIVED`
|
|
|
|
- Emitted once after backend derives hexagram data.
|
|
- Payload field: `divination` (strict object).
|
|
- Emitted only when `runtime_mode=chat`.
|
|
|
|
`divination` object:
|
|
|
|
```json
|
|
{
|
|
"question": "我最近换工作是否合适?",
|
|
"questionType": "事业",
|
|
"divinationMethod": "手动起卦",
|
|
"divinationTime": "2026年04月03日 20:30",
|
|
"binaryCode": "101001",
|
|
"changedBinaryCode": "100001",
|
|
"guaName": "山火贲",
|
|
"guaNameHant": "山火賁",
|
|
"upperName": "艮",
|
|
"lowerName": "离",
|
|
"targetGuaName": "山雷颐",
|
|
"targetGuaNameHant": "山雷頤",
|
|
"worldPosition": 1,
|
|
"responsePosition": 4,
|
|
"hasChangingYao": true,
|
|
"ganzhi": {
|
|
"yearGanZhi": "丙午",
|
|
"monthGanZhi": "壬辰",
|
|
"dayGanZhi": "辛亥",
|
|
"timeGanZhi": "乙巳",
|
|
"yearKongWang": "子丑",
|
|
"monthKongWang": "午未",
|
|
"dayKongWang": "寅卯",
|
|
"timeKongWang": "戌亥",
|
|
"yueJian": "辰土",
|
|
"riChen": "亥水",
|
|
"yuePo": "戌土",
|
|
"riChong": "巳火"
|
|
},
|
|
"wuXingStatuses": {
|
|
"木": "囚",
|
|
"火": "休",
|
|
"土": "旺",
|
|
"金": "相",
|
|
"水": "死"
|
|
},
|
|
"yaoInfoList": [
|
|
{
|
|
"position": 1,
|
|
"spiritName": "虎",
|
|
"spiritNameHant": "虎",
|
|
"relationName": "官鬼",
|
|
"relationNameHant": "官鬼",
|
|
"tiganName": "卯",
|
|
"elementName": "木",
|
|
"isYang": true,
|
|
"isChanging": false,
|
|
"specialMark": "世"
|
|
}
|
|
],
|
|
"targetYaoInfoList": [],
|
|
"fushenPositions": [2],
|
|
"fushenInfoList": [
|
|
{
|
|
"position": 2,
|
|
"relationName": "父母",
|
|
"relationNameHant": "父母",
|
|
"tiganName": "午",
|
|
"elementName": "火"
|
|
}
|
|
],
|
|
"specialStatus": [],
|
|
"interactions": [],
|
|
"timeEffect": [],
|
|
"riChenZhangSheng": []
|
|
}
|
|
```
|
|
|
|
Field notes:
|
|
|
|
- `guaNameHant`, `targetGuaNameHant`: Traditional Chinese variants for卦名.
|
|
- `spiritNameHant`, `relationNameHant`: Traditional Chinese variants for六神/六亲 names.
|
|
- `specialStatus`: Special hexagram status indicators.
|
|
- `interactions`: 爻位 interaction descriptions.
|
|
- `timeEffect`: Time-based effect descriptions.
|
|
- `riChenZhangSheng`: 日辰长生相关 information.
|
|
|
|
### 2) `TEXT_MESSAGE_END`
|
|
|
|
- Standard final answer event.
|
|
- `runtime_mode=chat` fields: `status`, `sign_level`, `conclusion`, `focus_points`, `advice`, `keywords`, `answer`, `error`, `divination_derived`.
|
|
- `runtime_mode=follow_up` fields: `status`, `answer`, `error`.
|
|
- `runtime_mode=follow_up` MUST NOT include `sign_level`, `conclusion`, `focus_points`, `advice`, `keywords`, `divination_derived`.
|
|
- Language rule: `conclusion`, `focus_points`, `advice`, `keywords`, `answer` should follow user `language` preference unless user explicitly requests otherwise.
|
|
- Canonical six-yao terms remain Chinese in protocol text (for example: 世爻、应爻、动爻、静爻、六亲、六神、伏神、月建、日辰、月破、日冲、空亡、五行旺衰).
|
|
|
|
Frontend should combine:
|
|
|
|
- structural divination data from `DIVINATION_DERIVED`
|
|
- interpretation text from `TEXT_MESSAGE_END`
|
|
|
|
## History snapshot contract
|
|
|
|
`GET /api/v1/agent/history` is the canonical replay source for frontend history list and result reconstruction.
|
|
|
|
- When `threadId` is provided, backend returns full session messages ordered by `seq asc`.
|
|
- When `threadId` is omitted, backend returns one latest assistant message per session for history list summary.
|
|
- `threadId` is the session identifier (same value as AG-UI run/events path parameter).
|
|
|
|
### Required response shape
|
|
|
|
```json
|
|
{
|
|
"scope": "history_session_full",
|
|
"threadId": "uuid|null",
|
|
"day": null,
|
|
"hasMore": false,
|
|
"messages": [
|
|
{
|
|
"id": "uuid",
|
|
"threadId": "session-uuid",
|
|
"seq": 12,
|
|
"role": "assistant",
|
|
"content": "...",
|
|
"timestamp": "2026-04-05T12:34:56+00:00",
|
|
"agent_output": {
|
|
"status": "success",
|
|
"sign_level": "中上签",
|
|
"conclusion": ["..."],
|
|
"focus_points": ["..."],
|
|
"advice": ["..."],
|
|
"keywords": ["..."],
|
|
"answer": "...",
|
|
"divination_derived": {
|
|
"binaryCode": "101001",
|
|
"changedBinaryCode": "100001",
|
|
"guaName": "山火贲"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"id": "uuid",
|
|
"threadId": "session-uuid",
|
|
"seq": 11,
|
|
"role": "user",
|
|
"content": "我最近换工作是否合适?",
|
|
"timestamp": "2026-04-05T12:34:12+00:00",
|
|
"attachments": [
|
|
{
|
|
"mimeType": "image/png",
|
|
"url": "https://...signed..."
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Rules:
|
|
|
|
- `scope=history_session_full` means full-thread replay with `messages` ordered by `seq`.
|
|
- `scope=history_sessions_latest_assistant` means cross-session summary list.
|
|
- Each `messages[i].threadId` is required and points to the owning session. Frontend must use this value to open detail replay and follow-up.
|
|
- `day` and `hasMore` are retained for compatibility with old clients, but in `history_session_full` mode backend currently returns `day=null` and `hasMore=false`.
|
|
- In `history_sessions_latest_assistant`, backend computes `hasMore` from `session_limit + 1` query.
|
|
- `assistant` message MUST provide `agent_output` when backend has valid worker output metadata.
|
|
- `agent_output.divination_derived` uses the same shape as `DIVINATION_DERIVED.divination` payload.
|
|
- Frontend reconstructs divination result page from `agent_output` data, not from local mock data.
|
|
- `agent_output.sign_level` allowed values: `上上签` / `中上签` / `中下签` / `下下签`.
|
|
|
|
## Migration note (`requires-migration`)
|
|
|
|
### Change set
|
|
|
|
1. `GET /agent/history?threadId=...` now returns full session replay (`scope=history_session_full`) instead of day-window snapshot semantics.
|
|
2. `messages[i].threadId` is now required.
|
|
3. `ui_schema` is removed from history payload.
|
|
4. `runtime_mode=follow_up` uses minimal `TEXT_MESSAGE_END` schema and no longer emits `DIVINATION_DERIVED`.
|
|
5. Added `DELETE /api/v1/agent/sessions/{threadId}` with idempotent `204 No Content` (already deleted or not found also returns 204).
|
|
|
|
### Frontend migration steps
|
|
|
|
1. Parse history as ordered message stream by `messages[].seq`.
|
|
2. Use `messages[].threadId` as session id for opening follow-up.
|
|
3. Do not depend on `ui_schema` in history payload.
|
|
4. Keep `day`/`hasMore` optional-read only; do not use them for pagination in thread replay mode.
|
|
|
|
### Rollback notes
|
|
|
|
- If backend rolls back to day-window semantics, frontend must switch detail replay back to day-based load logic and stop requiring `messages[].threadId`.
|
|
- Keep legacy parser branch behind feature flag during staged rollout.
|
|
|
|
## Error contract linkage
|
|
|
|
- All errors use RFC7807 with extension `code` and optional `params`.
|
|
- Error code registry source: `docs/protocols/common/http-error-codes.md`.
|
|
|
|
## Session delete contract
|
|
|
|
### `DELETE /api/v1/agent/sessions/{threadId}`
|
|
|
|
- Authorization: current user must own the session.
|
|
- Semantics: soft delete session (`deleted_at`), history reads filter deleted sessions by default.
|
|
- Success: `204 No Content`.
|
|
- Idempotent: already deleted or not found also returns `204 No Content`.
|
|
|
|
## Run cancel contract
|
|
|
|
### `POST /api/v1/agent/runs/{threadId}/cancel?runId=...`
|
|
|
|
- Authorization: current user must own the session.
|
|
- Request: `runId` query parameter required.
|
|
- Success response:
|
|
|
|
```json
|
|
{
|
|
"threadId": "uuid",
|
|
"runId": "run_xxx",
|
|
"accepted": true
|
|
}
|
|
```
|
|
|
|
Error codes (see common registry):
|
|
|
|
- `AGENT_SESSION_NOT_FOUND`
|
|
- `AGENT_FORBIDDEN`
|
|
|
|
## Attachment upload contract
|
|
|
|
### `POST /api/v1/agent/attachments`
|
|
|
|
- Authorization: authenticated user.
|
|
- Request: `multipart/form-data` with fields:
|
|
- `threadId` (string, required)
|
|
- `file` (binary, required)
|
|
- Max file size: 5MB
|
|
- Success response:
|
|
|
|
```json
|
|
{
|
|
"attachment": {
|
|
"bucket": "agent-inputs",
|
|
"path": "agent-inputs/{userId}/{filename}",
|
|
"mimeType": "image/png",
|
|
"url": "https://...signed..."
|
|
}
|
|
}
|
|
```
|
|
|
|
Error codes (see common registry):
|
|
|
|
- `AGENT_ATTACHMENT_EMPTY`
|
|
- `AGENT_ATTACHMENT_TOO_LARGE`
|
|
- `AGENT_ATTACHMENT_STORAGE_UNAVAILABLE`
|
|
- `AGENT_SESSION_NOT_FOUND`
|
|
- `AGENT_FORBIDDEN`
|
|
|
|
## Attachment signed URL contract
|
|
|
|
### `GET /api/v1/agent/attachments/signed-url?bucket=...&path=...`
|
|
|
|
- Authorization: authenticated user.
|
|
- Query params:
|
|
- `bucket` (string, required)
|
|
- `path` (string, required)
|
|
- Path must be under `agent-inputs/{userId}/` scope.
|
|
- Success response:
|
|
|
|
```json
|
|
{
|
|
"bucket": "agent-inputs",
|
|
"path": "agent-inputs/{userId}/{filename}",
|
|
"url": "https://...signed..."
|
|
}
|
|
```
|
|
|
|
Error codes (see common registry):
|
|
|
|
- `AGENT_ATTACHMENT_BUCKET_INVALID`
|
|
- `AGENT_ATTACHMENT_PATH_SCOPE_INVALID`
|
|
- `AGENT_SIGNED_URL_GENERATION_FAILED`
|
|
|
|
## Transcribe contract
|
|
|
|
### `POST /api/v1/agent/transcribe`
|
|
|
|
Request:
|
|
|
|
- `multipart/form-data`
|
|
- field name: `audio`
|
|
- allowed content types: `audio/wav`, `audio/x-wav`, `audio/wave`
|
|
- max payload bytes: `10MB`
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"transcript": "我今天适合出门谈合作吗?"
|
|
}
|
|
```
|
|
|
|
Error codes (see common registry):
|
|
|
|
- `AGENT_AUDIO_UNSUPPORTED_FORMAT`
|
|
- `AGENT_AUDIO_TOO_LARGE`
|
|
- `AGENT_AUDIO_EMPTY`
|
|
- `AGENT_ASR_UNAVAILABLE`
|