Appearance
Hello Agent 教程(GL-05 · 二开方 curl 级全流程)
状态基准:2026-09-15(§12 内容专栏 / §13 站内信 / §14 品牌资产自助已收录,各波次验收状态以 platform-status 为准;§7 补 Webhook 配置归属说明——回推为运维配置非租户自助,issue #354;SOP / 会话 / 多模态章节仍以既有实测为准;知识库 Tier B 与支付开闸见 knowledge-base-retrieval.md / payment-and-recharge.md。本文自 DX1-2 起归属
docs/guides/tutorial/)。 读者:拿到租户 + M2M client 凭据的二开方开发者(开通步骤见tenant-onboarding-runbook.md)。 目标:30 分钟内独立跑通「取 token → 提交含真实 LLM 节点的 SOP 任务 → SSE 收全程事件 → 查用量」。 底稿:以下命令 2026-07-07 全部在 ECS-B 真实栈实测跑通(scripts/gl-acceptance.sh --hello-agent即其脚本化形态)。 环境变量约定:BASE=https://<EIP>(单源 TLS 网关);自签证书阶段 curl 需-k。
0. 你需要的东西
| 项 | 来源 |
|---|---|
CLIENT_ID / CLIENT_SECRET | ops 发放(M2M client,client credentials grant) |
| 租户/用户 UUID | 已配置在 token claims 里,无需手工传参(API 从 JWT 取,绝不从 body 取) |
| scopes | 本教程需要 task:submit + task:read(建模另需 builder:* / sop:* / agent:create) |
1. 取 token(client credentials)
bash
# 本地 / 平台 realm 试验用 agenthub;品牌线换成该线 realm 名
TOKEN=$(curl -sk -X POST "$BASE/auth/realms/agenthub/protocol/openid-connect/token" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET" | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')- token 是 RS256 JWT。平台 realm 试验:
iss=https://<主机>/auth/realms/agenthub。品牌线:iss=https://<品牌域>/auth/realms/<realm>,aud含你的 SPA / M2M client。 - 常见 401:直连 Keycloak
:8080取 token(iss 不匹配)、client secret 轮换后未更新、permissions属性未配置。
2. 建模(项目 → Agent → SOP → 发布)
已有发布 SOP 的租户可跳到 §3。全部端点均为
Envelope响应({"code","message","data"})。UI 路径(Tier 0 配置台,ADR-0043):§2.1/§2.2 的项目与 Agent 建模、以及模型组/模型注册/路由与知识库文档上传,也可经 web-builder 配置台在浏览器完成——部署
--with-frontend后打开https://<EIP>/builder/→ OIDC 登录 → 项目列表 → 项目详情 → Resource Modeling(Models / Knowledge Bases / Agents 三个 Tab;Agent 发布流含模型组+KB 绑定与乐观锁)。SOP 建模(§2.3/§2.4)自 Tier 1 起也可全程走画布(Tier 1 建模控制台波次,2026-06 落地):项目详情 → SOP Design 列表页新建 SOP → 打开画布(左侧节点面板拖start → single_agent → tool(kb_search) → branch → end,右侧面板配 agent/input_mapping/CEL 边条件,底栏配 Globals)→ 保存草稿(刷新可回读)→ 发布;调试走 Debug Panel(单步 + run-e2e 端到端时间轴)。本节 curl 路径保留为 API/自动化基线。运维健康检查:bash scripts/verify-testbed.sh --frontend-health。
bash
# 2.1 项目(scope builder:project:write)
curl -sk -X POST "$BASE/api/v1/builder/projects" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"name":"hello-agent","slug":"hello-agent"}'
# 2.2 Agent(scope agent:create)
curl -sk -X POST "$BASE/api/v1/builder/agents" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"project_id":"<project-uuid>","name":"hello","system_prompt":"You are a helpful assistant."}'
# 2.3 SOP + 版本(scope sop:create):最小 start → agent → end 工作流
curl -sk -X POST "$BASE/api/v1/builder/sops" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"project_id":"<project-uuid>","slug":"hello-sop","name":"Hello SOP"}'
curl -sk -X POST "$BASE/api/v1/builder/sop-versions" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{
"sop_id": "<sop-uuid>",
"workflow_spec": {
"spec_version": "1.0",
"entry_node_id": "start",
"nodes": [
{"node_id": "start", "node_type": "system", "executor_type": "SystemNodeExecutor", "system_action": "start"},
{"node_id": "agent_node", "node_type": "single_agent", "executor_type": "AgentNodeExecutor",
"agent_id": "<agent-uuid>", "prompt": "Answer briefly.", "input_mapping": {"q": "topic"}},
{"node_id": "end", "node_type": "system", "executor_type": "SystemNodeExecutor", "system_action": "end"}
],
"edges": [{"from": "start", "to": "agent_node"}, {"from": "agent_node", "to": "end"}]
}
}'
# 2.4 发布(scope sop:publish)
curl -sk -X POST "$BASE/api/v1/builder/sops/<sop-uuid>/publish" -H "Authorization: Bearer $TOKEN"条件分支:
edges[]支持 CEL 条件(如ctx.score > 80),由 LangGraph 条件路由求值(H-A05 修复后 节点输出会合并进context供 CEL 读取)。
3. 提交任务(免计费默认:estimated_points=0)
bash
SUBMIT=$(curl -sk -X POST "$BASE/api/v1/app/workflow-tasks" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
"sop_version_id": "<sop-version-uuid>",
"project_id": "<project-uuid>",
"app_id": "hello-agent",
"channel": "api",
"input_data": {},
"context_variables": {"topic": "Say hello to AgentHub"},
"idempotency_key": "hello-'$(date +%s)'",
"estimated_points": 0
}')
TASK_ID=$(echo "$SUBMIT" | python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["task_id"])')- 需要 scope
task:submit(缺失 → 403AUTH_SCOPE_INSUFFICIENT,实测); estimated_points=0= 免计费运行(不冻结积分;见billing-free-run-mode.md);idempotency_key相同的重复提交幂等返回同一 task。
4. SSE 实时进度(一次性 ticket,浏览器 EventSource 友好)
bash
# 4.1 mint 30s 一次性 ticket(scope task:read;避免长效 JWT 进 URL)
TICKET=$(curl -sk -X POST "$BASE/api/v1/app/workflow-tasks/$TASK_ID/events/ticket" \
-H "Authorization: Bearer $TOKEN" | python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["ticket"])')
# 4.2 打开事件流(终态事件后自动关闭;支持 Last-Event-ID 断点续传)
curl -skN "$BASE/api/v1/app/workflow-tasks/$TASK_ID/events?ticket=$TICKET"实测事件序列(完整链):
text
event: workflow_task.submitted
event: workflow_task.started
event: node.started {"node_id": "start"}
event: node.completed {"node_id": "start", "status": "succeeded"}
event: node.started {"node_id": "agent_node"}
event: node.completed {"node_id": "agent_node", "status": "succeeded", "duration_ms": 5593}
event: node.started {"node_id": "end"}
event: node.completed {"node_id": "end", "status": "succeeded"}
event: workflow_task.succeeded5. 查结果与用量
bash
# 终态(completed);output_data 为驱动器终态标记,节点级输出经 context/检查点承载
curl -sk "$BASE/api/v1/app/workflow-tasks/$TASK_ID" -H "Authorization: Bearer $TOKEN"
# 积分余额(scope billing:points:read)
curl -sk "$BASE/api/v1/billing/points/balance" -H "Authorization: Bearer $TOKEN"模型用量:每次 LLM 调用落一条 cost_records(call_kind='llm'、真实 tokens、免计费期 cost_cents=0)。 实测样例:llm | tokens_in=275 | tokens_out=377 | cost_cents=0 | model=mimo-v2.5。
5.5 计费运行与终端用户归属(T2 起 · ADR-0044)
§3 的
estimated_points=0免计费默认仍然保留(试跑/联调场景);本节是接入积分闭环后的正式口径。
5.5.1 影子用户 + 积分入账(tenant scope)
你的终端用户 100% 在你自己的 SaaS 侧注册/登录(模式 A);在 agent_hub 只需要一个影子用户:
bash
# 1) 影子用户(username = 你侧不可变用户 ID;重复调用幂等返回既有用户)
curl -sk -X POST "$BASE/api/v1/identity/users:provision" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"username": "saas-user-10086"}'
# → data.id 即 shadow_user_id
# 2) 给影子用户入账(scope billing:points:tenant_grant;operation_id 幂等,两侧对账键)
curl -sk -X POST "$BASE/api/v1/billing/points:tenant-grant" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
"user_id": "<shadow_user_id>",
"project_id": "<project-uuid>",
"amount": 1000,
"operation_id": "grant:order-20260711-0001",
"expires_at": "2027-07-11T00:00:00Z",
"reason": "用户充值订单 20260711-0001"
}'- 目标 user 无长期账户时自动开户;重复
operation_id返回idempotent_replay: true(安全重试); - 跨租户的 user_id →
403 AUTH_TENANT_MISMATCH(fail-closed); - 平台→租户的商务结算入账由 ops 走
POST /billing/points:grant(ops scope,web-ops 积分发放页)。
5.5.2 API-key + on-behalf-of 提交(计费任务归属终端用户)
bash
# 1) 领 API-key(scope delivery:api_key:write;明文仅本响应返回一次,妥善保存)
curl -sk -X POST "$BASE/api/v1/delivery/api-keys" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name": "saas-prod"}'
# → data.api_key = "ahk_..."(轮转 :rotate / 吊销 :revoke)
# 2) headless 提交任务并归属到影子用户(binding billing_rules 配置 estimated_points>0 即计费)
curl -sk -X POST "$BASE/api/v1/app/<app_id>/tasks" \
-H "X-AgentHub-Api-Key: ahk_..." \
-H "X-AgentHub-On-Behalf-Of: <shadow_user_id>" \
-H 'Content-Type: application/json' \
-d '{"workflow_key": "chat", "input": {"message": "hi"}}'X-AgentHub-On-Behalf-Of的 user 必须属于 API-key 解析出的租户且 active,否则403(不降级);- 任务 → 冻结 → 结算 → 流水全链以该影子用户
user_id归属;不带头时维持项目 owner 兜底(向后兼容); - 流水核对:
GET /api/v1/billing/points/ledger?user_id=<shadow_user_id>(scopebilling:ledger:read)—— 完整链可见manual_adjust(入账)/freeze/deduct/unfreeze。
6. 注册 HTTP Tool(ADR-0042 反向接缝 · 过渡形态)
工具目录 CRUD(PT-A06)尚未实装;过渡期用 TOOL_REGISTRY env JSON(operator 配置,非租户自助):
jsonc
// api/worker 容器环境变量 TOOL_REGISTRY(single-line JSON)
{
"<tool-uuid>": {
"url": "https://tools.example.com/ocr", // 仅 https
"method": "POST",
"timeout_s": 10,
"max_bytes": 1048576,
"allow_hosts": ["tools.example.com"], // SSRF 白名单(必填;DNS 解析后还会做私网 IP 校验 + IP pin)
"auth_header": "Authorization",
"auth_scheme": "Bearer" // 凭据经 OpenBao CredentialsStore 解析,不写进 JSON
}
}SOP 里以 {"node_id":"my_tool","node_type":"tool","executor_type":"ToolNodeExecutor","tool_id":"<tool-uuid>"} 引用;内置 kb_search 工具的 sentinel UUID 为 00000000-0000-0000-0001-000000000001。 未知 tool_id / 调用失败一律 fail-closed(任务 FAILED,积分解冻)。
7. Webhook 验签(任务结果回推)
配置归属(重要):Webhook 回推是平台运维配置,不是租户自助——Builder / 交付页没有 (按当前设计也不应有)回调地址或签名密钥的配置入口。需要接收回推时,把 HTTPS 回调地址 提供给平台方,由 ops 在
WEBHOOK_ENDPOINTS登记 + 经密钥库发放验签密钥(经安全通道交付), 步骤见tenant-onboarding-runbook.md§3.7。未配置的 租户平台侧静默跳过回推,任务事件消费不受影响(仍走 SSE / 轮询)。租户级自助配置是 ADR-0042 §5 演进方向,暂未实装。
平台外呼 webhook 携带 X-AgentHub-Signature(HMAC-SHA256 hex)等头;验签算法(app/delivery/headless/webhook_signer.py):
text
material = f"{event_id}.{ts}.{nonce}.{sha256_hex(body)}"
signature == HMAC_SHA256(secret, material) # 常数时间比较
abs(now - ts) > 300 → 重放拒绝(400 WEBHOOK_REPLAY_DETECTED)TS/Python 参考实现见 @agent-hub/sdk 与 sdk/python/agenthub_sdk(GL-06 薄 SDK)。
8. 排障速查
| 症状 | 原因 | 修复 |
|---|---|---|
401 AUTH_UNAUTHENTICATED | token 直连 :8080 取的(iss 不匹配)/ HS256 伪造 / 过期 | 经网关取 token;只用 RS256 |
403 AUTH_SCOPE_INSUFFICIENT | service account permissions 缺对应 scope | ops 补属性后重取 token |
任务 retrying→failed,failure_reason=agent_unavailable:model_credential | 模型 key 未 provisioning(fail-closed 预期) | ops 按 onboarding §3 写 OpenBao key |
| 收不到任务终态 webhook | 回调地址未登记(WEBHOOK_ENDPOINTS)或验签密钥未发放——平台侧静默跳过 | 联系平台 ops 配置(onboarding §3.7);未配置不影响 SSE / 轮询消费 |
| SSE 404 | ticket 过期(30s)/ 已消费 / task 不属于你 | 重新 mint |
9. Starter SaaS 验证台用法(T6)
定位:
web-starter是基座端到端验证台 / 终端渠道参考实现,不是官方二开集成面(官方通道仍是 Headless API,见 ADR-0041/0042)。
- 浏览器打开 Starter → 顶栏 App 下拉(数据源
GET /api/v1/app/manifests)选择已发布 App;VITE_APP_ID/VITE_PROJECT_ID仅作默认值。 - 「发起任务」页:多绑定时选择
feature_key;提交前展示目录摘要中的estimated_points预扣。 - 「积分与会员」页:余额 +
GET /billing/points/ledger流水(freeze / deduct / unfreeze / manual_adjust 徽章)。 - 余额不足时提交返回 HTTP 402
POINTS_INSUFFICIENT,UI 提示「积分不足,请联系运营充值」。 - 真栈验收脚本:
bash scripts/t6-starter-verification.sh --verify(依赖 P1 计费点火与deploy-from-cloud-agent.sh --with-celery)。
10. 会话式接入(CV 波次 · 会话 Agent + Headless 多轮对话)
定位:任务型 SOP 之外的第二条通道——会话 Agent(
agent_type=conversation),跑 AgentScope 2.0 运行时,按轮计费(operation_id=turn:*,与 SOP 的task:*分账)。完整设计见 CV 系列 program(内部过程文档,不对外分发)。
10.1 建模:发布一个会话 Agent
会话 Agent 复用 agents 目录(agent_type=conversation 判别)与发布快照机制,发布时必须带 conversation 配置块(单价与护栏强制配对,CV3-1 发布校验):
bash
# 注册会话 Agent(category 任意;agent_type=conversation)
curl -sk -X POST "$BASE/api/v1/builder/agents" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"project_id":"'$PROJECT_ID'","code":"chat-hello","name":"会话助手","agent_type":"conversation"}'
# 发布:role_card + bindings(KB/Skill/工具)+ conversation 块(单价+护栏+SOP 白名单+卡片+可选 content_guardrail)
curl -sk -X POST "$BASE/api/v1/builder/agents/$AGENT_ID/publish" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"agent_id":"'$AGENT_ID'","expected_agent_version":0,
"role_card":{"persona_prompt":"你是耐心的客服助手。"},
"bindings":{"primary_model_group_id":"'$MODEL_GROUP_ID'","knowledge_base_ids":[],"skill_ids":[],"tool_ids":[]},
"conversation":{
"points_per_turn":10,
"guardrails":{"max_input_tokens":8000,"max_attachments":5,"max_tool_calls":10,"max_sop_invocations":2,"turn_timeout_seconds":120},
"content_guardrail":{"mode":"shadow"},
"space_permissions":{"user_space":"read_write","project_space":"read"},
"sop_allowlist":[],"sop_budget_per_conversation":500,
"card_types_enabled":["choice_card"]
}
}'- SOP 节点绑会话 Agent、或会话通道收到非
conversationAgent → 发布/开会话409(program §三 四条校验规则); - 单价缺护栏(或反之)→ 发布
409 AGENT_PUBLISH_VALIDATION_FAILED; content_guardrail可选(ADR-0064,CG1/CG2 已落地):off(默认,与缺席同义,平台级默认由CONVERSATION_CONTENT_GUARDRAIL_DEFAULT_MODE给)|shadow(影子观察——本地 PII 校验位 + 三级词表扫描模型可见文本与模型输出,模糊命中升级 LLM 裁决〔平台侧归账、fail-open〕,全程只审计不拦截,命中落 WORM 审计content类目不落 PII 明文);enforce归 CG3,当前非法。
10.2 chat-dry-run 预览(发布前冒烟,零计费/零持久化)
bash
curl -sk -X POST "$BASE/api/v1/builder/agents/$AGENT_ID/chat-dry-run" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"message":"你能做什么?"}'
# → data.assistant_text + 解析后的配置摘要(points_per_turn / guardrails / kb_count / tool_count / sop_allowlist)10.3 开会话 + 多轮对话(Headless,API-key + On-Behalf-Of)
会话通道复用 T2 的 API-key + 影子用户委托模型(§5.5)。开会话与轮次端点在 /api/v1/app/conversations:
bash
# 开会话(幂等键 client_conversation_key 可选)
curl -sk -X POST "$BASE/api/v1/app/conversations" \
-H "X-AgentHub-Api-Key: $API_KEY" -H "X-AgentHub-On-Behalf-Of: $SHADOW_USER_ID" \
-H 'Content-Type: application/json' -d '{"agent_id":"'$AGENT_ID'"}'
# → 201 data.id = CONVERSATION_ID
# 一轮对话(SSE:turn_started → turn_delta* → [turn_card] → turn_completed)
curl -sk -N -X POST "$BASE/api/v1/app/conversations/$CONVERSATION_ID/turns" \
-H "X-AgentHub-Api-Key: $API_KEY" -H "X-AgentHub-On-Behalf-Of: $SHADOW_USER_ID" \
-H 'Content-Type: application/json' \
-d '{"content_blocks":[{"type":"text","text":"帮我查退款政策"}]}'输入内容块支持 text | file | page_ref | activity(后三类第三方内容以 <data> 标签包裹注入防御,CV4-3)。选项卡片下发后,下一轮回传 {"type":"choice_selection","card_message_id":"...","selected":["<option_id>"]}(CV2-4)。
10.4 计费 / 余额 / 断连重拉
- 轮初
freeze(operation_id=turn:{turn_id}, billing_model=upfront-once)→ 轮末settle;失败unfreeze全额退(CV3-2); - 余额不足开轮 →
402 POINTS_INSUFFICIENT(零副作用);结清后可用余额 < 3×单价 →turn_completed附balance_warning:true(ADR-0050 D4); - SSE 断连凭首帧
turn_id走GET /api/v1/app/conversations/{id}/turns/{turn_id}重拉,不重跑、不重复扣费(CV1-2/CV3-2); - token 计量入
cost_records(task_id=turn_id);md 编制的 LLM 成本归平台(task_id空,ADR-0050 D5)。
10.5 SOP 三件套 / 工具 / MCP(会话内调用)
会话 Agent 经工具白名单调用已发布 SOP(sop_submit/sop_status/sop_result,白名单 + 单会话预算闸,CV2-2);HTTP 工具与 MCP 服务器经目录(/api/v1/builder/tools,CV2-3/CV5-1)注册,MCP 保存与运行时双重 allowlist + SSRF + 每租户配额 + 审计(ADR-0051 D3)。越权(未白名单 SOP / 只读空间写 / 未 allowlist MCP)在执行器 fail-closed 拒绝并入 WORM 审计。
10.6 结束会话与 md 资产
POST /api/v1/app/conversations/{id}:end(或空闲超时)→ closing → 编制链把转写/画像/笔记/记忆写入用户空间(版本+1,CONVERSATION_MD_COMPILATION_ENABLED=true 时启用真实 LLM 编制,CV4-2)→ closed 只读。用户空间在影子用户 provision 时幂等初始化、注销时级联关闭(CV4-1)。
10.7 验收链
mock 段(本地/CI):backend/tests/integration/runtime/conversation/test_cv5_3_acceptance_chain.py 跑通「开会话→5 轮→freeze/settle→402→断连重拉→closed 拒新轮」的计费/生命周期骨架;KB/附件/卡片/SOP 三件套/MCP 各段由对应 CV 集成套件覆盖。
真栈段(ECS-B,program §十一):
bash
bash scripts/deploy-from-cloud-agent.sh --with-celery # worker/beat 负责 :end 闭合与 idle sweep
bash scripts/verify-testbed.sh --worker-health
bash scripts/smoke-conversation-testbed.sh # S1~S10;固定 UUID 租户,结束即删冒烟种子强制 agent_type=conversation(否则开会话 409)。运维细节与踩坑见 docs/ops/testbed-runbook.md §10。
10.8 多模态接入(MM 波次 · 语音与附件,MM5-1 真栈实证)
凭据布局(实证):语音/文生图走经典版 DashScope(
dashscope.aliyuncs.com,按量 key,OpenBao 租户 vault ref);chat 走平台 fallback(MODEL_FALLBACK_CHAT_*)。Token Plan 网关(token-plan.*.maas.aliyuncs.com/compatible-mode/v1)仅服务 chat——非 chat 端点一律url error,订阅型sk-sp-key 在经典原生树InvalidApiKey,两把 key 不可混用。
模型选型(2026-08-19 ECS-B 实测可用;STT 形状 2026-08-23 修正):
| 模态 | model_type | 模型 | 备注 |
|---|---|---|---|
| STT | stt | qwen3-asr-flash | 同步端点;input.messages[].content[].audio 收 Data URI(#296 修正:原 file_urls+prompt 形状实为纯文本调用,模型逐字复述 prompt) |
| TTS | tts | cosyvoice-v3-flash | voice 如 longanyang(longwan_v2 被 engine 418 拒,#293);24h OSS URL 二次下载 |
| T2I | image_gen | qwen-image-3.0-pro | multimodal 形状;OSS 预签名 URL 二次下载 |
| 视觉理解 | llm | qwen3-vl-plus / qwen-vl-max | capabilities 勾 vision;放 chat 路由 fallback 位——带图轮次独占 vision 端点,纯文本轮次走主模型(MM4-1,2026-08-23 真栈实证) |
| chat | llm | deepseek-v4-pro 等 | compatible-mode |
租户在 web-builder「资源建模 → Model Registry」注册上述模型(vendor 选 dashscope;key 只入 OpenBao 引用),并在 Model Routes 为每个 model_type 设主模型(unit_price/price_unit:second/char/image)。
三模态 curl(接 §10.3 会话之后;snapshot conversation 块需带 points_per_transcription/points_per_speech/voice,guardrails 需带 max_images_per_turn):
bash
# ① STT:录音 multipart 直传 → 文本回填(用户可改)→ 作为普通 text 轮次发送
curl -sk -X POST "$BASE/api/v1/app/conversations/$CONVERSATION_ID/transcriptions" \
-H "X-AgentHub-Api-Key: $API_KEY" -H "X-AgentHub-On-Behalf-Of: $SHADOW_USER_ID" \
-F "audio=@speech.mp3;type=audio/mpeg"
# → data.text / points_charged(快照 speech 组价)
# ② TTS:助手消息按需合成;(message_id, voice) 缓存,重放零扣点
curl -sk -X POST "$BASE/api/v1/app/conversations/$CONVERSATION_ID/messages/$MESSAGE_ID/speech" \
-H "X-AgentHub-Api-Key: $API_KEY" -H "X-AgentHub-On-Behalf-Of: $SHADOW_USER_ID"
# → data.url(签名下载)/ cached / points_charged
# ③ 附件前门:authorize → 直传 PUT → commit(服务端抽取摘录)
curl -sk -X POST "$BASE/api/v1/app/conversations/$CONVERSATION_ID/attachments:authorize" \
-H "X-AgentHub-Api-Key: $API_KEY" -H "X-AgentHub-On-Behalf-Of: $SHADOW_USER_ID" \
-H 'Content-Type: application/json' \
-d '{"filename":"notes.txt","mime":"text/plain","size_bytes":123}'
# → data.fid + data.upload_url;PUT 字节后 :commit 带 sha256 → data.excerpt
# 发轮时 content_blocks 携带 {"type":"file","file_id":"...","filename":"...","excerpt":"..."}
# ④ 文生图:轮内触发内置工具 generate_image → SSE 追加 turn_file 帧
# → GET /api/v1/app/conversations/{id}/files/{file_id} 取签名 URL 下载 PNG计费维度:stt:(秒,向上取整)/tts:(字符)/image_gen(张);全部经 ModelUsageEvent→cost_records 归因,配对护栏(价↔上限 XOR 拒绝)沿用 ADR-0055 D4。图片输入理解(vision content parts)MM4-1 已落地(ADR-0056):chat 路由含 vision 能力端点时图片以原生 image_url parts 随轮(7MB/图守门),无 vision 端点时降级 vision_deferred 占位文本(非 fail-closed,2026-08-23 真栈实证含 fid 读取修复 #297)。
真栈验收:bash scripts/smoke-conversation-testbed.sh(S1~S14;脚本前置把经典 key 双路径写入冒烟租户 vault,密钥不落盘不回显)。
10.8.1 租户自助配置路径(MM-FIX-01 · 注册≠路由≠计价三断点收口)
背景(issue #353):「注册了模型」不等于「路由可达」也不等于「可计费」——三个断点过去只能在故障现场暴露。MM-FIX-01 后,配置错误在发布前被拦截或警告、运行时失败有可操作中文提示、路由语义在 UI 可见。
自助配置清单(Builder → 资源建模 → Models 页签,按序做齐即可发布多模态 Agent):
- 注册即计价:注册 tts/stt/image_gen/video_gen 模型时,表单会随类型出现「按次单价(unit_price)+ 计价单位」字段(默认 tts→char / stt→second / image_gen→image / video_gen→frame)。不填计价的模态模型在首次真实使用时会以
MODEL_USAGE_METADATA_MISSING拒绝计费——Registered Models 列表的「按次计价」列对未计价模态显示红色「未计价」标签。 - 注册后建路由:注册模态模型成功后,若该类型还没有生效路由,页面顶部出现引导条「注册模型不会自动建路由」——点「一键填入 Set Route 表单」预填组与 primary(注意同组唯一约束
uk_model_routes_tenant_group:所选组已有其他类型路由时需先建独立分组),确认保存。没有任何平台级模态兜底——不建路由,该模态发布即被拦。video_gen除外:该类型当前无能力映射(见第 5 条),注册后不出现引导条。 - 发布 preflight:发布启用语音/图片的会话 Agent 时,后端复用与运行时同一策略查询做可达性检查——模态无 active 路由或生效模型未计价 →
409 AGENT_PUBLISH_PREFLIGHT_FAILED,data.issues[]逐条给出中文修复指引(前端发布表单直接渲染);启用附件(max_attachments>0)但 chat 生效路由无vision能力端点 → 发布仍成功,但响应data.warnings[]黄条提示「图片输入将降级为占位文本」。存量快照零阻断:未启用语音/图片者发布路径零变化;已启用附件但 chat 模型无 vision 标的存量 Agent,重发布时会新增这条非阻断黄条(此前被遮蔽的既有事实,现在如实提示)。 - 存量修复通道:Registered Models 行内「编辑」模态可改四字段(按次单价/计价单位/能力标记/状态,
PATCH /api/v1/builder/models/{id};名称/分组/类型不可改——类型错了删除重注册);跨租户 404 反枚举、未知字段 422;计价字段成对——只填/只清单价会被 422 拒绝(前端表单同步成对校验),清除计价 = 两字段一起清空。 - 生效路由透明化:Routes 区顶部「当前生效路由」卡片按 model_type 展示运行时真正会选的路由与计价(
GET /api/v1/builder/routes:effective)。选择规则:同类型中「创建最早且主模型 active」的路由生效;其余标注不生效原因(晚于生效路由创建 / 主模型非 active);video_gen标注「无能力映射,路由恒不生效」;胜出路由的 vendor 网关无法解析时标注「运行时不可用:vendor 网关无法解析」(经与运行时同一策略源判定——模态调用会 503,chat/embed 会静默走平台兜底)。删除再重建路由会重置创建时间,可能翻转 chat 的生效模型——删除确认弹窗有此提示。 - 模型组绑定声明(A5 方向二):发布表单的模型组选择区已注明「模型组绑定暂不参与运行时路由——实际模型由租户级 Model Routes 决定」。这是当前的真实语义(issue #353 owner 裁定):per-Agent 独立选模型是登记中的后续能力(MR5-1),落地前请以 Model Routes 为准配置路由。
- 运行时失败的可操作提示:
MODEL_ROUTER_UNAVAILABLE(无路由)与MODEL_USAGE_METADATA_MISSING(未计价)已入前端错误码中文映射——试用抽屉/用户侧的报错直接给出「去哪、做什么」的指引,而非裸错误码;计量守卫已在上游调用之前触发,未计价不再产生白付的上游成本。
E2E 全链(mock 模式):e2e/multimodal-selfservice.spec.ts(注册带价模型 → 引导建路由 → 发布先拦后放 + vision 降级警告 → 未计价录音失败中文提示)。
11. 界面化配置与用户试用(CU 波次 · web-builder / web-starter)
定位:§10 的 curl 链路在 CU 波次已全部接到界面上——建设者在
web-builder完成会话 Agent 的建/改/发布/试跑,终端用户在web-starter的「对话陪练」里直接对话。端点契约不变(本章只引用,不重复),变的是操作面。方案见 CU 波次 program(内部过程文档,不对外分发)。
11.1 建设者:在 web-builder 界面管理会话 Agent
入口:登录 web-builder → 项目列表 → 进入项目 → 资源建模 → Agents 页签(/projects/{projectId}/resources)。
- 新建:点
Create Agent→ 填code/name→ 类型选「会话 Agent(多轮)」(即 §10.1 的agent_type=conversation)。列表「类型」列显示紫色「会话 Agent」标签。 - 发布:行内
Details / Publish打开抽屉 → 填人设(persona_prompt)、选主模型组 → 会话配置块:轮次单价(如 10 分/轮)、五项护栏、空闲超时秒数、内容护栏三态开关(平台默认 / off / 影子观察——对应 §10.1 的content_guardrail.mode,ADR-0064;影子期只审计不拦截),按需勾选「启用 choice_card」;工具多选(tool_ids)来自工具目录(ADR-0051,面板见 CV5-1/CU 波次),选中后随发布冻结进版本快照——发布成功横幅里的快照 JSON 可见bindings.toolIds非空。漏填单价/护栏任一 → 前端内联红框拦截(与 §10.1 的409 AGENT_PUBLISH_VALIDATION_FAILED同语义);故意漏填护栏提交会被拦下并给出可读 409 文案。 - 改:行内「编辑」可改名称/描述,保存后列表即刻反映;已发布 Agent 的类型字段置灰并提示「类型不可修改」(ADR-0053 D4,对应
PATCH /api/v1/builder/agents/{id}改agent_type→409 AGENT_TYPE_IMMUTABLE)。 - 归档 / 删:行内「归档 / 删除」弹窗默认归档(软删,ADR-0053:归档后立即从用户侧目录消失、不可开新会话
409 CONVERSATION_AGENT_ARCHIVED、既有会话仍可读);勾选「彻底删除」走硬删守卫——有已发布版本/会话/团队引用 →409 AGENT_DELETE_CONFLICT且弹窗内列出中文冲突原因;从未发布的空壳 →204真删。
11.2 两级试跑:预览(免费)vs 试用(计费)
会话 Agent 的详情抽屉比 SOP Agent 多两个页签,语义强区分,别混用:
| 预览(免费) | 试用(真实会话) | |
|---|---|---|
| 端点 | POST …/chat-dry-run(§10.2) | /api/v1/app/conversations*(§10.3,平台 JWT 通道,ADR-0052) |
| 计费 | 零计费、零落库、无工具(横幅明示) | 按轮真实扣减积分,单价前置红色警示 + Popconfirm 二次确认 |
| 用途 | 发布前后冒烟:发一句话拿回显 + 配置读数(单价/五护栏/KB 数/工具数/SOP 白名单/卡片类型) | 全链体验:≥3 轮流式(逐字流出)、choice_card 卡片往返、每轮扣费回显与累计、余额预警条、历史分页、断连重拉(不重跑不重复扣费)、结束会话 |
试用页签的每轮扣费与 points_ledger 的 turn:* 流水对得上(§10.4);余额不足开轮 → 402 POINTS_INSUFFICIENT 零副作用。
11.3 终端用户:web-starter「对话陪练」
入口:saas_user 登录 web-starter → 侧边栏「对话陪练」(/chat)→ 目录选 Agent「开始对话」→ /chat/{conversationId} 多轮对话。行为与 §10.3/§10.4 的契约一一对应:开会话幂等(重复打开/刷新得到同一 conversation id)、回复逐字流出、卡片选择回传 choice_selection 块、扣费行逐轮回显、断连提示「连接中断,已重拉完整结果」且不重复扣费、余额耗尽 → 402 + 「前往充值」引导、「结束会话」后输入区禁用。「我的会话」列表为本浏览器本地记忆(封顶 20 条),回到会话后从服务端拉状态与历史。
11.4 用户侧目录的字段收敛边界
终端用户目录的数据源是 GET /api/v1/app/agents?agent_type=conversation(CU3-2),响应恰好五个字段:
json
{"id":"…","name":"英语陪练小助手","description":"…","points_per_turn":10,"idle_timeout_seconds":900}- 看不到
sop_nodeAgent、未发布、已归档的 Agent(服务端过滤,fail-closed:快照缺会话块/缺价格键宁可少列不可错价); - 看不到人设、模型组、KB/Skill/工具绑定、护栏、SOP 白名单——这些是会话 Agent 的「配方」,属建设者侧资产;响应体对 24 个敏感键(
persona_prompt/bindings/tool_ids/guardrails/sop_allowlist/config_snapshot_jsonb等)做递归零命中断言(CU3-2 AC5 探针)。终端用户只需要知道「它是谁、每轮多少钱、多久不聊会自动结束」,其余一概不下发; - 该端点仅平台 JWT 通道(
conversation:readscope):API-Key 直连 → 401,缺 scope → 403,跨租户 Agent 永不出现(CU3-2 AC3/AC4)。
11.5 验收链与演示
§十 13 条端到端验收链的逐条证据(mock 段 Playwright/vitest/契约守卫 + 真栈段状态)收口在 CU7-1 任务包验证报告(内部过程文档,不对外分发);两条界面演示录屏(web-builder 试用全链 / web-starter 对话陪练全链)在同目录 demo/ 下。
12. 内容专栏(Blog 公共阅读面,ADR-0065)
会话之外的第二类租户能力:发布即静态的内容专栏。markdown 在发布时渲染成整页 HTML(SSG——AI 爬虫不执行 JS,这是「阅读面退出 SPA」的架构决策),匿名可读、SEO/GEO 原语自动生成。管理面在 Builder 控制台「内容」区(专栏/文章/SEO 字段/签名预览链接),本节给 curl 级全流程。
12.1 建专栏与文章 → 发布 → 公共 URL
bash
TOKEN=… # tenant_admin(content:column:write / content:post:write)
# 专栏
curl -X POST "$BASE/api/v1/content/columns" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"slug":"basics","name":"基础专栏"}'
# 文章(创建即 draft;slug 小写-数字-连字符)
curl -X POST "$BASE/api/v1/content/posts" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{
"column_id":"<上一步 id>","slug":"hello-world","title":"Hello World",
"summary":"首篇","body_markdown":"# 你好\n\n正文","pen_name":"运营君"}'
# 立即发布(发布时渲染整页落库;publish_at 未来时刻 = 定时发布,beat 到点自动上)
curl -X POST "$BASE/api/v1/content/posts/<post_id>/publish" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}'公共 URL(无需任何凭据,浏览器与爬虫同权):
- 品牌域:
https://<brand>/blog/hello-world/;基座域过渡形态:https://bhsl.cc/<app>/blog/hello-world/ - SEO 面:
/blog/{site}/sitemap.xml、/rss.xml、/llms.txt、/robots.txt(平台默认放行 GPTBot/ClaudeBot/PerplexityBot/Google-Extended/CCBot,租户可在 settings 按 UA 覆盖);基座域根/robots.txt聚合各站点 Sitemap 行 - 语义:改 slug 自动留 301;缺失 slug 真 404(不冒充 SPA);文章页 ETag 协商 304;草稿用
GET /api/v1/content/posts/{post_id}/preview-url拿短时效签名链接(/blog/{site}/{slug}/preview?token=…,默认 10 分钟)
12.2 设置对外域(品牌接入必做)
PATCH /api/v1/content/settings 设 public_base_url = https://<你的品牌域>——canonical/og/sitemap 的唯一基址;不设则回退请求 Host(品牌域形态恰好正确,基座域路径形态会得到品牌形态的 canonical,应显式设置)。收录与 AI 爬虫验证流程见 blog 收录验证 runbook。
开放状态:已验收(CB5,2026-09-12 测试栈 + SGP 生产段双段 13/13)——全租户开闸前勿在产品文案承诺「已开放」(platform-status §2)。
13. 站内信与消息中心(IM 波次,ADR-0066)
平台/运维 → 终端用户的单向通知通道:公告(system_notice)与申诉结果(appeal_result,CG3 联动自动生成)。用户端三 SPA 已带消息中心(铃铛红点 + 抽屉 + 已读/删除),实时性走用户级 SSE——与余额流同一通道范式,不新增连接管理。
13.1 平台侧:发一条公告(ops)
bash
OPS_TOKEN=… # 平台管理员(inbox:manage)
# 指定租户定向发送(Idempotency-Key 重放安全:同 key+同内容 → 同 batch、零新增行)
curl -X POST "$BASE/api/v1/inbox/broadcasts" -H "Authorization: Bearer $OPS_TOKEN" \
-H 'Content-Type: application/json' -H 'Idempotency-Key: announce-2026-09-11-001' \
-d '{"tenant_ids":["<tenant-uuid>"],"subject":"维护通知",
"body":"今晚 02:00-03:00 例行维护","payload":{"broadcast_title":"维护"}}'
# 或全平台(all_tenants:suspended 租户自动跳过)
# {"all_tenants":true, ...} —— 二选一,互斥- 配额:每租户每日 10 批 / 总量 10000 条(
INBOX_BROADCAST_DAILY_QUOTA/INBOX_MESSAGE_TOTAL_QUOTA),超限整体 422INBOX_QUOTA_EXCEEDED、零半批落库 payload白名单:cta_url/cta_label/broadcast_title;正文不进审计(WORM 只记 batch 形状)- 界面入口:ops 控制台 Broadcasts 页(模式/租户标签/结果回显)
13.2 用户侧:收件箱 API + SSE
bash
TOKEN=… # 普通用户(inbox:read,仅本人收件箱)
# 未读汇总(铃铛红点;三 kind 键恒返回)
curl "$BASE/api/v1/inbox/summary" -H "Authorization: Bearer $TOKEN"
# 列表(keyset 分页,最新在前;?unread_only=true / ?kind=appeal_result 过滤)
curl "$BASE/api/v1/inbox/messages?limit=50" -H "Authorization: Bearer $TOKEN"
# 标记已读(幂等,重复标记零变化)/ 软删(幂等,删后 404)
curl -X POST "$BASE/api/v1/inbox/messages/<msg_id>/read" -H "Authorization: Bearer $TOKEN"
curl -X DELETE "$BASE/api/v1/inbox/messages/<msg_id>" -H "Authorization: Bearer $TOKEN"
# 实时订阅(SSE;首帧 inbox.snapshot 全量补位,随后 inbox.message_created 实时帧,30s ping)
curl -N "$BASE/api/v1/events/inbox" -H "Authorization: Bearer $TOKEN"- 重连语义:每次连接先收快照——断线期间的消息由快照补位,无需 offset 持久化
appeal_result消息由 CG3 申诉裁决事件自动生成(payload 携appeal_case_id/verdict/cta_url),不可手工发送
范围口径:本期 = 平台→用户单向通知;用户间私信(IM5)在延后池(触发 = 真实需求 + owner 拍板,ADR-0066 D6)。真栈验收(IM4)已收官——详见 platform-status §2。
14. 品牌资产自助(BA 波次,ADR-0067 / ADR-0068)
品牌资料 = 每个 Keycloak realm 一行(brand_profiles,per-realm 锚定:toC 一品牌一租户、toB 一套品牌授权多租户通吃)。七列资产(display_name / slogan / intro / 明暗 logo / 明暗条幅)全可空——未设置的字段由渲染回落链解析为平台默认,可只配一半(半配对合法)。运营租户在 builder「品牌设置」页自助维护;同 realm 其他租户(toB 客户租户)只读可见、写被拒。
14.1 ops 侧:建租户时勾选「设为品牌运营租户」(造册)
bash
OPS_TOKEN=… # 平台管理员(ops:tenants:write)
# realm 下拉的数据源 = 只读注册表(GET /api/v1/ops/realms, ops:realms:read)
curl "$BASE/api/v1/ops/realms" -H "Authorization: Bearer $OPS_TOKEN"
# 建租户 + 勾选 → profile 行随同一事务诞生(INSERT-or-409:已被占位/平台托管 → 409 中文提示,不改派)
curl -X POST "$BASE/api/v1/ops/tenants" -H "Authorization: Bearer $OPS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"口语搭子运营域","slug":"oral-buddy","realm_id":"<realm>",
"set_as_brand_operator":true}'
# → 201 data.id / data.brand_operator_assigned=true- realm 绑定建后不可改(PATCH 携 realm 字段 → 422);重复 slug → 409
TENANT_SLUG_CONFLICT;非法/未注册 realm → 422TENANT_REALM_INVALID - 界面入口:ops 控制台 Tenants 页建租户表单(realm 下拉 + 勾选框);
/realms页可见每 realm 的绑定租户数与品牌资料完备度
14.2 运营租户侧:录品牌资产(通用文件前门 + PATCH)
bash
BRAND_TOKEN=… # 运营租户管理员(brand:profile:read/write)
# ① 通用前门三步上传(purpose=brand-asset;png/jpeg/webp,禁 svg,≤2MB)
curl -X POST "$BASE/api/v1/files:authorize" -H "Authorization: Bearer $BRAND_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"purpose":"brand-asset","filename":"logo-light.png","mime":"image/png",
"size_bytes":<bytes>,"sha256":"<hex64>"}'
# → data.fid + data.upload_url;直传字节后 commit(同 sha256 重放 = 幂等同行同 id)
curl -X POST "$BASE/api/v1/files:commit" -H "Authorization: Bearer $BRAND_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"purpose":"brand-asset","fid":"<fid>","mime":"image/png",
"size_bytes":<bytes>,"sha256":"<hex64>"}'
# → data.url(公开可读 URL——品牌/博客图片语义,稳定不过期)
# ② PATCH 落品牌资料(七列白名单部分更新;null 清除单列;外链仅收 https://)
curl -X PATCH "$BASE/api/v1/brand/profile" -H "Authorization: Bearer $BRAND_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"display_name":"口语搭子","slogan":"每天开口 10 分钟",
"logo_light_url":"<①的 data.url>","logo_dark_url":"<①的 data.url>"}'
# ③ 读回(同 realm 任何租户可读;无行 / 跨 realm → 404)
curl "$BASE/api/v1/brand/profile" -H "Authorization: Bearer $BRAND_TOKEN"- 写权限三段链(全 fail-closed):持
brand:profile:writescope → 租户属本 realm → 租户 == 该 realm 的运营租户;非运营租户上传品牌图 / PATCH 一律 403BRAND_PROFILE_FORBIDDEN operator_tenant_id不可经 PATCH 改派(携带即 422;改派 = 平台特权通道决策项)- 变更审计:diff 落 WORM(
brand.profile_update,old/new 仅实改字段;同值重写零审计) - 界面入口:builder「品牌设置」页(运营租户可写,同 realm 他租户只读态)
范围口径:本期 = 品牌资料存储 + 自助录入 + realms 可视;博客品牌呈现与编辑器贴图上传(BA2)在延后池(接缝已备,触发 = 真实需求 + owner 立项)。验收 runbook(
docs/ops/ba6-brand-acceptance.md)为仓内运维面,不入对外站。