AIP TALK 第三方 SSO / 嵌入集成指南
将 AIP TALK 对话能力嵌入到第三方门户、ERP 或办公平台。本指南面向第三方系统开发、运维人员,提供从配置到运行的完整步骤与可直接复制粘贴的代码示例。
当前服务器地址
能力概览
第三方系统可以通过标准 SSO 流程,让用户无感登录 CowAgent,并在自己的页面内嵌入 AIP TALK 智能对话窗口。集成后可实现:
- 单点登录:第三方用户通过 JWT assertion 换取 CowAgent token,无需再次输入密码。
- iframe 嵌入:一行 iframe 即可将 AIP TALK 嵌入到第三方页面。
- 主题/语言控制:父页面可通过 postMessage 设置主题与语言。
- 历史与决策:可直接调用 AIP-TALK 历史、收藏、决策标记与复盘 API。
前置条件与配置清单
在 CowAgent 服务端完成以下配置后,第三方系统才能接入。
| 配置项 | 说明 |
|---|---|
| multi_user_mode | 必须设置为 true,SSO 流程才能启用。 |
| auth_secret | CowAgent v2 token 签名密钥,必须设置为强随机字符串。 |
| web_public_url | 建议配置公网地址,用于生成文档中的示例 URL。 |
| sso_trusted_issuers |
添加第三方系统的 issuer 配置,见下方示例。
其中 allowed_origins 可在登录 CowAgent 后,进入
系统管理 > 第三方集成设置
在线修改,保存后立即生效,无需重启服务。
|
config.json 中的 issuer 示例
{
"multi_user_mode": true,
"auth_secret": "your-strong-random-secret",
"sso_trusted_issuers": [
{
"issuer_id": "partner_erp",
"secret": "issuer-shared-secret-only-you-and-cowagent-know",
"audience": "http://localhost:9899",
"tenant_mapping": {},
"default_role_ids": ["tenant:user"],
"auto_create_tenant": true,
"auto_create_user": true,
"sync_attributes": true,
"assertion_ttl_seconds": 60,
"ip_allowlist": ["10.0.0.0/8"],
"enabled": true,
"default_return_to": "/chat#view=aip-talk",
"embed_return_to": "/embed/aip-talk",
"allowed_origins": ["https://partner.example.com"],
"embed_title": "AIP TALK",
"embed_subtitle": "AI-Powered Automation for Every Decision",
"welcome_greeting": "我是企业AI大脑,请提问"
}
]
}
embed_title / embed_subtitle / welcome_greeting 为可选项,
分别控制嵌入页头部两行标题与欢迎屏初始提示语;不配置时使用上方缺省值。
secret 必须严格保密,只应存在于 CowAgent 服务端和第三方服务端,禁止暴露在前端代码中。
快速开始:三步完成嵌入
配置 issuer
在 CowAgent 的 config.json 中添加上方 issuer 示例。
allowed_origins 与页面文案(embed_title / embed_subtitle / welcome_greeting)
可在应用内「系统管理 > 第三方集成设置」中在线修改,无需重启。
换取 token
第三方后端生成 JWT assertion,调用 POST /api/v1/sso/token 获取 CowAgent v2 token。
嵌入 iframe
在第三方页面使用 iframe 加载 /embed/aip-talk?token=<TOKEN>,并确保域名已加入 allowed_origins(留空则不限制来源)。
参考实现下载
我们提供了一套可直接运行的最小示例工程 3rd-apitalk-int,包含 Vite 前端、Python Flask 后端代理、SSO assertion 签名、iframe 嵌入、主题/语言控制以及 Playwright/pytest 测试。你可以将其作为第三方门户接入 AIP-TALK 的起点。
目录结构
3rd-apitalk-int/
├── server/
│ ├── app.py # Flask 后端:保管 issuer secret、签发 assertion、换 token
│ ├── config.py # 配置读取
│ └── sso.py # SSO JWT assertion 生成
├── src/
│ ├── main.ts # 页面入口与表单控制
│ ├── embed.ts # AipTalkEmbed:iframe 创建、postMessage 协议
│ ├── api.ts # 与后端代理通信
│ └── styles.css
├── tests/e2e/ # Playwright 端到端测试
├── tests_py/ # pytest 后端/链路测试
├── .env.example
├── package.json
├── requirements.txt
└── README.md
快速运行
# 1. 启动 CowAgent(默认 http://localhost:9899)
# 2. 启动示例后端代理
cd 3rd-apitalk-int
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python server/app.py
# 3. 启动前端
cd 3rd-apitalk-int
npm install
npm run dev
# 4. 浏览器打开 http://localhost:6001,填写 CowAgent 服务地址并加载 AIP-TALK
demo_erp issuer,
allowed_origins 需包含 http://localhost:6001。生产环境请替换为自有 issuer 配置。
additional_audiences / allowed_origins;
点击 「查看示例代码」可复制带真实配置的 Python / Node.js / 前端 iframe / cURL 片段。
下面的通用 ZIP 使用 demo_erp 占位配置,需手动对齐后才能访问成功。
SSO 断言格式
assertion 是一个标准的 HMAC-SHA256(HS256) JWT。CowAgent 会校验签名、有效期、受众、issuer 身份,并防止同一 jti 被重放。
JWT Header
{
"alg": "HS256",
"typ": "JWT"
}
JWT Payload 字段
| 字段 | 必填 | 说明 |
|---|---|---|
| iss | 是 | issuer_id,必须与 config.json 中配置的 issuer_id 一致。 |
| sub | 是 | 第三方用户唯一标识。 |
| tid | 是 | 第三方租户/组织唯一标识。 |
| aud | 条件 | 若 issuer 配置了 audience,则必须匹配。 |
| iat / exp | 是 | 签发与过期时间,过期时间距签发时间不能超过 assertion_ttl_seconds。 |
| jti | 是 | 唯一 nonce,用于防止重放攻击。 |
| name | 否 | 用户显示名称,auto_create_user 时会同步。 |
| 否 | 用户邮箱。 | |
| roles | 否 | 角色列表,未提供时使用 issuer.default_role_ids。 |
Python 生成 assertion
import base64
import hashlib
import hmac
import json
import time
import uuid
ISSUER_SECRET = "issuer-shared-secret-only-you-and-cowagent-know"
AUDIENCE = "http://localhost:9899"
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode().rstrip("=")
def make_assertion(issuer_id: str, user_id: str, tenant_id: str) -> str:
header = {"alg": "HS256", "typ": "JWT"}
now = int(time.time())
payload = {
"iss": issuer_id,
"sub": user_id,
"tid": tenant_id,
"name": "张三",
"email": "zhangsan@example.com",
"aud": AUDIENCE,
"iat": now,
"exp": now + 60,
"jti": uuid.uuid4().hex,
"roles": ["tenant:user"],
}
header_b64 = b64url(json.dumps(header, separators=(",", ":")).encode())
payload_b64 = b64url(json.dumps(payload, separators=(",", ":")).encode())
signing_input = f"{header_b64}.{payload_b64}".encode()
signature = b64url(hmac.new(ISSUER_SECRET.encode(), signing_input, hashlib.sha256).digest())
return f"{header_b64}.{payload_b64}.{signature}"
assertion = make_assertion("partner_erp", "user-001", "org-001")
print(assertion)
后端换 Token
第三方服务端生成 assertion 后,调用 CowAgent 的 token 交换接口,获取可在前端使用的 v2 token。
cURL
curl -X POST http://localhost:9899/api/v1/sso/token \
-H "Content-Type: application/json" \
-d '{"assertion":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}'
Python
import requests
resp = requests.post(
"http://localhost:9899/api/v1/sso/token",
json={"assertion": assertion},
)
data = resp.json()
print(data["token"]) # CowAgent v2 token
print(data["user_id"]) # 自动生成的用户 ID
print(data["tenant_id"]) # 自动生成的租户 ID
print(data["expires_in"]) # 秒
成功响应示例
{
"status": "success",
"token": "cowagent-v2-token-string",
"user_id": "ext:partner_erp:user-001",
"tenant_id": "ext:partner_erp:org-001",
"roles": ["tenant:user"],
"expires_in": 2592000
}
浏览器跳转模式
如果第三方系统希望使用 Cookie 会话而不是前端 Bearer token,可以让浏览器自动提交表单到 /auth/sso。登录成功后 CowAgent 设置 cow_auth_token cookie 并 302 跳转。
<form id="sso-form" method="POST" action="http://localhost:9899/auth/sso">
<input type="hidden" name="assertion" value="GENERATED_ASSERTION" />
<input type="hidden" name="return_to" value="/embed/aip-talk" />
</form>
<script>document.getElementById("sso-form").submit();</script>
该模式适合同域或一级域名相同的场景;跨域 iframe 嵌入建议使用后端换 token 方案。
iframe 嵌入
拿到 token 后,在第三方页面渲染 iframe。CowAgent 会校验 embedding origin 是否在 allowed_origins 中,并通过 Content-Security-Policy: frame-ancestors 限制 framing。allowed_origins 留空时不限制来源,任何 origin 均可嵌入(响应头为 frame-ancestors *)。
最小嵌入代码
<iframe
id="aip-talk-frame"
src="http://localhost:9899/embed/aip-talk?token=YOUR_TOKEN"
width="100%"
height="600"
frameborder="0"
allow=" microphone"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
动态高度建议
window.addEventListener("message", (event) => {
if (event.origin !== "http://localhost:9899") return;
if (event.data?.type === "cow:resize" && event.data.height) {
document.getElementById("aip-talk-frame").style.height = event.data.height + "px";
}
});
父页面 postMessage 协议
嵌入页加载完成后会向父页面发送 cow:ready。父页面可发送以下消息控制嵌入页。
| 消息类型 | 方向 | 说明 |
|---|---|---|
| cow:ready | 子 → 父 | 嵌入页初始化完成,event.data 包含 version。 |
| cow:set-theme | 父 → 子 | 设置主题,payload: {"theme": "light|dark"}。 |
| cow:set-lang | 父 → 子 | 设置语言,payload: {"lang": "zh|en"}。 |
| cow:refresh-token | 父 → 子 | 刷新 token,payload: {"token": "NEW_TOKEN"}。 |
完整示例
const iframe = document.getElementById("aip-talk-frame");
const allowedOrigin = "http://localhost:9899";
window.addEventListener("message", (event) => {
if (event.origin !== allowedOrigin) return;
if (event.data?.type === "cow:ready") {
console.log("AIP TALK ready, version:", event.data.version);
// 同步第三方系统的主题
const theme = localStorage.getItem("theme") || "light";
iframe.contentWindow.postMessage({ type: "cow:set-theme", theme }, allowedOrigin);
// 同步语言
iframe.contentWindow.postMessage({ type: "cow:set-lang", lang: "zh" }, allowedOrigin);
}
});
// token 即将过期时刷新
function refreshToken(newToken) {
iframe.contentWindow.postMessage({ type: "cow:refresh-token", token: newToken }, allowedOrigin);
}
直接调用 AIP-TALK API
除了 iframe,第三方系统也可以直接调用后端 API。所有 API 都需要在请求头中携带 Authorization: Bearer <token>。
获取会话历史
curl -G http://localhost:9899/api/v1/aip-talk/history \
-H "Authorization: Bearer YOUR_TOKEN" \
-d "session_id=session_xxx" \
-d "is_decision=true"
标记关键决策
curl -X POST http://localhost:9899/api/v1/aip-talk/messages/session_xxx/42/decision \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"is_decision": true,
"title": "Q3 销售目标调整",
"note": "基于上月数据,建议将 Q3 目标上调 10%",
"expected_outcome": "季度销售额达到 1000 万"
}'
列出决策复盘
curl -G http://localhost:9899/api/v1/insights/decisions \
-H "Authorization: Bearer YOUR_TOKEN" \
-d "session_id=session_xxx" \
-d "status=reviewed" \
-d "page=1" \
-d "page_size=20"
安全最佳实践
- issuer secret 只存服务端:禁止将 secret 写入前端 JS、Git 仓库或日志。
-
断言短有效期:建议
exp - iat ≤ 60 秒,并同步配置assertion_ttl_seconds。 - 唯一 jti:每次换 token 必须生成新的 UUID,避免重放攻击。
- HTTPS only:生产环境必须使用 HTTPS,防止 assertion 和 token 被中间人截获。
- 严格 allowed_origins:只添加真正需要嵌入的域名,避免被未知站点 iframe 嵌套。留空表示不限制来源(任何站点均可嵌入),生产环境建议显式配置。
-
token 过期刷新:在 token 过期前通过后端重新换取,并通过
cow:refresh-token同步给 iframe。
故障排查
| 现象 | 排查方向 |
|---|---|
| 401 / token invalid | token 是否已过期;请求头是否正确携带 Authorization: Bearer ...。 |
| 403 Origin not allowed | 检查 issuer allowed_origins 是否包含当前页面的协议+域名+端口;注意 http 与 https 被视为不同 origin。allowed_origins 留空时任何 origin 均可嵌入,不会再返回 403。 |
| iframe 被 CSP 拒绝 | 浏览器控制台查看 Refused to frame,确认响应头 Content-Security-Policy: frame-ancestors 包含当前 origin。 |
| assertion expired | 确认服务器时间与 CowAgent 服务器时间同步;缩短 exp - iat 或增大 assertion_ttl_seconds。 |
| unknown issuer | 确认 iss 与 config.json 中 issuer_id 完全一致(区分大小写)。 |
| assertion already used | 同一个 jti 已被使用过,必须每次生成新的 jti。 |
AIP TALK 第三方集成指南 · CowAgent Enterprise AI
OpenAPI: /api/aip-talk/openapi.json