接入会话服务
会话服务识别来信号码、完成 hi 验证、检查开通,并把渠道身份和所选产品交给 AI service。
| 环境 | 登录地址 | 带证书调用的接口地址 |
|---|---|---|
local | https://id.local.globalbes.sg | https://id.local.globalbes.sg:19443 |
test | https://id.test.globalbes.sg | https://id.test.globalbes.sg:9443 |
staging | 待定 | 待定 |
prod | 待定 | 待定 |
Sign in to send requests to see which of these steps are done. Sign in
生成私钥和证书请求
在哪里做:会话服务服务器
生成会话服务私钥和证书请求,把证书请求送到客户端 CA 签发。
CERT_DIR="$HOME/bes-certs/assistant" mkdir -p "$CERT_DIR" SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z) openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \ -subj "/CN=Assistant" \ -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature" \ -addext "extendedKeyUsage=clientAuth" \ -keyout "$CERT_DIR/assistant-service-key.pem" -out "$CERT_DIR/assistant-service.csr"
SERVICE_ACCOUNT_ID每次新生成,写进证书唯一的 URI SANurn:bes:service:<uuid>;登记平台账号后它就是账号的service_account_id。- 私钥
assistant-service-key.pem放进 Assistant 部署的密钥存储。
检查:openssl req -in "$HOME/bes-certs/assistant/assistant-service.csr" -noout -verify 成功;生成失败时核对目录写权限和 OpenSSL 版本。
本环境(test)
在该环境的会话服务服务器执行上面的命令,证书目录为 ~/bes-certs/assistant/。
其他环境
local
在该环境的会话服务服务器执行上面的命令,证书目录为 ~/bes-certs/assistant/。
staging
在该环境的会话服务服务器执行上面的命令,证书目录为 ~/bes-certs/assistant/。
prod
在该环境的会话服务服务器执行上面的命令,证书目录为 ~/bes-certs/assistant/。
核对结果
确认生成的证书请求、签发证书或账号输出对应同一个 service_account_id。
签发证书
在哪里做:客户端 CA 所在的机器
客户端 CA 签发会话服务证书,将证书安装到会话服务和 Control 登记路径。
前提:OpenSSL 3.0 及以上,openssl version 显示 OpenSSL 3.x。Mac 自带的 /usr/bin/openssl 是 LibreSSL,不认 -copy_extensions:先执行 brew install openssl@3,再执行 export PATH="$(brew --prefix openssl@3)/bin:$PATH"。
检查:在 CA 所在机器执行 openssl verify -CAfile "$CA_DIR/client-ca.pem" "$CERT_DIR/assistant-service.pem" 返回 OK;证书用途或有效期有误时重新签发。
本环境(test)
CA_DIR:待定,test 环境的客户端 CA 位置未定。- 签发前把
assistant-service.csr拷到客户端 CA 所在机器的~/bes-certs/assistant/;签发后把assistant-service.pem拷回 Assistant 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/assistant" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/assistant-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/assistant-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/assistant-service.pem"
其他环境
local
本地的客户端 CA 在本机,证书请求不用拷,在本机执行:
CERT_DIR="$HOME/bes-certs/assistant"
mkdir -p "$CERT_DIR"
CA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/assistant-service.csr" \
-CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
-CAserial "$CERT_DIR/assistant-service.srl" -CAcreateserial -copy_extensions copy \
-out "$CERT_DIR/assistant-service.pem"
staging
CA_DIR:待定,staging 环境的客户端 CA 位置未定。- 签发前把
assistant-service.csr拷到客户端 CA 所在机器的~/bes-certs/assistant/;签发后把assistant-service.pem拷回 Assistant 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/assistant" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/assistant-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/assistant-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/assistant-service.pem"
prod
CA_DIR:待定,prod 环境的客户端 CA 位置未定。- 签发前把
assistant-service.csr拷到客户端 CA 所在机器的~/bes-certs/assistant/;签发后把assistant-service.pem拷回 Assistant 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/assistant" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/assistant-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/assistant-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/assistant-service.pem"
核对结果
确认生成的证书请求、签发证书或账号输出对应同一个 service_account_id。
登记平台账号
在哪里做:Control 服务器
登记会话服务的平台账号,后续用该证书调用 Control。
账号角色为 conversation-gateway,命令输出应包含 role=conversation-gateway;重复执行保留同一账号。显示名使用 Assistant。
本环境(test)
- 在 Control 服务器上执行
bes-control的方式:待定,test 环境的 Control 部署方式未定。 assistant-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role conversation-gateway --certificate "待定/assistant-service.pem" --display-name Assistant
其他环境
local
本地 Control 在容器 control-api 里,先把证书拷进 ~/.local/share/bes/local/stack/secrets/control/,每次执行时它被复制成容器里的 /run/bes/control/assistant-service.pem。在本机执行:
BES_INFRA="$HOME/Projects/bes-infra" # bes-infra 仓的路径
cp "$HOME/bes-certs/assistant/assistant-service.pem" "${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control/assistant-service.pem"
"$BES_INFRA/envs/local/dev" stack compose exec -T control-api /bin/sh /opt/bes/stack-entrypoint.sh \
bes-control seed-trusted-caller --role conversation-gateway --certificate /run/bes/control/assistant-service.pem --display-name Assistant
staging
- 在 Control 服务器上执行
bes-control的方式:待定,staging 环境的 Control 部署方式未定。 assistant-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role conversation-gateway --certificate "待定/assistant-service.pem" --display-name Assistant
prod
- 在 Control 服务器上执行
bes-control的方式:待定,prod 环境的 Control 部署方式未定。 assistant-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role conversation-gateway --certificate "待定/assistant-service.pem" --display-name Assistant --confirm-production
核对结果
确认生成的证书请求、签发证书或账号输出对应同一个 service_account_id。
做完这一部分,手里有:取得会话服务证书,并登记持有 conversation-gateway 角色的平台账号。
查来信地址属于谁
在哪里做:会话服务代码
按来信 WhatsApp 号码查人,取得 verified_at 和可选的产品、租户、项目。
GETIdentity Resolve程序里发这个请求(id 用前面步骤返回的):
带:证书
请求:
GET https://id.test.globalbes.sg:9443/internal/control/v1/identities/resolve?channel_type=whatsapp&channel_identifier=6591234567
返回:
HTTP/1.1 200 OK
{
"found": true,
"verified_at": null,
"person_id": "5a9d3c1b-7e24-4f60-b8a1-c03e6d2f9475",
"contexts": [
{
"product_id": "b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614",
"tenant_id": "7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11",
"project_id": "e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903",
"tenant_name": "Acme Construction",
"project_name": "Harbour Tower"
}
]
}
录入成员号码时使用 whatsapp 类型、纯数字不带加号,例如 6591234567。收到 Messaging 已验 Meta 签名的一对一消息后,用同一规范号码调用。found: false 时提示登记地址;found: true 时保存 person_id 和 contexts,供后续选择产品范围。
本环境(test)
Control 网关:https://id.test.globalbes.sg:9443;证书目录 ~/bes-certs/assistant/。
其他环境
local
Control 网关:https://id.local.globalbes.sg:19443;证书目录 ~/bes-certs/assistant/。
staging
Control 网关:待定;证书目录 ~/bes-certs/assistant/。
prod
Control 网关:待定;证书目录 ~/bes-certs/assistant/。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 403 | ACCESS_DENIED |
核对平台账号的 conversation-gateway 角色。 |
请求示例
curl --fail-with-body --cert "$HOME/bes-certs/assistant/assistant-service.pem" --key "$HOME/bes-certs/assistant/assistant-service-key.pem" --get "https://id.test.globalbes.sg:9443/internal/control/v1/identities/resolve" --data-urlencode 'channel_type=whatsapp' --data-urlencode 'channel_identifier=6591234567'
完成 hi 验证
在哪里做:会话服务代码
verified_at 为空时请对方回复 hi,收到回复后把入站消息证明交给 Control。
POSTProve inbound channel possession程序里发这个请求(id 用前面步骤返回的):
带:证书
请求:
POST https://id.test.globalbes.sg:9443/internal/control/v1/channel-identities/prove-inbound
Content-Type: application/json
{
"request_ref": "inbound-wamid-001",
"channel_type": "whatsapp",
"channel_identifier": "6591234567",
"provider_message_id": "wamid.inbound-001",
"received_at": "2026-10-06T08:00:00+00:00",
"conversation": "direct"
}
返回:
HTTP/1.1 200 OK
{
"channel_identity_id": "6b1f4e9a-8c30-4d72-a5e6-f09c3b7d1e24",
"verified_at": "2026-10-06T08:00:01+00:00",
"newly_verified": true
}
verified_at 为空时,通过 Messaging 向该号码发 hi 提示;等待官方 WhatsApp 一对一回复,再传该回复的 provider_message_id 和 received_at。以消息标识生成稳定 request_ref,网络失败时原样重试。成功后保存 verified_at;newly_verified 为 false 同样继续。入站来源证明号码持有,可用于对话和委托;本人完成验证码或首次设密后来源升级为本人完成。 检查结果:再次认人时 verified_at 非空;同一 request_ref 重试返回首次结果。
本环境(test)
Control 网关:https://id.test.globalbes.sg:9443;证书目录 ~/bes-certs/assistant/。
其他环境
local
Control 网关:https://id.local.globalbes.sg:19443;证书目录 ~/bes-certs/assistant/。
staging
Control 网关:待定;证书目录 ~/bes-certs/assistant/。
prod
Control 网关:待定;证书目录 ~/bes-certs/assistant/。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 404 | NOT_FOUND |
重新认人,确认号码已有 active 绑定且人员 active。 |
| 400 | INVALID_REQUEST |
核对 whatsapp、direct、号码和带时区的 received_at。 |
| 409 | IDEMPOTENCY_CONFLICT |
同一 request_ref 重试保留原参数;不同来信使用不同 request_ref。 |
请求示例
curl --fail-with-body --cert "$HOME/bes-certs/assistant/assistant-service.pem" --key "$HOME/bes-certs/assistant/assistant-service-key.pem" -H "Content-Type: application/json" "https://id.test.globalbes.sg:9443/internal/control/v1/channel-identities/prove-inbound" --data '{"request_ref": "inbound-wamid-001", "channel_type": "whatsapp", "channel_identifier": "6591234567", "provider_message_id": "wamid.inbound-001", "received_at": "2026-10-06T08:00:00+00:00", "conversation": "direct"}'
查开通
在哪里做:会话服务代码
按所选租户、项目检查 ai-service 和 whatsapp-channel 的有效授权,取得可用期限。
GETEntitlement Resolve程序里发这个请求(id 用前面步骤返回的):
带:证书
请求:
GET https://id.test.globalbes.sg:9443/internal/control/v1/entitlements/resolve?tenant_id=7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11&project_id=e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903&product_id=b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614
返回:
HTTP/1.1 200 OK null
从环境产品配置取得 ai-service 和 whatsapp-channel 各自的 product_id(值待定),分别查询。样例 product_id 替换为对应产品的 ID。两项结果都为有效记录时继续;返回 null 时提示开通。缓存记录的 ends_at,并在到期后重新查询。租户级产品省略 project_id。
本环境(test)
Control 网关:https://id.test.globalbes.sg:9443;证书目录 ~/bes-certs/assistant/。
其他环境
local
Control 网关:https://id.local.globalbes.sg:19443;证书目录 ~/bes-certs/assistant/。
staging
Control 网关:待定;证书目录 ~/bes-certs/assistant/。
prod
Control 网关:待定;证书目录 ~/bes-certs/assistant/。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 404 | PRODUCT_NOT_FOUND |
核对环境中登记的产品 ID。 |
| 403 | ACCESS_DENIED |
核对平台账号的授权解析权限。 |
检查两项开通
AI_SERVICE_PRODUCT_ID="待定"
WHATSAPP_CHANNEL_PRODUCT_ID="待定"
TENANT_ID="7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11"
PROJECT_ID="e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903"
for PRODUCT_ID in "$AI_SERVICE_PRODUCT_ID" "$WHATSAPP_CHANNEL_PRODUCT_ID"; do
curl --fail-with-body --cert "$HOME/bes-certs/assistant/assistant-service.pem" \
--key "$HOME/bes-certs/assistant/assistant-service-key.pem" \
--get "https://id.test.globalbes.sg:9443/internal/control/v1/entitlements/resolve" \
--data-urlencode "tenant_id=$TENANT_ID" --data-urlencode "project_id=$PROJECT_ID" \
--data-urlencode "product_id=$PRODUCT_ID"
done
交给 AI service
在哪里做:会话服务代码
把规范渠道身份、消息和确认的产品、租户、项目交给 AI service,由 AI 换委托令牌调用产品。
用 BES_ASSISTANT_AI_URL 配置的 AI mTLS 地址调用 POST /internal/ai/v1/conversation-turns。 按 conversation.submit 传 turn_ref、surface、subject、context、session_id、messages、supersedes_turn_ref、trace_id; 渠道 subject 使用 kind=channel_identity、channel_type=whatsapp、identifier 与 direct/group surface; context 是已确认的 product_id、tenant_id、project_id。多项上下文先由会话服务筛选和选择。 不把用户登录令牌、person_id 或 verified_at 当成该请求的额外字段。 相同 turn_ref 重试保留业务参数,响应 ai_turn_id、session_id、replayed;异参返回 CONFLICT.IDEMPOTENCY_KEY。 打断调用 POST /internal/ai/v1/conversation-turns/{turn_ref}/abort;AI 向登记的 POST /internal/assistant/v1/turn-results 回调,completed 带 reply、failed 带 failure。 挂件使用 widget_turn subject,AI 经 turn-token.issue 换取当前回合的窄 scope 委托令牌。
本环境(test)
AI 地址取部署配置 BES_ASSISTANT_AI_URL;真实服务就绪后验收。
其他环境
local
AI 地址取 BES_ASSISTANT_AI_URL,本机可使用 README 中的 AI 替身;回调经 Assistant mTLS 入口。
staging
AI 地址取部署配置 BES_ASSISTANT_AI_URL。
prod
AI 地址取部署配置 BES_ASSISTANT_AI_URL。
接收事件并清缓存
在哪里做:会话服务代码
接收 Control 事件,持久化去重即返回 2xx,再本地应用与重试。
按事件契约验证签名与 audience,使用 event_id 去重,持久化即 2xx,应用失败本地重试;发送方仅在未持久化时重试投递。
channel_access.revoked:丢掉该 channel_identity_id 的地址、认人和会话身份缓存;按 person_id 清除关联范围。person.status_changed:丢掉该 person_id 的地址、认人、授权和会话身份缓存。entitlement.changed:丢掉对应产品、租户、项目的开通结果和包含该范围的 contexts。channel_identity.verified:丢掉该渠道的未验证认人结果和 hi 等待状态,重新解析 verified_at。membership.revoked:丢掉该 person_id 在该 tenant_id 下的 contexts 和当前会话所选范围。role_assignment.changed:丢掉该 person_id 对应租户、项目、产品的 contexts 和权限缓存。
本环境(test)
在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 assistant-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。
其他环境
local
在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 assistant-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。
staging
在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 assistant-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。
prod
在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 assistant-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。
订阅配置
{
"assistant": {
"endpoint": "待定",
"audience": "assistant-api",
"event_types": [
"channel_access.revoked",
"person.status_changed",
"entitlement.changed",
"channel_identity.verified",
"membership.revoked",
"role_assignment.changed"
]
}
}
做完这一部分,手里有:取得可用于 AI 委托的渠道身份和已确认范围,并保持缓存与 Control 状态一致。
展示上下文名称
在哪里做:会话服务代码
显示 Control 返回的租户与项目名称;租户级项目名为 null。
身份上下文的显示名称来自 identity.resolve:每项包含 tenant_name 与 project_name;租户级 project_name 为 null。示例:{product_id: "<产品 UUID>", tenant_id: "<租户 UUID>", tenant_name: "Acme Construction", project_name: null}。
挂件使用独立不可售卖的 Assistant 产品。机密客户端登记 assistant.widget scope;授权请求带 host_product_id 与 scope=openid assistant.widget,回调取环境配置,本机为 https://assistant.local.globalbes.sg/widget/callback。
prompt=none 缺少 SSO 或有效宿主登录时返回 login_required;先在弹窗登录宿主,再重试挂件静默授权。无需 Assistant 成员关系或开通,也不开放登录页 iframe。
换码事务将 sid 绑定到同一 SSO 的宿主登录及 generation。Assistant 使用服务端令牌和自己的 mTLS 身份调用 tenant-list(product_id=宿主)、validate(product_id=宿主);两处都读取绑定。
validate 的 target 使用产品目录 assistant.chat 对应的 method/path/deployment_version,并携带选定 tenant_id/project_id。挂件本身不缓存结果。
委托交换以用户访问令牌为 subject、宿主产品 UUID 为 audience;act.sub 为 Assistant 服务账号 UUID。产品需登记唯一有效的服务客户端,其 scopes 与用户当前权限限定委托范围。
宿主退出或全 SSO 退出后失败;刷新不改绑定。宿主重新登录后,旧挂件仍失败,必须重新走挂件授权和换码。
做完这一部分,手里有:按 Control 当前上下文展示租户与项目名称。