Answer Template — Danh sách API

Thiết kế đích (2026-07-09) — card lưu giống agent & workflow, qua tầng edge chuẩn

Answer template thành kind thứ 5 trong version-svc. Mô hình v0-mutable (đổi immutability invariant): gộp draft + version vào MỘT bảng version.components — Draft = version_no=0 (v0, mutable), published = version_no≥1 (frozen + ký Ed25519). DSL luôn ở version-svc, project-svc chỉ giữ metadata entity (GET detail/builder lấy DSL v0 qua S2S resolve). Tầng gọi: FE → web-bff → project-svc (authz + metadata) → S2S → version-svc (DSL + versioning). version-svc /v1/internal/* KHÔNG lộ FE (FR-BFFWS-011); agent-runtime chỉ render. Đổi hard invariant + re-platform version-svc → cần RFC + ADR. Chi tiết: documents/ANSWER_TEMPLATE_VERSION_INTEGRATION.md.

Danh mục API RIÊNG của chức năng Answer Template — 7 route FE-facing (5 CRUD template project-svc + 2 versioning draft/publish) + 4 S2S internal (upsert-v0 / resolve-v0 / publish / resolve-version) + 1 S2S delete-gate + 1 đặc tả transport (card đi trên stream chat, KHÔNG endpoint riêng). Publish riêng của template đã bỏ — publish theo Workspace Version. Mỗi mục có đặc tả (thiết kế đích) — bấm "Đặc tả" trong từng card. POST/GET-only per OPTIMAL §1.8.8. Chat / follow-up chips / transcript / config env là API phân hệ khác, không liệt kê ở đây.

5 CRUD (project-svc) 6 versioning (v0-model) 1 S2S nội bộ FE→web-bff→project-svc→version-svc · transport 3.12.6

Nguyên tắc chung

6 quy tắc chi phối toàn bộ bề mặt API bên dưới.

FE không chạm service trực tiếp

Mọi thao tác Studio: FE → web-bff (public /v1, JWT) → project-svc (authz + orchestrate) → S2S → version-svc / agent-runtime. version-svc /v1/internal/* (draft/publish/resolve) KHÔNG lộ FE (FR-BFFWS-011). project-svc own template CRUD + gọi clients/version.py cho draft/publish. agent-runtime chỉ render.

POST/GET-only

Mutation → POST sub-resource (.../update, .../{id}/delete). Cấm PUT/PATCH/DELETE.

Idempotency Pattern C

POST side-effect require header Idempotency-Key — ULID 26 ký tự hoặc UUID v4. Key trùng, body khác → IDEMPOTENCY_REPLAY.

v0-mutable: Draft = version_no 0

Draft + version chung MỘT bảng version.components. Sửa DSL = UPSERT row version_no=0 (v0, mutable). Publish = copy v0 đổi hash → INSERT row version_no=next (freeze DSL + ký Ed25519); không đổi → carry-forward version_no cũ. Row version_no≥1 INSERT-only immutable (enforce partial theo version_no). v0 vẫn mutable cho version sau. Đây là đổi immutability invariant — merge draft_components + workspace_version_components, cần RFC.

Allowlist filter MỘT lớp

Filter card_action_url_allowlist chạy ở agent-runtime lúc render (Seam 1). Strip là log-only — không sinh mã lỗi, không fail turn. conversation-svc persist bản đã strip, web-bff forward nguyên trạng.

Card không có endpoint riêng

Card đi trên chính stream chat: SSE message_end.metadata.card — always-present, turn không card trả null. Gắn card vào agent cũng KHÔNG có API riêng — ref nằm trong agent definition, đóng băng theo version.

Error envelope thống nhất

{"error": {code, message, trace_id, ts, details}} — chi tiết ở Bảng mã lỗi.

Luồng tổng quan

Từ thiết kế đến replay — 5 chặng, mỗi chặng map vào một nhóm API bên dưới.

1 · Thiết kếVisual Builder GrapesJS (block định nghĩa ở FE) dựng card JSON + variablesstudio-svc (FE-only)
2 · Soạn cardCRUD template — FE → web-bff → project-svcproject-svc (đích)
2b · v0 → Versionproject-svc S2S → version-svc: UPSERT components v0 (version_no=0) → publish copy v0 → version_no≥1 (freeze + ký)project-svc → version-svc (đích)
3 · Cấu hình envallowlist per env → resolve → AgentSpec (API thuộc phân hệ Environment)environment-svc
4 · Chat runtimerender Jinja sandbox + filter → SSE metadata.cardagent-runtime → conv-svc → web-bff
5 · Replayđọc card_payload đã lưu, không re-filterconversation-svc

Design-time — Answer Template đích: project-svc

Thiết kế đích: owner CRUD chuyển sang project-svc (như agent/workflow entity), public qua web-bff path /v1/projects/{project_id}/answer-templates/*, auth JWT user. As-built (TKCT nhúng): owner agent-runtime schema card, path /v1/answer-templates, CG-004. RLS app.project_id.

Thư viện UI Component — FE-only (GrapesJS)

8 khối dựng card (Form · Text · Image/Media · List · Selector · Button/CTA · Carousel · Confirmation) do Visual Builder GrapesJS định nghĩa ở FE (js/builder.js blockManager) — KHÔNG gọi API. Không đưa GET /v1/card-components vào danh mục answer-card (cùng loại FE-only với Visual Builder / Live Preview). Chặn block sai lúc publish do agent-runtime validate bằng danh sách 8 type cố định phía server. TKCT 3.9.2 từng đề xuất endpoint catalog để builder + validator dùng chung props_schema — không cần khi builder là GrapesJS.

GET/v1/projects/{pid}/answer-templatesDanh sách template
List theo project, phân trang. Template đã xóa mềm (deleted_at) không hiện.
đích: project-svcTKCT 3.9.1.1
Đặc tả (thiết kế đích) — sequence · luồng · request/response · mã lỗi

Sequence diagram

sequenceDiagram
    actor Dev as Dev/PO (Studio)
    participant Web as web-bff (public /v1)
    participant PS as project-svc (owner + authz)
    Dev->>Web: GET /v1/projects/{pid}/answer-templates?q=&status=&cursor= (JWT)
    Web->>PS: forward (verified JWT, X-Project-Id)
    Note over PS: verify project-member + quyền viewer<br/>SET LOCAL app.project_id (RLS)
    Note over PS: SELECT project.answer_templates WHERE deleted_at IS NULL (filter + cursor)
    PS-->>Web: 200 {data, next_cursor}
    Web-->>Dev: Render danh sách + trạng thái published

Mô tả luồng nghiệp vụ

BướcĐối tượngHành độngMô tả
1Dev/POMở màn Answer TemplateStudio gọi list khi mount + đổi filter/search. FE → web-bff → project-svc.
2project-svcVerify JWT + quyềnJWT sai → 401; thiếu quyền viewer → 403. RLS SET LOCAL app.project_id.
3project-svcQuery danh sáchSELECT project.answer_templates (loại soft-deleted), filter q (ILIKE), status, cursor keyset.
4project-svcTrả kết quảMỗi item kèm current_version (version_no đã publish gần nhất, null nếu chưa).
5FEHiển thịRender bảng template; lỗi → toast theo error.code.

Mô tả API

Method typePathMô tả
GET/v1/projects/{pid}/answer-templatesList answer template của project (RLS), cursor pagination, filter + search. Owner project-svc.
API Request

Header: Authorization Bearer JWT (Y).

Query: q (search ILIKE), status (published/draft_only), cursor, limit (≤100).

API Response

200{data[], next_cursor}; mỗi item: id, name, description, current_version, updated_by, updated_at.

Bảng mã lỗi

HTTPcodemessage
400VALIDATION_FAILEDBody/param sai schema hoặc vượt giới hạn
400IDEMPOTENCY_KEY_REQUIREDPOST side-effect thiếu Idempotency-Key hoặc sai format ULID/UUID v4
401JWT_MISSING · JWT_INVALIDThiếu Authorization / signature-aud-exp không hợp lệ
403FORBIDDENToken thiếu claim project_id/sub hoặc thiếu quyền editor/viewer
404NOT_FOUNDanswer_template_id không tồn tại trong project scope
409NAME_CONFLICTTrùng (project_id, name) với template chưa xóa
409TEMPLATE_IN_USEXóa khi còn Workspace Version tham chiếu
500INTERNAL_ERRORLỗi không phân loại — response không lộ stacktrace
Thiết kế đích: owner project-svc, path /v1/projects/{pid}/answer-templates, bảng project.answer_templates
GET/v1/projects/{pid}/answer-templates/{id}Chi tiết template
DSL đầy đủ của template + version history.
đích: project-svcTKCT 3.9.1.2404 NOT_FOUND
Đặc tả (thiết kế đích) — sequence · luồng · request/response · mã lỗi

Sequence diagram

sequenceDiagram
    actor Dev as Dev/PO
    participant Web as web-bff
    participant PS as project-svc
    participant VS as version-svc
    Dev->>Web: GET /v1/projects/{pid}/answer-templates/{id} (JWT)
    Web->>PS: forward
    Note over PS: SELECT metadata entity (id, name, description, RLS project)
    alt không có / đã xóa mềm
        PS-->>Web: 404 NOT_FOUND
    else tồn tại
        PS->>VS: S2S GET /v1/internal/components/{ws}/resolve (lấy DSL v0)
        VS-->>PS: DSL v0 + version history (version_no≥1)
        PS-->>Web: 200 {metadata + dsl + versions[]}
    end
    Web-->>Dev: Mở Visual Builder với DSL v0

Mô tả luồng nghiệp vụ

BướcĐối tượngHành độngMô tả
1Dev/POClick templateFE gọi detail để mở builder + panel version.
2project-svcVerify + metadata401/403 chuẩn; id không trong project (RLS) → 404. project-svc CHỈ giữ metadata entity, KHÔNG có DSL.
3project-svc → version-svcLấy DSL v0 (S2S)DSL sống ở version-svc: S2S GET /v1/internal/components/{ws}/resolve trả row version_no=0 (draft đang sửa) + version history version_no≥1.
4FEHiển thịMở Visual Builder (GrapesJS) với DSL v0; so v0 vs current_version để badge 'chưa publish'.

Mô tả API

Method typePathMô tả
GET/v1/projects/{pid}/answer-templates/{id}Chi tiết: project-svc metadata + S2S version-svc lấy DSL v0 + version history.
API Response

200{id, name, description, dsl, current_version_id, versions[]}. 404 NOT_FOUND nếu không thuộc project.

Bảng mã lỗi

HTTPcodemessage
400VALIDATION_FAILEDBody/param sai schema hoặc vượt giới hạn
400IDEMPOTENCY_KEY_REQUIREDPOST side-effect thiếu Idempotency-Key hoặc sai format ULID/UUID v4
401JWT_MISSING · JWT_INVALIDThiếu Authorization / signature-aud-exp không hợp lệ
403FORBIDDENToken thiếu claim project_id/sub hoặc thiếu quyền editor/viewer
404NOT_FOUNDanswer_template_id không tồn tại trong project scope
409NAME_CONFLICTTrùng (project_id, name) với template chưa xóa
409TEMPLATE_IN_USEXóa khi còn Workspace Version tham chiếu
500INTERNAL_ERRORLỗi không phân loại — response không lộ stacktrace
Thiết kế đích: project-svc metadata + S2S version-svc resolve v0 lấy DSL; version history từ components version_no≥1
POST/v1/projects/{pid}/answer-templatesTạo template
201. Tên unique trong project (tính trên bản chưa xóa).
Idempotency-Keyđích: project-svcTKCT 3.9.1.3409 NAME_CONFLICT400 VALIDATION_FAILED
Đặc tả (thiết kế đích) — sequence · luồng · request/response · mã lỗi

Sequence diagram

sequenceDiagram
    actor Dev as Dev/PO
    participant Web as web-bff
    participant PS as project-svc
    Dev->>Web: POST /v1/projects/{pid}/answer-templates (JWT + Idempotency-Key)
    Web->>PS: forward
    Note over PS: verify quyền editor + validate (name 3-100, description ≤ 500)
    Note over PS: INSERT project.answer_templates
    alt tên chưa dùng
        PS-->>Web: 201 template (draft)
    else trùng (project_id, name)
        PS-->>Web: 409 NAME_CONFLICT
    end

Mô tả luồng nghiệp vụ

BướcĐối tượngHành độngMô tả
1Dev/POTạo templateFE gửi name + description (+ layout khởi tạo).
2project-svcValidate + INSERTVerify editor; name unique trong project (bản chưa xóa) → 409 NAME_CONFLICT nếu trùng.
3FEHiển thị201 → mở builder với template rỗng.

Mô tả API

Method typePathMô tả
POST/v1/projects/{pid}/answer-templatesTạo answer template mới (draft, chưa publish). Owner project-svc.
API Request

Header: Authorization (Y), Idempotency-Key (Y). Body: {name, description?}.

API Response

201{id, name, description, current_version_id: null, created_at}.

Bảng mã lỗi

HTTPcodemessage
400VALIDATION_FAILEDBody/param sai schema hoặc vượt giới hạn
400IDEMPOTENCY_KEY_REQUIREDPOST side-effect thiếu Idempotency-Key hoặc sai format ULID/UUID v4
401JWT_MISSING · JWT_INVALIDThiếu Authorization / signature-aud-exp không hợp lệ
403FORBIDDENToken thiếu claim project_id/sub hoặc thiếu quyền editor/viewer
404NOT_FOUNDanswer_template_id không tồn tại trong project scope
409NAME_CONFLICTTrùng (project_id, name) với template chưa xóa
409TEMPLATE_IN_USEXóa khi còn Workspace Version tham chiếu
500INTERNAL_ERRORLỗi không phân loại — response không lộ stacktrace
Thiết kế đích: owner project-svc; INSERT project.answer_templates
POST/v1/projects/{pid}/answer-templates/{id}/updateCập nhật template
Sửa bản draft: name / description / layout / variables.
Idempotency-Keyđích: project-svcTKCT 3.9.1.4404 NOT_FOUND400 VALIDATION_FAILED
Đặc tả (thiết kế đích) — sequence · luồng · request/response · mã lỗi

Sequence diagram

sequenceDiagram
    actor Dev as Dev/PO
    participant Web as web-bff
    participant PS as project-svc
    Dev->>Web: POST /v1/projects/{pid}/answer-templates/{id}/update (JWT + Idempotency-Key)
    Web->>PS: forward
    Note over PS: load template (404 nếu không có)
    Note over PS: validate field đổi (name/description/layout/variables)
    Note over PS: UPDATE bản draft
    PS-->>Web: 200 template đã cập nhật

Mô tả luồng nghiệp vụ

BướcĐối tượngHành độngMô tả
1Dev/POSửa metadata/layoutVisual Builder hoặc JSON editor; nút Lưu enable khi có diff.
2project-svcValidate + UPDATEid không thuộc project → 404; field sai → 400 VALIDATION_FAILED; đổi name trùng → 409. UPDATE bản draft (version đã publish bất biến).
3FEHiển thị200 → tắt badge diff; lỗi → highlight details.field.

Mô tả API

Method typePathMô tả
POST/v1/projects/{pid}/answer-templates/{id}/updateCập nhật metadata + layout bản draft. Không đụng version đã publish.
API Request

Header: Authorization (Y), Idempotency-Key (Y). Body: {name?, description?, layout?, variables?} (full-replace phần gửi).

API Response

200 — template đã cập nhật. 404 nếu không thuộc project.

Bảng mã lỗi

HTTPcodemessage
400VALIDATION_FAILEDBody/param sai schema hoặc vượt giới hạn
400IDEMPOTENCY_KEY_REQUIREDPOST side-effect thiếu Idempotency-Key hoặc sai format ULID/UUID v4
401JWT_MISSING · JWT_INVALIDThiếu Authorization / signature-aud-exp không hợp lệ
403FORBIDDENToken thiếu claim project_id/sub hoặc thiếu quyền editor/viewer
404NOT_FOUNDanswer_template_id không tồn tại trong project scope
409NAME_CONFLICTTrùng (project_id, name) với template chưa xóa
409TEMPLATE_IN_USEXóa khi còn Workspace Version tham chiếu
500INTERNAL_ERRORLỗi không phân loại — response không lộ stacktrace
Thiết kế đích: owner project-svc; UPDATE bản draft, publish qua Workspace Version
POST/v1/projects/{pid}/answer-templates/{id}/deleteXóa template
Xóa mềm. Bị chặn khi còn agent version tham chiếu (template_id, version).
Idempotency-Keyđích: project-svcTKCT 3.9.1.6409 TEMPLATE_IN_USE
Đặc tả (thiết kế đích) — sequence · luồng · request/response · mã lỗi

Sequence diagram

sequenceDiagram
    actor Dev as Dev/PO
    participant Web as web-bff
    participant PS as project-svc
    participant VS as version-svc
    Dev->>Web: POST /v1/projects/{pid}/answer-templates/{id}/delete (JWT + Idempotency-Key)
    Web->>PS: forward
    PS->>VS: S2S GET /v1/internal/answer-templates/{id}/references
    alt còn Workspace Version tham chiếu
        VS-->>PS: references[...]
        PS-->>Web: 409 TEMPLATE_IN_USE + details.referenced_by[]
    else không còn ref
    Note over PS: UPDATE deleted_at = now() (xóa mềm)
        PS-->>Web: 200 {deleted: true}
    end

Mô tả luồng nghiệp vụ

BướcĐối tượngHành độngMô tả
1Dev/POXóa + confirmModal cảnh báo xóa mềm; version đã publish vẫn giữ để replay.
2project-svcCheck ref (delete gate)S2S hỏi version-svc: còn Workspace Version nào ref template? Còn → 409 TEMPLATE_IN_USE + referenced_by[].
3project-svcSoft deleteKhông còn ref → UPDATE deleted_at = now(). KHÔNG hard-delete version (immutable, audit).

Mô tả API

Method typePathMô tả
POST/v1/projects/{pid}/answer-templates/{id}/deleteXóa mềm template; gate TEMPLATE_IN_USE nếu còn Workspace Version tham chiếu.
API Request

Header: Authorization (Y), Idempotency-Key (Y).

API Response

200{deleted: true} (idempotent). 409 TEMPLATE_IN_USE + details.referenced_by[].

Bảng mã lỗi

HTTPcodemessage
400VALIDATION_FAILEDBody/param sai schema hoặc vượt giới hạn
400IDEMPOTENCY_KEY_REQUIREDPOST side-effect thiếu Idempotency-Key hoặc sai format ULID/UUID v4
401JWT_MISSING · JWT_INVALIDThiếu Authorization / signature-aud-exp không hợp lệ
403FORBIDDENToken thiếu claim project_id/sub hoặc thiếu quyền editor/viewer
404NOT_FOUNDanswer_template_id không tồn tại trong project scope
409NAME_CONFLICTTrùng (project_id, name) với template chưa xóa
409TEMPLATE_IN_USEXóa khi còn Workspace Version tham chiếu
500INTERNAL_ERRORLỗi không phân loại — response không lộ stacktrace
Thiết kế đích: owner project-svc; delete gate qua S2S version-svc references (workspace_version_components)

Versioning — Draft & Workspace Version thiết kế đích

Answer template thành kind='answer_template' trong version-svc, mô hình v0-mutable: Draft = row version_no=0 mutable trong bảng version.components, published = version_no≥1 frozen + ký. DSL luôn ở version-svc. FE → web-bff → project-svc → S2S → version-svc; /v1/internal/* KHÔNG lộ FE. Chi tiết: documents/ANSWER_TEMPLATE_VERSION_INTEGRATION.md.

FE-facing (public — web-bff → project-svc)
POST/v1/projects/{pid}/answer-templates/{id}/draftLưu DSL vào v0
FE lưu answer-template DSL nháp. project-svc verify authz rồi S2S POST /v1/internal/components/{ws}/upsert {kind:'answer_template'} xuống version-svc. UPSERT idempotent, expected_content_hash lệch → 409 DRAFT_CONFLICT.
web-bff → project-svc → version-svcJWT user
Đặc tả (thiết kế đích) — sequence · luồng · request/response · mã lỗi

Sequence diagram

sequenceDiagram
    actor Dev as Dev/PO
    participant Web as web-bff (public /v1)
    participant PS as project-svc (authz)
    participant VS as version-svc (S2S)
    Dev->>Web: POST /v1/projects/{pid}/answer-templates/{id}/draft (JWT + Idempotency-Key)
    Web->>PS: forward (verified JWT, X-Project-Id)
    Note over PS: verify quyền editor
    PS->>VS: S2S POST /v1/internal/components/{ws}/upsert {kind:'answer_template', component_id, dsl}
    Note over VS: content_hash JCS RFC 8785
    Note over VS: UPSERT components version_no=0 (v0, mutable) — KHÔNG mint
    alt expected_content_hash lệch
        VS-->>PS: 409 DRAFT_CONFLICT
    else ok
        VS-->>PS: 200 {content_hash, updated_at}
    end
    PS-->>Web: 200
    Web-->>Dev: Đã lưu nháp

Mô tả luồng nghiệp vụ

BướcĐối tượngHành độngMô tả
1Dev/POLưu nhápFE gửi answer-template DSL. web-bff → project-svc.
2project-svcAuthz + orchestrateVerify quyền editor rồi S2S xuống version-svc (clients/version.py) — KHÔNG lộ route internal ra FE.
3version-svcUPSERT Draftcontent_hash JCS server-side, UPSERT components version_no=0 (v0, mutable). content_hash JCS. KHÔNG mint, KHÔNG ký.
4version-svcOptimistic lockexpected_content_hash lệch → 409 DRAFT_CONFLICT (fetch lại rồi retry).

Mô tả API

Method typePathMô tả
POST/v1/projects/{pid}/answer-templates/{id}/draftLưu answer-template DSL vào Draft (mutable) qua project-svc → S2S version-svc. Chưa sinh version.
API Request

Header: Authorization (Y), Idempotency-Key (Y). Body: {dsl, expected_content_hash?}.

API Response

200{content_hash, updated_at}. 409 DRAFT_CONFLICT khi hash lệch.

Thiết kế đích: public project-svc → S2S version-svc /v1/internal/components/{ws}/upsert {kind:answer_template}
POST/v1/projects/{pid}/versions/publishPublish Workspace Version (mint card)
Route publish chung của project-svc (đã phục vụ agent/workflow). project-svc S2S POST /v1/internal/versions/publish. Answer template mint tự động qua diff. Idempotency-Key bắt buộc.
Idempotency-Keyweb-bff → project-svc → version-svcJWT user
Đặc tả (thiết kế đích) — sequence · luồng · request/response · mã lỗi

Sequence diagram

sequenceDiagram
    actor Dev as Dev/PO
    participant Web as web-bff (public /v1)
    participant PS as project-svc (authz)
    participant VS as version-svc (S2S)
    participant OB as OpenBao Transit
    Dev->>Web: POST /v1/projects/{pid}/versions/publish (JWT + Idempotency-Key)
    Web->>PS: forward
    PS->>VS: S2S POST /v1/internal/versions/publish {workspace_id}
    Note over VS: [1 tx] lock ws → đọc v0 → diff TỪNG component (gồm answer_template)
    loop mỗi answer_template đổi hash
        VS->>OB: sign Ed25519 (content_hash)
        OB-->>VS: signature
    Note over VS: INSERT components version_no=next (freeze DSL, immutable)
    end
    Note over VS: INSERT workspace_versions header trỏ các version_no chốt
    VS-->>PS: 201 {workspace_version_id, version_no, minted[]}
    PS-->>Web: 201
    Web-->>Dev: Version mới + badge published

Mô tả luồng nghiệp vụ

BướcĐối tượngHành độngMô tả
1Dev/POPublishFE gọi publish Workspace Version. web-bff → project-svc.
2project-svcOrchestrateS2S POST /v1/internal/versions/publish {workspace_id} (route chung agent/workflow).
3version-svcDiff + mint1 tx: v0 đổi hash → INSERT components version_no=next (freeze + ký Ed25519 OpenBao); không đổi → carry-forward version_no cũ.
4version-svcGhi componentsINSERT workspace_versions header trỏ version_no vừa chốt. minted[] gồm answer template.
5version-svcTrả201 {workspace_version_id, version_no, minted[]}. Không phát event; vkey agent provision sau commit.

Mô tả API

Method typePathMô tả
POST/v1/projects/{pid}/versions/publishSnapshot Draft → Workspace Version, mint mọi component đổi gồm answer_template.
API Request

Header: Authorization (Y), Idempotency-Key (Y). Body: {workspace_id}.

API Response

201{workspace_version_id, version_no, minted[]}.

Thiết kế đích: public project-svc → S2S version-svc /v1/internal/versions/publish; answer template mint như agent/workflow
S2S internal (project-svc → version-svc — KHÔNG lộ FE, FR-BFFWS-011)
POST/v1/internal/components/{workspace_id}/upsertUPSERT DSL vào v0 (mutable)
Body {kind:'answer_template', component_id, dsl, expected_content_hash?}. UPSERT version.components row version_no=0 — mutable, tính content_hash JCS server-side, chưa ký, KHÔNG mint. expected_content_hash lệch → 409 DRAFT_CONFLICT. Áp cho cả 5 kind (v0 chung).
X-Project-Id · RLSv0-modelversion-svc
GET/v1/internal/components/{workspace_id}/resolveResolve v0 — DSL draft hiện hành
Đọc row version_no=0 — DSL draft đang sửa. Dùng cho GET detail/Visual Builder (lấy DSL v0) + runtime env development (trôi cùng v0). project-svc gọi khi mở chi tiết template.
S2Sv0-modelversion-svc
POST/v1/internal/versions/publishPublish — copy v0 → version_no≥1
1 transaction: lock workspace → đọc v0 → diff TỪNG component gồm answer template. v0 đổi hash → INSERT components version_no=next (freeze DSL + ký Ed25519), không đổi → carry-forward version_no cũ. INSERT workspace_versions header trỏ các version_no chốt. minted[] gồm answer template. v0 vẫn mutable cho version sau.
Idempotency-KeyS2Sv0-modelversion-svc
GET/v1/internal/versions/{workspace_version_id}/resolveResolve version — DSL chốt
Trả DSL đã đóng băng (từ components version_no≥1workspace_versions trỏ tới) cho agent-runtime render lúc chat. Khác resolve v0 ở trên (v0 = draft đang sửa; đây = version đã publish).
S2Sv0-modelversion-svc

S2S nội bộ — delete gate

Reverse lookup refs phục vụ gate xóa TEMPLATE_IN_USE. As-built: refs materialize vào agent_version_answer_template_refs lúc mint. Thiết kế đích: gate đọc ref từ components (kind='answer_template', version_no≥1) mà Workspace Version trỏ tới — TEMPLATE_IN_USE khi card còn trong Workspace Version chưa retire.

GET/v1/internal/answer-templates/{template_id}/referencesReverse refs — delete gate (S2S)
version-svc trả danh sách version còn tham chiếu template — project-svc (owner CRUD ở thiết kế đích) hỏi TRƯỚC khi xóa mềm; còn ref → 409 TEMPLATE_IN_USE. Template không còn ref trả [], KHÔNG 404. As-built: caller là agent-runtime, index (project_id, template_id).
S2S · Ed25519 aud=version-svcCG-004 Req 4 · TKCT 3.9.1.6version-svc
Đặc tả (thiết kế đích) — sequence · luồng · request/response · mã lỗi

Sequence diagram

sequenceDiagram
    participant PS as project-svc
    participant VS as version-svc
    PS->>VS: GET /v1/internal/answer-templates/{id}/references (S2S Ed25519, aud=version-svc)
    Note over VS: query index (project_id, component_id)<br/>trên components (kind=answer_template, version_no≥1)
    alt còn version tham chiếu
        VS-->>PS: 200 references[...]
    else không còn ref
        VS-->>PS: 200 references []
    end

Mô tả luồng nghiệp vụ

BướcĐối tượngHành độngMô tả
1project-svcGọi reverse lookupTrước khi xóa mềm, hỏi version-svc version nào còn ref template.
2version-svcTrả refs từ indexĐọc components (kind=answer_template, version_no≥1) mà Workspace Version trỏ tới — không cross-schema. Không còn ref trả [] (KHÔNG 404).
3project-svcQuyết định gateCòn ref → 409 TEMPLATE_IN_USE; rỗng → cho xóa mềm.

Mô tả API

Method typePathMô tả
GET/v1/internal/answer-templates/{template_id}/referencesReverse refs của answer template trong project scope — phục vụ delete gate. S2S nội bộ.
API Request

Header: S2S bearer Ed25519, aud=version-svc; tenant từ claim.

API Response

200{template_id, references[]}; mỗi ref: workspace_version_id, version_no. Rỗng → [] (không 404).

Bảng mã lỗi

HTTPcodemessage
401S2S_INVALIDS2S bearer thiếu / sai aud / hết hạn
500INTERNAL_ERRORAn unexpected error occurred.
Thiết kế đích: S2S version-svc; ref đọc từ components (kind=answer_template, version_no≥1) mà Workspace Version trỏ tới

Transport — card trên stream chat (không có endpoint riêng)

Answer Card KHÔNG thêm API runtime nào: card là field trên stream POST /v1/chat-messages của phân hệ Conversation, persist conv.messages.card_payload, replay đọc qua transcript API. Đặc tả canonical = TKCT 3.12.6 (nhúng dưới).

SSEmessage_end.metadata.card · trên POST /v1/chat-messagesHiển thị Card — transport, KHÔNG có endpoint riêng
Card render một lớp ở agent-runtime rồi đi về Chat UI trên CHÍNH stream chat (field ChatMessageMetadata.card always-present nullable, OpenAPI 04 v1.5.0); conversation-svc persist card_payload bản POST-FILTER để transcript replay re-render không re-filter. Crisis bypass không render card.
TKCT 3.12.6agent-runtime renderconversation-svc persistweb-bff forward verbatim
Đặc tả — TKCT 3.12.6 (sequence diagram · luồng nghiệp vụ · request/response · mã lỗi)
Contract khâu vận chuyển + lưu card (ĐÃ LAND — OpenAPI 04 v1.5.0 + DDD 04 §3.4): chức năng "Hiển thị Answer Template" KHÔNG có endpoint riêng — card render lúc runtime bởi agent-runtime Card folder (card/card_payload_renderer.py, Jinja sandbox; authoring/versioning đã có per CG-004: schema card, card.templates/card.template_versions, migration 0010_card_schema.py) rồi đi về Chat UI trên CHÍNH stream của POST /v1/chat-messages (3.12.3.1). OpenAPI 04 v1.5.0 đã định nghĩa field card (ChatMessageMetadata.card, always-present nullable) + x-sse-event-message-end.metadata.card + cột card_payload (DDD 04 §4.1). Cơ chế đã chốt:

1. As-built (T-CONV-09): card đi trên message_end.metadata.card (kênh parity engine có sẵn, KHÔNG thêm event mới) — KHÔNG phải done. Contract always-present-nullable: key metadata.card LUÔN có mặt trên frame message_end, null khi turn không card. OpenAPI 04 v1.5.0 x-sse-event-message-end.metadata.card (nullable object).
2. As-built (T-CONV-09): cột conv.messages.card_payload JSONB (migration 0017_message_card_payload) lưu bản POST-FILTER để transcript replay re-render được card.
3. card_payload lưu bản POST-FILTER — conversation-svc apply CÙNG allowlist filter Answer Card với web-bff trước khi persist, để transcript replay (3.12.2.2, không đi qua output filter) không "sống lại" action đã strip.
4. Allowlist action URL phải ENFORCE trong code — OPTIMAL §13 filter 5 hiện chỉ là spec, chưa enforce. Three-layer defense-in-depth: agent-runtime (render-time) → conversation-svc (pre-proxy + pre-persist) → web-bff (output filter). open_url ngoài allowlist → strip action, log warning (card_action_url_blocked), KHÔNG fail turn (degrade log-only — KHÔNG mã lỗi mới). Allowlist cấu hình per-env ở environment-svc (card_action_url_allowlist, FR-ENV-007), bao gồm cả scheme deeplink thương mại (vd viettelmoney://, https://vm.viettel.vn/...) cho luồng bán hàng 3.12.9. Cả ba lớp gọi CÙNG một shared helper vds_platform_common.card.filter_card_actions(card, allowlist) (KHÔNG copy-paste, T-CONV-07).
- Config surface (environment-svc): card_action_url_allowlist: string[] (list literal scheme://host) nằm trong body POST /v1/environments/{env_id}/config ngang hàng 4 binding type; đọc qua GET .../config (KHÔNG mask — không phải secret) + resolve façade /v1/internal/resolve trả key card_action_url_allowlist theo env. Env chưa set → [].
- Match rule: literal scheme + host (urlsplit → so scheme + netloc); path/query KHÔNG vào match (viettelmoney://pay?order=X khớp entry viettelmoney://pay). Allowlist rỗng → strip MỌI action có URL. Action postback (không có URL) luôn giữ.
- Injection guard: query param client-tô (ctx=/order=) bị loại TRƯỚC khi so host (không lách host-match); param nhạy cảm server-sourced (conversation_id từ turn ctx, order_id từ webhook) do caller seam inject SAU filter, helper KHÔNG tự sinh.
5. Naming action: action kind là postback (gửi turn mới về LLM) / open_url (URL/deeplink trong allowlist) — card_templates/catalog.py:139.
6. Agent-runtime emit card trên RUN path: hiện run path trả final_answer: string (executor message.done), catalog 8-component (card_templates/) chỉ là authoring CRUD, không vào graph/executor/invoke. Cần: khi DSL node trả card, executor render rồi emit object card trong done.metadata.card (mục 1). Đây là khâu net-new, không phải authoring có sẵn.
7. As-built (T-CONV-11, Seam 4) — FE renderer Answer Card (studio-svc): thêm component net-new app/components/base/chat/chat/answer/answer-card.tsx render 8 catalog type (form/text/image_media/list/selector/button_cta/carousel/confirmation) sau khối Citation trong answer/index.tsx; đọc card từ message_end.metadatahooks.ts onMessageEnd (mirror responseItem.citation), null khi vắng card. Action: postbackonSend(payload) (gửi turn mới, reuse hạ tầng chat engine), open_urlwindow.open(url, '_blank', 'noopener,noreferrer') sau scheme-guard (https/http; scheme lạ → console.warn, KHÔNG mở), confirmation yes/no→onSend(yes_payload/no_payload). Schema version lạ (!= "1.0") → fallback text "phiên bản card không hỗ trợ", bubble không vỡ. FE chỉ render — KHÔNG re-validate schema (server validate trước wire), KHÔNG dựng allowlist riêng (server strip ở Seam 1+2). Chi tiết §4.2.
8. As-built (T-CONV-10, Seam 3): web-bff là MAPPER thuần — forward metadata.card nguyên trạng ở CẢ HAI điểm emit message_end trong compat router sse.py (site streaming terminal + site advanced_chat done/workflow-fallback); always-present-nullable giữ nguyên (card = null khi vắng). web-bff KHÔNG re-filter, KHÔNG validate card schema, KHÔNG import allowlist ở scope này: card đã được strip ở agent-runtime (render-time) + conversation-svc (pre-persist), nên lớp web-bff là no-op defense-in-depth (§N2 point 3) — không phải output-filter thật. Nếu sau này bật lớp filter web-bff thì là task riêng.

Owner end-to-end: agent-runtime (emit card trên run path + render-time filter) → conversation-svc (SSE contract + persistence + filter) → web-bff (output filter) → studio-svc FE (Answer Card renderer). Authoring/versioning card thuộc TKCT 3.9 Card Template (CG-004 done). Card là tính năng net-new ở MỌI tầng, không phải wiring nhỏ.

Bảng ghi nhận thay đổi tài liệu

_Loại thay đổi: A – Tạo mới, M – Sửa đổi, D – Xóa bỏ_

Ngày thay đổiVị trí thay đổiLoại thay đổiNguồn gốcPhiên bản cũMô tả thay đổiPhiên bản mớiNgười approve
11/06/2026Toàn bộAOPTIMAL §18.3.6/§13 + DDD 08 Card folder + DDD 17-1.0-
17/06/2026Blockquote + §4.1.3 kind + §4.1.4MĐối soát gap CONV-GAP-003/004/005 + ADR-DRAFT sales-integration1.0Chốt-theo-thiết-kế: card transport done.metadata.card + card_payload persist; ENFORCE allowlist 3 lớp (gồm scheme deeplink thương mại); align naming postback/open_url1.1-
22/06/2026Blockquote §1-2 + §1 serviceMT-CONV-09 Seam 2 as-built (conversation-svc)1.1Card vận chuyển trên message_end.metadata.card (always-present nullable, KHÔNG phải done) + persist conv.messages.card_payload (migration 0017); OpenAPI 04 v1.5.0; allowlist-strip log-only (không error code mới)1.2-
22/06/2026Blockquote §8 + §1 serviceMT-CONV-10 Seam 3 as-built (web-bff)1.2web-bff forward metadata.card ở CẢ HAI site message_end (compat router sse.py); MAPPER thuần — no-op output filter (đã strip ở producer/persist), KHÔNG re-filter/validate schema1.3-
22/06/2026§1 service + §38 note + §4.2 (mới)MT-CONV-11 Seam 4 as-built (studio-svc FE)1.3FE Answer Card renderer answer/answer-card.tsx render 8 catalog type sau Citation; đọc cardhooks.ts onMessageEnd (mirror citation); postbackonSend, open_urlwindow.open scheme-guard, confirmation→yes/no payload; schema-version lạ → fallback text1.4-
22/06/2026Blockquote + §2 + mermaid + §4.1.2/3/4 + Link swaggerMT-CONV-15 doc-sync (gỡ framing "đề xuất")1.4Gỡ framing "đề xuất"/"chốt theo thiết kế (đưa vào regen khi triển khai)" → card delivery trình bày là ĐÃ LAND; OpenAPI 04 version v1.3.0 → v1.5.0 (field card + x-sse-event-message-end.metadata.card); rename card/renderer.pycard/card_payload_renderer.py ở luồng answer-card1.5-
05/07/2026§4.3 (mới)MKBNV 2.2.10 Conversation v2 (2026-06-16) §2.2.10.31.5Bổ sung đặc tả control màn Chat end-user theo KBNV 2.2.10.3 — ràng buộc control-level Answer Template Card (maxlen từng field, banner + badge, "Xem thêm", Meta Row ≤3, Primary vs Secondary Button)1.6-

1. Tên service

agent-runtime (:50056) — render card từ resolved_dsl.json (Jinja sandbox, JCS hash verify) trên run path = ĐÃ BUILD (Seam 1 producer, T-CONV-08); web-bff (:4001) — MAPPER thuần forward message_end.metadata.card (engine-parity) = ĐÃ BUILD (Seam 3, T-CONV-10), no-op output filter (card đã strip ở producer/persist — §N2 point 3); conversation-svc (:50067) — vật mang SSE (message_end.metadata.card) + persistence (card_payload) = ĐÃ BUILD (Seam 2, T-CONV-09); studio-svc (:3000) — FE Answer Card renderer (answer/answer-card.tsx) render 8 catalog type + dispatch postback/open_url = ĐÃ BUILD (Seam 4, T-CONV-11).

As-built (đừng nhầm authoring với delivery): card/renderer.py Jinja sandbox render system prompt (agent-card DSL), KHÔNG phải card UI; answer-card render path mới là card/card_payload_renderer.py (T-CONV-08) — reuse card_templates/schema_tools.validate_schema + compute_schema_hash + sandbox chung. JCS hash + Ed25519 sig là publish-time trên card.template_versions (migration 0010_card_schema); run path verify lại schema_hash pin trước khi emit. Run path đã gắn card lên frame done (executor.py _done_payload, producer-native done.payload.card) khi DSL node trả card hợp lệ; degrade hash-fail/schema-invalid → bỏ card, trả final_answer text. Authoring template (CRUD + versions + catalog 8-component catalog.py:139) ĐÃ CÓ. Cả 4 seam ĐÃ BUILD: SSE transport + persist (Seam 2 T-CONV-09) → web-bff passthrough (Seam 3 T-CONV-10) → FE renderer (Seam 4 T-CONV-11, answer/answer-card.tsx).

2. Mô tả chung

Khi agent trả lời dạng có cấu trúc (form, danh sách lựa chọn, nút CTA, carousel...), answer không phải text thuần mà là một Answer Card — layout + field binding + action định nghĩa trong agent DSL, dựng bằng Visual Builder (TKCT 3.9). Runtime flow: agent-runtime load card definition từ resolved_dsl.json (verify JCS hash trước khi nạp), render qua Jinja sandbox (block __import__, missing variable defaults) + filter action URL allowlist (render-time), conversation-svc filter lại + persist bản POST-FILTER, web-bff forward metadata.card nguyên trạng (mapper — Seam 3), Chat UI render component tương ứng.

Khâu vận chuyển — card đã render đi trong SSE stream (message_end.metadata.card) và lưu vào message (card_payload) ra sao để xem lại transcript — đã land (T-CONV-08..11). Trang này đặc tả phần mở rộng đó.

3. Tính năng

End-user thấy answer dạng card tương tác trong chat (thay vì text); xem lại transcript vẫn render đúng card (từ card_payload đã persist); action trên card (button/selector) gửi turn chat mới hoặc mở URL trong allowlist.

4. Mô tả luồng xử lý

4.1. Hiển thị Card Template
4.1.1. Luồng tổng quan
sequenceDiagram
    autonumber
    actor U as End User
    participant FE as Chat UI
    participant CV as conversation-svc (:50067)
    participant AR as agent-runtime (:50056)
    participant B as web-bff (:4001)
    participant PG as PostgreSQL (schema conv)

    U->>FE: Gửi câu hỏi (turn chat thường)
    FE->>B: POST /v1/chat-messages (SSE)
    B->>CV: proxy request
    Note over CV: Crisis check in-process trước (p99 < 5ms)<br/>— crisis bypass KHÔNG render card
    CV->>AR: invoke (non-crisis)
    Note over AR: Agent quyết định trả lời bằng card<br/>card/card_payload_renderer.py: Jinja sandbox render<br/>resolved_dsl.json (JCS hash verify)
    AR-->>CV: SSE chunk* + done.payload.card (producer-native)
    Note over CV: Apply allowlist filter Answer Card<br/>(schema + length cap + action URL —<br/>CÙNG cấu hình với web-bff) trước khi proxy
    CV-->>B: proxy SSE 1:1 (card đã filter)
    Note over B: Output filter (OPTIMAL §13 filter 5):<br/>validate lại schema + allowlist —<br/>defense-in-depth, no-op khi CV đã strip
    B-->>FE: message_end.metadata.card
    CV->>PG: INSERT assistant msg + card_payload POST-FILTER<br/>+ outbox conv.message.appended.v1 (1 transaction, sau done)
    FE-->>U: Render card component (form/list/button/carousel)
4.1.2. Danh sách API và tiến trình
STTNameTypeURLMethodServiceTài liệu tham khảo
1Gửi chat message — field card trên message_endAPI/v1/chat-messagesPOSTconversation-svc(file này + 3.12.3.1)
4.1.3. Mô tả API
API 1: Field card trên message_end.metadata của stream chat
Method typePathMô tả
POST/v1/chat-messagesNhư 3.12.3.1; trang này chỉ đặc tả delta card

API Request

Không đổi so với 3.12.3.1 — card là quyết định phía agent, client không gửi gì thêm.

_Permission yêu cầu:_ như 3.12.3.1.

API Response

_Body — field card trong message_end.metadata (always-present nullable):_

Tên tham sốKiểu dữ liệuMô tả
cardObject (nullable)Rendered Answer Card — null với answer text thuần
card.template_idStringCard template trong agent DSL
card.template_versionStringSemver card definition đã render
card.layoutObjectCây component đã render (đã thay binding bằng giá trị)
card.layout.typeEnumform, text, image, list, selector, button, carousel, confirmation
card.layout.childrenArrayComponent con (đệ quy cùng shape)
card.actionsArrayAction khả dụng — URL đã qua allowlist filter (conversation-svc, web-bff validate lại)
card.actions[].idStringAction id
card.actions[].kindEnumpostback (gửi turn mới về LLM), open_url (URL/deeplink trong allowlist — gồm deeplink thương mại Viettel Money cho 3.12.9)
card.actions[].labelStringNhãn nút
card.actions[].payloadString (nullable)Nội dung gửi khi send_message, hoặc URL đích (trong allowlist) khi open_url

_Example response 200 (trích frame message_end — field card):_

{
  "conversation_id": "b3f1c2ae-8d4f-4c2a-9b7e-5a6d3e8f1c2b",
  "message_id": "a8d4f7c2-3e6b-4f9d-8b2a-1c5e9d7f3a48",
  "answer": "Anh/chị tham khảo 2 gói cước phù hợp dưới đây.",
  "metadata": {
    "is_crisis": false,
    "served_by": "agent_runtime",
    "tokens_in": 410,
    "tokens_out": 188,
    "ttft_ms": 580,
    "latency_ms": 2310,
    "card": {
      "template_id": "goi-cuoc-compare",
      "template_version": "1.2.0",
      "layout": {
        "type": "list",
        "children": [
          { "type": "text", "children": [], "props": { "value": "C90 — 4GB/ngày, 90.000đ/tháng" } },
          { "type": "text", "children": [], "props": { "value": "SD135 — 5GB/ngày, 135.000đ/tháng" } }
        ]
      },
      "actions": [
        { "id": "act-1", "kind": "open_url", "label": "Đăng ký 5G Gia Đình", "payload": "viettelmoney://pay?order=5G-GIADINH&ctx={conversation_id}" },
        { "id": "act-2", "kind": "postback", "label": "So sánh thêm gói", "payload": "So sánh các gói 5G khác" }
      ]
    }
  }
}

_Link swagger:_ /openapi.jsonPOST /v1/chat-messages (field card = ChatMessageMetadata.card + x-sse-event-message-end.metadata.card, OpenAPI 04 v1.5.0).

4.1.4. Mô tả luồng nghiệp vụ
BướcĐối tượngHành độngMô tả
1End UserGửi câu hỏiTurn chat thường — không khác 3.12.3.1.
2agent-runtimeRender cardDSL node trả card → card/card_payload_renderer.py render Jinja sandbox; biến thiếu áp missing variable defaults; hash verify fail → bỏ card, trả text thuần (degrade). Emit done.payload.card (producer-native).
3conversation-svcFilter cardApply CÙNG allowlist filter Answer Card với web-bff (validate schema + length cap; action URL ngoài allowlist → strip action đó, log warning, KHÔNG fail cả turn) trước khi proxy SSE.
4web-bffForward (mapper)As-built T-CONV-10: forward metadata.card nguyên trạng ở cả hai site message_end (compat router sse.py) — KHÔNG re-filter/validate schema trong scope này (card đã strip ở bước 2-3); lớp output filter web-bff là no-op defense-in-depth (§N2 point 3), bật thật là task riêng.
5conversation-svcPersistSAU done (DDD 04 §2.3), lưu card_payload bản POST-FILTER (bước 3) cùng message trong transaction outbox — transcript replay render thẳng bản đã lưu, không cần re-filter vì stored = filtered (action strip không sống lại).
6Chat UIRenderComponent theo layout.type; postback → gửi turn mới; open_url deeplink thương mại (vd "Đăng ký 5G Gia Đình") → mở app Viettel Money, xác nhận thanh toán đi luồng 3.12.9; transcript xem lại render từ card_payload.
4.2. Xử lý FE Answer Card (studio-svc — Seam 4, T-CONV-11)

Tầng FE cuối của chuỗi delivery: render card trong bubble trả lời và điều phối action. Reuse hạ tầng chat của engine (useChatContext().onSend, IChatItem, parse SSE message_endservice/base.ts) — chỉ thêm một component mới + một dòng đọc metadata, KHÔNG đụng pipeline SSE/persist (đã do Seam 1-3 lo).

Vị tríFileHành vi
Đọc cardapp/components/base/chat/chat/hooks.ts (onMessageEnd)`responseItem.card = messageEnd.metadata?.cardnull — mirror responseItem.citation; null` khi turn không card (bám contract always-present-nullable Seam 2).
Wire renderapp/components/base/chat/chat/answer/index.tsxSau khối Citation: {!!item.card && !responding && <AnswerCard data={item.card} />}.
Componentapp/components/base/chat/chat/answer/answer-card.tsx (net-new)FC<{ data: CardPayload; onAction?: (a: CardAction) => void }> — render theo data.components[], mỗi phần tử có type ∈ 8 enum.
Typeapp/components/base/chat/chat/type.tsThêm `card?: CardPayload \null vào IChatItem + Metadata; khai báo CardPayload/CardComponent/CardAction mirror catalog (card_templates/catalog.py`), KHÔNG import từ Python.

Render 8 type: form (fields + submit_label), text (variant heading/body/caption), image_media (image/video/audio), list (items), selector (single/multi options), button_cta (label + action), carousel (cards), confirmation (prompt + yes/no label). Action điều phối qua useChatContext():

- postbackonSend(payload) (gửi turn mới, reuse cơ chế SuggestedQuestions); no-op khi readonly. - open_urlwindow.open(url, '_blank', 'noopener,noreferrer') CHỈ khi url qua scheme-guard FE (https/http); scheme lạ (vd javascript:) → console.warn, KHÔNG mở. FE chỉ guard scheme cuối — KHÔNG dựng allowlist riêng (server đã strip action ngoài allowlist ở Seam 1+2). - confirmation → yes: onSend(yes_payload), no: onSend(no_payload).

Degrade: card_schema_version != "1.0" → render fallback text (chat.card.unsupportedVersion), KHÔNG throw — bubble vẫn sống. FE render plain/escaped text (KHÔNG dangerouslySetInnerHTML), KHÔNG re-validate schema (server validate trước wire).

4.3. Ràng buộc control-level Answer Template Card (KBNV §2.2.10.3)

Trên màn Chat end-user, Answer Template Card render dưới dạng control tổng hợp (control #14, 3.12.3.3) gồm các thành phần con #15–20. Ràng buộc hiển thị theo KBNV:

Thành phần conControlRequireMax LengthRàng buộc hiển thị
BannerTemplate Banner (#15)NoẢnh minh hoạ/icon lớn phía trên card, có badge label ở góc trên trái (vd "Dịch vụ 5G", "Khuyến mãi"); không có ảnh cấu hình → icon mặc định theo loại nội dung
Tiêu đềTemplate Title (#16)Yes100 ký tựQuá dài → hiển thị tối đa 3 dòng kèm nút "Xem thêm"
Mô tảTemplate Description (#17)No300 ký tựĐặt dưới Title
MetaTemplate Meta Row (#18)NoHàng icon + text, tối đa 3 item (vd tốc độ, giá, thời hạn hợp đồng)
Nút chínhTemplate Primary Button (#19)No50 ký tự (label)Nền đỏ chữ trắng; action ánh xạ card.actions[]open_url (deeplink/URL trong allowlist) hoặc postback (gửi turn mới)
Nút phụTemplate Secondary Button (#20)No50 ký tự (label)Nền đỏ nhạt chữ đỏ; độc lập với Primary Button, có thể vắng nếu template không cần action phụ

Các ràng buộc trên là tầng render FE — không đổi contract wire card ở §4.1.3; label nút map field card.actions[].label, độ dài cắt/ellipsis do FE xử lý. Use case tham chiếu: card so sánh gói với Title "5G Gia Đình", Primary "Đăng ký 5G Gia Đình", Secondary "Kiểm tra vùng phủ 5G".

Bảng mã lỗi

Không có mã lỗi riêng — card đi trên stream chat, mã lỗi dùng chung 3.12.3.1 → Bảng mã lỗi. Lỗi render card phía agent-runtime degrade về text thuần (không fail turn); action URL ngoài allowlist bị strip + log ở agent-runtime (render-time) + conversation-svc (pre-persist), answer text vẫn về client. web-bff (Seam 3, T-CONV-10) chỉ forward metadata.card — không phát mã lỗi card.

Nguồn: references/vds-ddd/TKCT-CONVERSATION/3.12.6_HienThiCardTemplate.md

Bảng mã lỗi — Answer Template API

Envelope {"error": {code, message, trace_id, ts, details}}. Bảng mã lỗi đầy đủ per API nằm trong đặc tả TKCT từng card.

400VALIDATION_FAILEDBody không phải JSON object / field sai schema / vượt giới hạn (name 3–100, description ≤ 500)
400IDEMPOTENCY_KEY_REQUIREDPOST side-effect thiếu header, hoặc key sai format ULID/UUID v4
401JWT_MISSING · JWT_INVALIDThiếu Authorization / signature-aud-exp không hợp lệ
403FORBIDDENToken thiếu claim project_id/sub, hoặc thiếu quyền viewer/editor trên answer template
404NOT_FOUNDtemplate_id không tồn tại trong project scope, hoặc đã xóa mềm
409NAME_CONFLICTTrùng (project_id, name) với template chưa xóa
409SEMVER_CONFLICTPublish trùng version đã tồn tại
409TEMPLATE_IN_USEDelete khi còn agent version giữ ref (template_id, version)
409IDEMPOTENCY_REPLAYIdempotency-Key tái sử dụng với body khác request gốc
422BUSINESS_RULE_VIOLATEDSchema nháp rỗng / component ngoài catalog / binding sai — không cho publish bản hỏng
429RATE_LIMITEDToo many requests — Kong per-route
500OPENBAO_SIGN_FAILED · INTERNAL_ERRORKý OpenBao Transit thất bại (retry 3 lần) / lỗi không phân loại — response không lộ stacktrace

Lưu ý & GAP

Visual Builder / Preview — FE-only

Canvas kéo-thả + preview substitute biến chạy trên Studio với definition từ API chi tiết. Không có REST render-preview riêng (đúng tiền lệ TKCT 3.9.3 / 3.9.4 — không bịa endpoint).

OpenAPI 04 đã phủ đủ

Cả 8 route design-time có trong 08-agent-runtime.openapi.yaml, kèm ChatMessageMetadata.card — không DRIFT contract.

Ngoài phạm vi danh mục

Chat (/v1/chat-messages public · /v1/projects/{pid}/environments/{env}/agents/{aid}/chat Studio), follow-up chips (TKCT 3.12.5), transcript replay (GET /v1/conversations/{id}/messages) là API phân hệ Conversation — card chỉ là field trên đó (xem mục Transport). DRIFT TKCT 3.12.5 vs as-built (route + cơ chế đã đổi sang GET /v1/conversations/{conv_id}/suggested-questions, conversation-svc, LLM-free) ghi nhận xử lý ở track T-958.