Chat API¶
Tạo client cho chat đã công khai bằng HTTP để quản lý phiên, lịch sử, tệp và đánh giá, cùng WebSocket để nhận tin nhắn dạng stream. Chat chạy workflow đã lưu bằng tài nguyên của chủ workflow. Người gọi chỉ truy cập được phiên của mình.
Hợp đồng và quyền truy cập¶
| Hợp đồng | Server đang chạy | Nguồn |
|---|---|---|
| HTTP v1 | /openapi.public.json, /docs/public, thẻ Chat |
Tham chiếu API công khai, OpenAPI JSON |
| Socket v1 | /asyncapi.public.yaml |
AsyncAPI YAML |
| Schema frame | Có trong AsyncAPI | JSON Schema client và server |
HTTP dùng /api/public/v1/chat/{slug}; stream dùng
/ws/public/v1/chat/{slug} (wss:// khi chạy HTTPS). Client mới nên dùng
đường dẫn có phiên bản. Xem Tương thích về
đường dẫn cũ và chính sách phiên bản. Mỗi kết nối socket gửi một tin nhắn
hoặc một câu trả lời Human; hiện chưa có SSE.
| Chế độ | Danh tính qua HTTP | Danh tính qua socket |
|---|---|---|
anonymous |
Cookie chat hoặc X-Chat-Visitor |
Cookie chat, auth.visitor hoặc ?visitor= |
signed_in |
Session/Bearer token của studio | Session studio hoặc studio ?token= trên cùng origin |
app |
Authorization: Bearer <user JWT> |
Frame đầu auth.token; ?token= là cách dự phòng |
Client ẩn danh và studio dùng cùng origin với Superstep (trang nhúng chạy
bên trong iframe của Superstep). Client gọi trực tiếp từ origin khác cần
app token và origin được phép: xem Xác thực bên thứ ba.
access có thể là chuỗi đơn kiểu cũ hoặc danh sách chế độ; app token hợp lệ
được ưu tiên hơn cookie ẩn danh. Lịch sử app, ẩn danh và studio tách biệt.
Tạo phiên¶
Lấy user token ngắn hạn từ backend đã xác thực của bạn. Ở đây userToken
là JWT đó; baseUrl là origin của Superstep, không có dấu gạch chéo cuối,
và slug là địa chỉ trang chat.
const baseUrl = 'https://studio.example.com'
const slug = 'support'
async function chatHttp(path, options = {}) {
const response = await fetch(`${baseUrl}/api/public/v1/chat/${slug}${path}`, {
...options,
credentials: 'omit',
headers: { ...options.headers, Authorization: `Bearer ${userToken}` },
})
const body = await response.json()
if (!body.success) throw body.error
return body.data
}
const page = await chatHttp('')
const session = await chatHttp('/sessions', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}',
})
const sessionId = session.id
JSON REST có envelope {"success":true,"data":...,"error":null}; lỗi có
success:false và error:{code,message,details}. Tải logo và tệp trả về
byte ảnh/tệp. Mọi route nằm trong
Tham chiếu HTTP.
Với client ẩn danh cùng origin không dùng cookie, bắt đầu bằng
X-Chat-Visitor: new. Giữ data.visitor_token trả về khi tạo/liệt kê phiên,
gửi lại trong X-Chat-Visitor và dùng auth.visitor trên socket.
Token này cho phép đọc lịch sử của khách đó; không để người khác lấy được.
Gửi và nhận stream¶
Ví dụ tiếp tục dùng baseUrl, slug, sessionId, userToken ở trên.
Token được gửi trong frame để không xuất hiện trong URL.
const socketUrl = new URL(`/ws/public/v1/chat/${slug}`, baseUrl)
socketUrl.protocol = socketUrl.protocol === 'https:' ? 'wss:' : 'ws:'
const socket = new WebSocket(socketUrl)
let lastSeq = -1
socket.addEventListener('message', ({ data }) => {
const frame = JSON.parse(data)
if (frame.v !== 1 || frame.seq <= lastSeq) return
lastSeq = frame.seq
switch (frame.type) {
case 'ready':
socket.send(JSON.stringify({ type: 'auth', token: userToken }))
socket.send(JSON.stringify({ type: 'message', chat_session_id: sessionId,
text: 'Đơn hàng của tôi ở đâu?', attachments: [], locale: 'vi' }))
break
case 'token':
// Nối frame.delta vào câu trả lời đang nhận.
break
case 'question':
// Giữ frame.node và hiện frame.prompt cho người chat.
break
case 'turn':
// Giữ frame.turn_id để đánh giá câu trả lời đã lưu.
break
case 'done':
// Kiểm tra frame.status rồi tải lại các lượt đã lưu qua HTTP.
break
case 'error':
// Xử lý frame.error.code; không hiển thị lỗi thô như HTML.
break
default:
// Bỏ qua kiểu frame chưa biết; ví dụ này không cần xử lý step/run_started.
break
}
})
socket.addEventListener('close', () => {
// Đóng mà chưa có done/error là gián đoạn: đọc lịch sử trước khi thử lại.
})
socket.addEventListener('error', () => {
// Hiện lỗi kết nối mà không ghi token vào log.
})
Các frame¶
Frame client có type cùng các trường dưới đây. Không thêm v, seq hay
trường tùy ý vào frame client; frame không hợp lệ trả về CHAT_FRAME_INVALID.
Gửi tối đa một auth, trước thao tác chính.
| Kiểu client | Trường |
|---|---|
auth |
token cho user app hoặc visitor cho khách ẩn danh |
message |
chat_session_id, text; tùy chọn attachments (id đã upload), locale (en/vi) |
answer |
chat_session_id, response; tùy chọn node lấy từ câu hỏi và locale |
cancel |
Không có trường khác; gửi khi run đang chạy |
Mọi frame server có v:1 và seq tăng từ 0 trong từng kết nối. ts, nếu
có, là giây Unix. Bỏ qua kiểu và trường chưa biết. seq bắt đầu lại khi mở
socket mới; nó không phải cursor lịch sử.
| Kiểu server | Ý nghĩa |
|---|---|
ready |
Đã nhận kết nối, protocol:"superstep-chat"; chưa xác nhận xác thực/quyền truy cập |
run_started |
run_id của run |
token |
Nối delta; có thể kèm node, field, iteration |
step |
event là started, finished hoặc error, kèm node; chi tiết theo chế độ hiện bước |
question |
Node Human đang chờ: node, prompt, kind/input_type, có thể kèm lựa chọn/trường |
turn |
turn_id của câu trả lời đã lưu, role, có thể kèm status, run_id; gửi trước done khi lưu kịp |
done |
status là ok, error, waiting, cancelled hoặc timeout; sau đó server đóng socket |
error |
error.code ổn định, message tiếng Anh, có thể kèm details; không đảm bảo có done sau lỗi |
Một lượt trao đổi minh họa (id là giá trị mẫu):
{"type":"ready","protocol":"superstep-chat","v":1,"seq":0}
{"type":"run_started","run_id":"r1","v":1,"seq":1}
{"type":"token","delta":"Hello","v":1,"seq":2}
{"type":"turn","turn_id":"t2","role":"assistant","status":"ok","run_id":"r1","v":1,"seq":3}
{"type":"done","status":"ok","run_id":"r1","v":1,"seq":4}
Trả lời Human và hủy¶
Sau question và done có status:"waiting", mở socket mới, xác thực
lại rồi gửi câu trả lời cho cùng phiên:
{"type":"answer","chat_session_id":"session-id","node":"human-node-id","response":"yes","locale":"vi"}
Dùng node của câu hỏi và kiểu câu trả lời node Human chấp nhận (ví dụ văn
bản hoặc boolean để duyệt). Schema giao thức còn cho phép object biểu mẫu;
điều đó tự nó không bổ sung runtime biểu mẫu. Người chat không được trả lời
câu hỏi dành cho reviewer (CHAT_ANSWER_BY_REVIEWER). Không gửi tệp kèm câu
trả lời. Để hủy run đang chạy, gửi {"type":"cancel"} trên socket đó.
Ngắt kết nối cũng yêu cầu hủy; đọc lịch sử để biết kết quả cuối.
Lịch sử và kết nối lại¶
GET .../sessions liệt kê phiên của bạn; GET .../sessions/{sid} gồm các
lượt đã lưu. Sau stream hoặc gián đoạn, gọi
GET .../sessions/{sid}/turns?after=<id-lượt-cuối-đã-lưu> để lấy lượt mới
hơn theo thứ tự; bỏ after để lấy tất cả. Kết quả nằm trong data.turns.
Cursor không tồn tại hoặc thuộc phiên khác trả 404 CHAT_TURN_NOT_FOUND.
Dùng id lượt đã lưu để đối chiếu với câu trả lời stream tạm thời. Việc lưu
có thể trễ: poll có backoff nếu lượt cuối chưa được lưu. Không có replay
socket hay khóa idempotency cho tin nhắn. Gửi lại sau ngắt kết nối có thể
tạo run khác, nên đọc lịch sử trước. Kết nối bị gián đoạn có thể không có
done cuối; không thể dùng seq để tiếp tục stream đó.
Tệp và đánh giá¶
- Đọc
page.attachments(enabled,kinds,max_files,max_mb). - Upload mỗi tệp bằng một
POST .../sessions/{sid}/attachments, trường multipartfile; giữdata.id. Trình duyệt tự đặt boundary Content-Type. - Đưa các id vào
message.attachments. Tệp phải thuộc phiên đó và chưa gửi. Tệp đã gửi không được dùng lại hay xóa qua endpoint tệp chưa gửi. - Tải bằng
GET .../attachments/{aid}có xác thực (byte). Xóa tệp chưa gửi bằngDELETE .../attachments/{aid}. DELETE JSON trảdata.ok, không nhận trường xác nhận.
Tài liệu gồm PDF hoặc UTF-8 .txt, .md, .csv; ảnh gồm PNG, JPEG, WebP.
Cài đặt chat và giới hạn của operator cùng áp dụng. Upload cũng tiêu thụ
quota tin nhắn. Xem Tệp đính kèm.
Đánh giá lượt assistant có trạng thái ok hoặc error bằng
PUT .../sessions/{sid}/turns/{tid}/rating, body {"value":"up"} hoặc
{"value":"down","comment":"Thiếu ngày giao hàng"}. Nhận xét cho đánh
giá down là tùy chọn, tối đa 1.000 ký tự; đánh giá up không có nhận xét.
DELETE cùng đường dẫn để bỏ đánh giá. Dùng turn_id đã lưu, đọc lịch sử
nếu stream không có frame turn.
Lỗi và giới hạn¶
Xử lý theo error.code, không theo nội dung thông điệp.
CHAT_SESSION_NOT_FOUND còn che phiên của người khác. Lỗi token và chính
sách origin nằm trong hướng dẫn xác thực; toàn
bộ mã nằm ở Mã lỗi.
429 RATE_LIMITED có HTTP Retry-After tính bằng giây (lỗi socket có
error.details.retry_after). Backoff trước khi mở socket khác hoặc thử
ghi lại. Giới hạn đọc/gửi theo IP, gửi theo khách hoặc user bên ngoài, và
quota mỗi ngày UTC của chat cùng áp dụng. Operator cấu hình
STUDIO_PUBLIC_CHAT_RATE_*; xem Cấu hình.
Chuẩn bị và chạy tin nhắn đã sửa¶
Kiểm tra capabilities.edit của GET .../sessions/{sid} và cờ editable trên tin nhắn. Sau khi người dùng xác nhận chạy lại workflow, gọi:
POST /api/public/v1/chat/{slug}/sessions/{sid}/edits
Content-Type: application/json
{"turn_id":"saved-user-turn","message":"Nội dung đã sửa","operation_id":"client-generated-unique-id","expected_last_turn_id":"latest-saved-turn"}
data trong envelope gồm session (nhánh độc lập và lịch sử giữ lại), message, attachment_ids, operation_id, run_id đã dành trước và state (prepared, started, finished). Bước chuẩn bị chưa chạy workflow. Với trạng thái prepared, mở socket v1 thông thường, gửi message với ID nhánh, message đã chuẩn hoá, attachment_ids đã sao chép và edit_operation_id: operation_id từ kết quả. Stream, lịch sử, tệp, câu trả lời Human và đánh giá sau đó đều thuộc nhánh mới. Giữ nguyên mã thao tác và nội dung khi phục hồi cùng một lần sửa; đổi nội dung sẽ nhận CHAT_EDIT_CONFLICT. Lần sửa mới có chủ đích dùng mã mới.
Nếu đã started/finished, chỉ đọc lịch sử nhánh theo run đã dành trước, không gửi lại lệnh chạy. CHAT_EDIT_STARTED cũng yêu cầu phục hồi bằng lịch sử. Nếu mất frame bắt đầu run, dùng run_id đã dành trước để tìm kết quả. Nhánh đã xoá trả CHAT_EDIT_GONE, không được tạo lại khi thử lại. Nguồn đang bận trả CHAT_EDIT_BUSY; lịch sử đã thay đổi trả CHAT_EDIT_STALE. API không nhận checkpoint, state, owner hoặc workflow ID tuỳ ý. Các quy tắc danh tính/origin/thu hồi khoá và quota vẫn áp dụng. Tin nhắn/câu trả lời v1 cũ không đổi.
Cuộc trò chuyện gốc được giữ nguyên. Lịch sử kế thừa có ID mới và inherited: true, không được sửa/đánh giá/trả lời Human, chỉ hiện các bước được phép. Nhánh có source_session_id và source_turn_id tuỳ chọn; nguồn bị xoá không ngăn truy cập lịch sử/tệp của nhánh. Xem phần sửa tin nhắn trong hướng dẫn chat để hiểu cảnh báo chạy lại và phiên bản workflow được chạy.
Mọi tin nhắn khởi tạo trong nguồn phải có kết quả kết thúc đã lưu; một câu trả lời mới đã xong không cho phép phân nhánh khi run cũ còn dang dở. Trạng thái agent được dựng lại từ run cuối được giữ, bao gồm việc trở về đầu workflow khi agent đích từng bị xoá. Nếu dữ liệu agent cần thiết đã hết thời gian lưu, CHAT_EDIT_CONTEXT_UNAVAILABLE từ chối đoán trạng thái: hãy bắt đầu cuộc trò chuyện mới. Nếu tiến trình dừng sau tiếp nhận nhưng trước khi lưu run, thao tác không được tự chạy lại; đọc lịch sử có giới hạn có thể báo gián đoạn. Chuẩn bị hoặc tải lại nhánh không tự chạy workflow.
Chuẩn bị tạo lại câu trả lời¶
Kiểm tra capabilities.regenerate và cờ regeneratable của câu trả lời cuối. Sau khi xác nhận rằng thao tác bên ngoài có thể lặp lại, gửi ID câu trả lời (không phải tin nhắn người dùng hay câu trả lời Human):
POST /api/public/v1/chat/{slug}/sessions/{sid}/regenerations
Content-Type: application/json
{"turn_id":"latest-terminal-answer","operation_id":"a-new-unique-intent-id","expected_last_turn_id":"latest-terminal-answer"}
Máy chủ chọn tin nhắn gốc và lịch sử cần giữ; từ chối input/state/checkpoint do bên gọi tự truyền. Phản hồi và bước tiếp nhận qua socket dùng chung luồng edit ở trên, gồm trường giao thức edit_operation_id và tên schema phản hồi ChatEdit hiện có. TypeScript đặt tên kết quả dùng chung là ChatRevision; Python có regenerate/prepare_regenerate. Giữ một operation ID khi phục hồi, đọc run đã tiếp nhận mà không chạy lại, và dùng ID mới khi chủ động tạo lần mới. Loại thao tác thuộc định danh yêu cầu; dùng cùng ID cho edit/regenerate gây xung đột.
Thông tin nhánh thêm source_action: "edit" | "regenerate". Khi tạo lại, source_turn_id là câu trả lời assistant gốc; nhánh cũ chưa lưu loại thao tác được hiểu là edit. Xoá nguồn không làm mất loại thao tác/file của nhánh. Các kiểm tra đang chạy, lịch sử thay đổi, chính sách file, chủ sở hữu, origin, thu hồi khoá và hạn mức được dùng chung. Câu hỏi Human/người duyệt đang chờ chưa kết thúc; câu trả lời/quyết định cũ không được dùng lại. Máy chủ chưa hỗ trợ sẽ bị từ chối trước POST; chỉ chọn được câu trả lời cuối đã kết thúc.
Khi đọc lại yêu cầu đã tiếp nhận sau khi nhánh có thêm tin nhắn, SDK vẫn tìm run đã dành cho yêu cầu ấy. TypeScript store giữ trạng thái chờ Human mới hơn trong khi trả về kết quả run được yêu cầu.