soukmind 2.0 对接文档
读者 = 把 soukmind 接进自己 App / 后端的开发者。基线 2026-09-17。 端点清单见
REF-api-external.md(代码生成)。 本文的端点路径、事件名、卡型由backend/tests/test_docs_alignment_gate.py回代码求值守护。
一句话:一个对话端点 + 一套 SSE 事件契约。你发一句商家原话,拿回答文与卡片; 需要落写的操作在对话里二次确认,不另开提交接口。
sequenceDiagram
participant C as 你的客户端
participant S as soukmind
C->>S: POST /api/v2/mind/turns {message, conversation_id}
S-->>C: conversation {conversation_id}
S-->>C: step × N(recognize / start / intent / acting / exec)
S-->>C: action_card {kind, data, …}
S-->>C: final {answer, tier, streaming, note}
1. 接入前提
| 项 | 值 |
|---|---|
| 线上根地址 | https://soukmind.ichain.top/ai-agent |
| 对话端点 | POST /api/v2/mind/turns |
| 本机开发 | http://127.0.0.1:8001(无前缀) |
⚠ 线上地址必须带 /ai-agent:nginx 只在这个前缀下转发后端,根路径是运营端静态站。
少了前缀返回 404 —— 这是最常见的一次误判(会被读成「端点没上线」)。
2. 鉴权
生产是 token 模式:除登录与健康检查外,一律带 Authorization: Bearer <token>,
身份只取令牌(商户号、门店、角色都从令牌里解,不看请求头)。
TOKEN=$(curl -s -X POST $BASE/api/v1/auth/login -H 'Content-Type: application/json' \
-d '{"username":"<账号>","password":"<密码>"}' | jq -r .access_token)
本机/测试环境是 headers 模式,身份取 X-Merchant-Id / X-Store-Id / X-User-Id / X-Role。
两种模式的判定与权限点见 TDD-auth-login-and-permission。
3. 一轮对话
请求
{ "message": "本周营业额", "conversation_id": "<你生成并全程带着>", "stream": false, "lang": "zh" }
| 字段 | 必填 | 说明 |
|---|---|---|
message | 是(除非带 action) | 商家原话。空串不会报错,会被当闲聊处理 —— 非空校验放在你这边 |
action | 否 | 卡片按钮的回传:{gid, args, confirmed?}。见 §6 |
conversation_id | 是 | 见 §7:不传不会自动生成,多轮会接不上 |
stream | 建议显式 | false = 一次性 JSON · true = SSE。不传时由服务端档位定,别依赖默认 |
lang | 否 | zh / en。客户端显式语言是唯一能压过原句的信号 |
响应形态
stream: false → application/json,三个顶层键:
{ "events": [ {"event": "...", "data": {...}}, ... ],
"answer": "本周营业额 AED 4,494.00。",
"cards": [ {...} ] }
stream: true → text/event-stream,逐条 event: <名> / data: <json>。
两条路同源:JSON 里的 events 就是 SSE 那串事件,解析代码可以共用一份。
出卡那一轮是确定性产物(一次识别、一次取数、模板直出),一两秒内返回,用 JSON 更省事; 多步分析十几秒起,用 SSE 让用户先看到「在查什么」。
answer 是按序拼接所有 final 帧,不是取最后一条 —— 服务端语义是分段
(streaming: true 为部分、末帧 false)。取最后一条今天同值,等接上分段流式会只剩最后一句。
4. 事件契约
闭集五种,三个客户端 SDK 按这些名字消费,取值不会改名:
| 事件 | 何时出 | data |
|---|---|---|
conversation | 每轮首帧 | conversation_id |
step | 过程可观测 | 见下 |
action_card | 有结构化结果时 | 见 §5 |
final | 终答 | answer tier streaming note |
error | 出错 | 错误信息 |
step 的 phase:recognize(判给谁、什么轮型)→ start(落到哪个专家)→
intent(意图与槽)→ acting(开始执行)→ exec(能力、参数、证据键、是否成功);
写确认那一轮多一个 hitl_execute。
⚠ step 是可观测面不是契约面:phase 与字段会随能力演进增删,渲染逻辑别依赖某个 phase 一定在。
契约面是 final / action_card / conversation / error。
final.note —— 客户端要分支的就这一个字段
note 是闭集码(app/mind/types/note_kind.py)。常用的几类:
| note | 含义 | 客户端该做什么 |
|---|---|---|
"" | 正常答完 | 渲染 answer + 卡 |
_needs_confirm | 写操作待确认 | 展示预览卡 + 让用户回一句确认(§6) |
_needs:<槽名> · _needs_slot | 缺参数 | 照 answer 的问句追问 |
_no_data | 该时间段没有数据 | 原样展示,别改写成「查询失败」 |
_no_kb | 知识库没有这条 | 与 _no_data 分开:这类问句根本没有时间段 |
_write_failed · _commit_uncertain | 写失败 / 可能已落地 | 后者要提示用户去核对,别重试 |
_cancelled | 用户放弃 | 收起待确认态 |
不带下划线的 note 是自由文本(成色提示),不要求分支。
5. 卡片
action_card.data 里是结构化证据,kind 说明是哪一类:
receipt(写回执/预览)· list(列表)· detail(详情)· chart(序列图)·
breakdown(构成)· data(单指标)· insight(洞察)· capability(能力说明)·
reference(引用)· offer(主动提议)。
每张卡都带 actions[](没有按钮时是空数组,不是缺键)与骨架字段
id / title / body / version / indicators。按钮怎么回传见 §6。
哪些卡会有按钮:
| 卡 | 按钮 | 回传 |
|---|---|---|
确认卡(receipt + note=_needs_confirm) | 「确认」 | souk_cap · {gid, args, confirmed:true} |
推荐卡(list·行里带 suggested_args) | 每方案一个「选「…」」(最多 3 个) | souk_cap · {gid, args} —— 带的是那一条方案的参数 |
提议卡(offer) | 「就这么办」 | ask · {text} —— 把这句诉求当新问题发 |
| 其余 | 无(空数组) | — |
推荐卡的按钮各带各的参数,所以「按第二个方案做」在按钮这条路上不需要指代 ——
点哪个就是哪个。列表类卡(活动/会员/商品)不出 actions,行交互走 form 由端上渲染。
列表类卡另有 form: {kind, items, total} 供直接渲染;form.kind 取 nb_campaign_list /
nb_campaign_detail / nb_member_list / nb_product_list。
⚠ total 是筛选命中总数,不是当页条数 —— 显示「共 N 条」用它。
6. 按钮回传:App 用 API 提交
卡片上的 actions[] 就是按钮。每个按钮带 action 名与 params,App 把 params 原样
POST 回 /api/v2/mind/turns 的 action 字段即可 —— 与 1.0 同一个机制、同一个动作名
(souk_cap),SDK 不用改。
{ "action": "souk_cap", "label": "确认",
"params": { "gid": "marketing-expert.create",
"args": { "activity_name": "…", "activity_type": "WHOLE_ORDER",
"discount": { "type": "DIRECT_AMOUNT_OFF", "condition": 100, "value": 20 } },
"confirmed": true } }
两种用法,判据是 confirmed:
| 作用 | 谁出这种按钮 | |
|---|---|---|
confirmed 不给 / false | 结构化直调:按 gid 直接执行、用带回的 args,跳过识别与抽参 | 推荐卡的「选「…」」 |
confirmed: true | 提交:执行服务端握着的那笔待确认写 | 确认卡的「确认」 |
另有一种按钮不带 gid:提议卡的 ask —— params.text 是一句诉求,
把它当新消息发出去即可({"message": params.text, "conversation_id": …}),
它会照常走一遍识别、按当时的现场补参数。
为什么跳识别:参数在出卡时就是确定的,再让模型从自然语言抽一遍只会丢 (长尾商品名的三种写法都抽不回来)。按钮轮因此不调模型,快且省。
写操作仍是两段式
写类能力在运行时被闸住,必须拿到确认才放行。两条路等价,选一条即可:
① 发起(自然语言或直调)
→ action_card kind=receipt(预览 + 确认按钮)
→ final {"note":"_needs_confirm"}
② 提交:POST {"action": <按钮的 params>} # App 点按钮
或 POST {"message": "确认"} # 用户打字
→ step.hitl_execute → final {"note":"_write_ok"}
- 判「要不要确认」看
final.note == "_needs_confirm",不要自己猜答文。 - 打字确认由模型判,不是词表:「确认」「好的,就这么办」「行,听你的」都算。
- 客户端带的
args不作为写入参数:提交时服务端用自己握着的那份(预览给用户看的就是它),args只用来核对 gid 对不对得上。凭据始终没离开服务端。 - 幂等键在出卡时就算好:用户照着「可能已经成功」再点一次,落的仍是同一笔。
- 待确认态会超龄,超龄回
_write_stale(重发确认卡,不执行)。 confirmed: true但服务端没有对应的待确认写 → 不执行,当作一次普通直调 (写类于是重新出一张确认卡)。宁可多问一次,也不凭客户端一句话写数据。
7. 多轮
conversation_id 由你生成并全程带着。不传时服务端原样回显空串(不会替你生成),
后果是多轮接不上、历史也对不上。同一 conversation_id 下服务端保留上下文,
所以「那上周呢」「第二个」这类残句能接住。
8. 排障
| 现象 | 多半是 |
|---|---|
| 404 | 线上少了 /ai-agent 前缀 |
| 401 | 没带令牌,或令牌过期(登出会让该账号所有令牌立即失效) |
拿不到 event: 行 | 没写 "stream": true,服务端按档位返了 JSON |
| 多轮像失忆 | conversation_id 每轮都变,或压根没传 |
| 写没落下去 | 只发了第一轮;要看 note == "_needs_confirm" 并把按钮 params(或用户的确认)发第二轮 |
| 按钮点了没反应 | action 里的 gid 写错或不属于任何专家;载荷形状不对会被当作没有 action,退回按 message 走 |
9. 完整示例
BASE=https://soukmind.ichain.top/ai-agent
TOKEN=$(curl -s -X POST $BASE/api/v1/auth/login -H 'Content-Type: application/json' \
-d '{"username":"<账号>","password":"<密码>"}' | jq -r .access_token)
CID=$(uuidgen)
# 查数据
curl -s -X POST $BASE/api/v2/mind/turns -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"message\":\"本周营业额\",\"conversation_id\":\"$CID\",\"stream\":false}"
# 发起写 → 拿到 note=_needs_confirm 与预览卡
curl -s -X POST $BASE/api/v2/mind/turns -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"message\":\"建个满100减20的活动\",\"conversation_id\":\"$CID\",\"stream\":false}"
# 点确认按钮 → 把上一步卡里 actions[0].params 原样放进 action(这一轮才落写)
curl -s -X POST $BASE/api/v2/mind/turns -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"conversation_id\":\"$CID\",\"stream\":false,\"action\":{\"gid\":\"marketing-expert.create\",\"args\":{...},\"confirmed\":true}}"
10. 其余端点
登录、会话历史、记忆召回、任务、反馈、探针共 20 条,逐条见
REF-api-external.md(方法 · 路径 · 职责 · 鉴权 · 消费方,代码生成)。