AI 客服系统开放平台对接文档


版本: v2.0.0

更新日期: 2026-01-14




📋 目录




1. 概述


1.1 介绍


AI 客服系统开放平台为第三方应用提供智能客服集成能力,支持实时对话、知识库问答、会话管理等功能。通过集成开放平台API,您可以快速将AI客服能力嵌入到自己的应用中。


1.2 核心特性


  • 双重验证机制:签名验证 + Token验证,确保安全性
  • WebSocket实时通信:支持长连接推送,实时接收客服消息
  • 多场景支持:Web、App、小程序全覆盖
  • 用户身份关联:支持匿名访客和登录用户两种模式
  • 会话管理:完整的会话生命周期管理
  • 智能知识库:AI自动问答,支持常见问题快速响应

1.3 接口域名


环境域名
测试环境https://test-api.example.com
生产环境https://api.example.com

⚠️ 请将域名替换为实际部署的域名


1.4 通信协议


  • 协议:HTTPS(推荐) / HTTP
  • 请求方式:POST / GET
  • 字符编码:UTF-8
  • 数据格式:JSON
  • WebSocket协议:WSS / WS


2. 认证机制


2.1 双重验证说明


开放平台采用两层验证机制,确保接口安全:


验证层级


┌─────────────────────────────────────────────┐
│  第1层:签名验证(Signature Verification)      │
│  验证商户身份合法性                          │
│  使用:open_app_key + open_secret_key        │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  第2层:Token验证(Token Verification)         │
│  验证访问权限和会话有效性                    │
│  使用:access_token                          │
└─────────────────────────────────────────────┘

接口验证规则


接口类型第1层签名验证第2层Token验证说明
获取Token✅ 必需❌ 不需要用签名换取Token
HTTP业务接口✅ 必需✅ 必需双重验证
WebSocket连接❌ 不需要✅ 必需Token已通过签名获取

安全机制


  • 防重放攻击:时间戳 + nonce 机制,签名5分钟内有效
  • 跨商户防护:Token与商户ID绑定,禁止跨商户访问
  • Token隔离:开放平台Token与前端Token分离,互不干扰


2.2 获取开放平台凭证


请联系平台管理员获取您的商户凭证:


{
  "open_app_key": "MERCHANT_123_abc456def789",
  "open_secret_key": "SECRET_xyz789uvw456rst123",
  "merchant_id": 123
}

⚠️ 安全提示:

  • open_secret_key 用于签名计算,切勿泄露或提交到代码仓库
  • 建议使用环境变量或配置中心管理密钥
  • 定期轮换密钥以提高安全性


2.3 签名算法


2.3.1 公共请求头参数


所有需要签名验证的接口都需要在HTTP头中包含以下参数:


参数类型必填说明
open_app_keystring商户密钥(公开)
timestampstringUnix时间戳(秒),如:1703567890
noncestring随机字符串,建议16-32位
signstring请求签名
sign_typestring签名类型:MD5 或 SHA256,默认MD5

2.3.2 签名计算步骤


步骤1:参数收集


  • 收集所有请求参数(包括 body 中的 JSON 参数)
  • 排除 sign 字段本身
  • 添加 open_secret_key 参数

步骤2:参数排序


按照参数名的字典序(ASCII码从小到大)排序


步骤3:拼接字符串


格式:key1=value1&key2=value2&...&open_secret_key=YOUR_SECRET


步骤4:计算签名


  • MD5方式:sign = MD5(拼接字符串)
  • SHA256方式:sign = SHA256(拼接字符串)

2.3.3 签名示例


请求示例:


POST /api/open/visitor/token HTTP/1.1
Host: api.example.com
Content-Type: application/json
open_app_key: MERCHANT_123_abc456
timestamp: 1703567890
nonce: abc123def456
sign_type: MD5
sign: 3a5f8c9d2e1b4f7a8c6e2d9f1a3b5c7d

{
  "app_key": "app_abc123",
  "scene": "app",
  "device_type": "mobile",
  "user_info": {
    "user_id": "customer_001",
    "phone": "13800138000"
  }
}

签名计算过程:


// 1. 参数收集
const params = {
  "app_key": "app_abc123",
  "scene": "app",
  "device_type": "mobile",
  "user_info": {"user_id":"customer_001","phone":"13800138000"}, // JSON序列化
  "timestamp": "1703567890",
  "nonce": "abc123def456",
  "open_secret_key": "SECRET_xyz789uvw456rst123" // 添加密钥
};

// 2. 字典序排序
// app_key, device_type, nonce, open_secret_key, scene, timestamp, user_info

// 3. 拼接字符串
const signStr = 'app_key=app_abc123&device_type=mobile&nonce=abc123def456&open_secret_key=SECRET_xyz789uvw456rst123&scene=app&timestamp=1703567890&user_info={"user_id":"customer_001","phone":"13800138000"}';

// 4. 计算MD5
const sign = MD5(signStr); // 结果:3a5f8c9d2e1b4f7a8c6e2d9f1a3b5c7d

2.3.4 注意事项


⚠️ 重要提示:


1. 时间戳有效期:服务器时间 ± 5分钟,超时则拒绝请求

2. nonce唯一性:建议使用UUID或随机字符串,防止重放攻击

3. 参数序列化:

  • 对象类型参数需要先 JSON 序列化(紧凑格式,无空格)
  • 空值参数不参与签名

4. 字符编码:统一使用 UTF-8 编码

5. 大小写敏感:参数名区分大小写




2.4 Token获取


接口说明


获取访客Token是使用开放平台的第一步,所有业务接口都需要先获取Token。


接口: POST /api/open/visitor/token


验证方式: 仅需签名验证(第1层)


请求参数


参数类型必填说明
app_keystring应用密钥(由后台管理员分配,不同于open_app_key)
visitor_idstring访客ID(设备唯一标识),首次调用可不传,服务端会生成
scenestring访问场景:web/app/miniapp/h5
device_typestring设备类型:mobile/pc/tablet
user_infoobject用户信息(登录用户模式必传)
user_info.user_idstring⚠️商户系统的用户ID,传user_info时必填
user_info.phonestring⚠️用户手机号,传user_info时必填
user_info.namestring用户姓名
user_info.emailstring用户邮箱
user_info.avatarstring用户头像URL
user_info.genderint用户性别:0-未知 1-男 2-女

请求示例


场景1:匿名访客模式


curl -X POST 'https://api.example.com/api/open/visitor/token' \
-H 'Content-Type: application/json' \
-H 'open_app_key: MERCHANT_123_abc456' \
-H 'timestamp: 1703567890' \
-H 'nonce: abc123def456' \
-H 'sign: 3a5f8c9d2e1b4f7a8c6e2d9f1a3b5c7d' \
-H 'sign_type: MD5' \
-d '{
  "app_key": "app_abc123",
  "scene": "web",
  "device_type": "pc"
}'

场景2:登录用户模式


curl -X POST 'https://api.example.com/api/open/visitor/token' \
-H 'Content-Type: application/json' \
-H 'open_app_key: MERCHANT_123_abc456' \
-H 'timestamp: 1703567890' \
-H 'nonce: abc123def456' \
-H 'sign: 4b6g9d0f3f2c5g8b9d7f3e0a2b4c6d8e' \
-H 'sign_type: MD5' \
-d '{
  "app_key": "app_abc123",
  "scene": "app",
  "device_type": "mobile",
  "user_info": {
    "user_id": "customer_001",
    "phone": "13800138000",
    "name": "张三",
    "avatar": "https://example.com/avatar.jpg"
  }
}'

响应示例


{
  "code": 200,
  "msg": "success",
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDM1Njk2OTAsImlhdCI6MTcwMzU2Nzg5MCwidmlzaXRvcl9pZCI6InZpc2l0b3JfMTcwMzU2Nzg5MF9hYmMxMjMifQ.xxx",
    "refresh_token": "550e8400-e29b-41d4-a716-446655440000",
    "visitor_id": "visitor_1703567890_abc123",
    "expires_in": 600,
    "user_info": {
      "user_id": "customer_001",
      "phone": "13800138000",
      "name": "张三",
      "avatar": "https://example.com/avatar.jpg"
    }
  }
}

响应字段说明


字段类型说明
access_tokenstring访问令牌,有效期10分钟,用于后续接口调用
refresh_tokenstring刷新令牌,有效期30天(暂未启用)
visitor_idstring访客唯一标识,客户端应持久化保存
expires_inintaccess_token过期时间(秒)
user_infoobject用户信息(仅登录用户模式返回)

注意事项


⚠️ 重要提示:


1. visitor_id 管理:

  • 首次调用可不传,服务端会生成
  • 客户端应将 visitor_id 持久化保存(localStorage/数据库)
  • 后续调用传递相同的 visitor_id 可关联会话历史

2. Token 有效期:

  • access_token 有效期为10分钟
  • Token 即将过期时应重新调用此接口获取新Token
  • 建议在有效期过半时刷新Token

3. 用户信息绑定:

  • 传递 user_info 时,user_id 和 phone 必须同时传递
  • 同一 user_id 的 phone 必须保持一致,否则返回错误
  • 用户信息会关联到会话记录,支持用户画像分析

4. 跨商户验证:

  • app_key 必须属于签名的商户
  • 禁止商户A使用商户B的app_key获取Token


3. API 接口参考


以下所有接口均需要双重验证(签名 + Token)。


通用请求头


除了签名相关的头部参数外,还需要携带Token:


参数类型必填说明
Authorizationstring格式:Bearer {access_token}

或者通过 URL 参数传递:


?access_token={access_token}



3.1 结束会话


主动结束客服会话。


接口: POST /api/open/chat/session/end


验证方式: 签名验证 + Token验证(双重)


请求参数


参数类型必填说明
session_idstring会话ID

请求示例


curl -X POST 'https://api.example.com/api/open/chat/session/end' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'open_app_key: MERCHANT_123_abc456' \
-H 'timestamp: 1703567890' \
-H 'nonce: abc123def456' \
-H 'sign: 5c7h0e1g4g3d6h9c0e8g4f1b3c5d7e9f' \
-H 'sign_type: MD5' \
-d '{
  "session_id": "session_1703567890_xyz"
}'

响应示例


{
  "code": 200,
  "msg": "会话已结束",
  "data": null
}



3.2 设置会话已解决


标记会话问题已解决。


接口: POST /api/open/chat/session/solve


验证方式: 签名验证 + Token验证(双重)


请求参数


参数类型必填说明
session_idstring会话ID
is_solveint是否解决:1-已解决 0-未解决

请求示例


curl -X POST 'https://api.example.com/api/open/chat/session/solve' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'open_app_key: MERCHANT_123_abc456' \
-H 'timestamp: 1703567890' \
-H 'nonce: def456ghi789' \
-H 'sign: 6d8i1f2h5h4e7i0d1f9h5g2c4d6e8f0g' \
-H 'sign_type: MD5' \
-d '{
  "session_id": "session_1703567890_xyz",
  "is_solve": 1
}'

响应示例


{
  "code": 200,
  "msg": "操作成功",
  "data": null
}



3.3 会话评价


用户对会话进行评价。


接口: POST /api/open/chat/session/rating


验证方式: 签名验证 + Token验证(双重)


请求参数


参数类型必填说明
session_idstring会话ID
ratingint评分:1-5星
commentstring评价内容
tagsarray评价标签,如:["响应及时","态度好"]

请求示例


curl -X POST 'https://api.example.com/api/open/chat/session/rating' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'open_app_key: MERCHANT_123_abc456' \
-H 'timestamp: 1703567890' \
-H 'nonce: ghi789jkl012' \
-H 'sign: 7e9j2g3i6i5f8j1e2g0i6h3d5e7f9g1h' \
-H 'sign_type: MD5' \
-d '{
  "session_id": "session_1703567890_xyz",
  "rating": 5,
  "comment": "客服态度很好,问题解决及时",
  "tags": ["响应及时", "态度好", "专业"]
}'

响应示例


{
  "code": 200,
  "msg": "评价成功",
  "data": null
}



3.4 标记消息已读


标记指定消息为已读状态。


接口: POST /api/open/chat/mark_read


验证方式: 签名验证 + Token验证(双重)


请求参数


参数类型必填说明
message_idstring消息ID

请求示例


curl -X POST 'https://api.example.com/api/open/chat/mark_read' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'open_app_key: MERCHANT_123_abc456' \
-H 'timestamp: 1703567890' \
-H 'nonce: jkl012mno345' \
-H 'sign: 8f0k3h4j7j6g9k2f3h1j7i4e6f8g0h2i' \
-H 'sign_type: MD5' \
-d '{
  "message_id": "msg_1703567890_abc"
}'

响应示例


{
  "code": 200,
  "msg": "已标记为已读",
  "data": null
}



3.5 获取常见问题


获取配置的常见问题列表。


接口: GET /api/open/chat/questions


验证方式: 签名验证 + Token验证(双重)


请求参数


无需 body 参数,Token 和签名通过请求头传递。


请求示例


curl -X GET 'https://api.example.com/api/open/chat/questions?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'open_app_key: MERCHANT_123_abc456' \
-H 'timestamp: 1703567890' \
-H 'nonce: mno345pqr678' \
-H 'sign: 9g1l4i5k8k7h0l3g4i2k8j5f7g9h1i3j' \
-H 'sign_type: MD5'

响应示例


{
  "code": 200,
  "msg": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "question": "如何退款?",
        "answer": "您可以在订单详情页面点击退款按钮...",
        "category": "售后服务",
        "sort": 1
      },
      {
        "id": 2,
        "question": "配送需要多久?",
        "answer": "正常情况下3-5个工作日送达...",
        "category": "物流配送",
        "sort": 2
      }
    ],
    "total": 2
  }
}



4. WebSocket 通信


4.1 建立连接


WebSocket用于实时接收客服消息和系统通知,仅需Token验证,无需签名。


接口: GET /api/open/chat/customer/ws


验证方式: 仅Token验证(第2层)


连接URL格式:


wss://api.example.com/api/open/chat/customer/ws?access_token={access_token}

或者通过请求头传递Token:


const ws = new WebSocket('wss://api.example.com/api/open/chat/customer/ws', {
  headers: {
    'Authorization': 'Bearer ' + access_token
  }
});

连接示例


JavaScript (浏览器)


// 1. 先获取Token
const tokenData = await fetch('https://api.example.com/api/open/visitor/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'open_app_key': 'MERCHANT_123_abc456',
    'timestamp': '1703567890',
    'nonce': 'abc123def456',
    'sign': '3a5f8c9d2e1b4f7a8c6e2d9f1a3b5c7d',
    'sign_type': 'MD5'
  },
  body: JSON.stringify({
    app_key: 'app_abc123',
    scene: 'web',
    device_type: 'pc'
  })
}).then(res => res.json());

// 2. 建立WebSocket连接
const ws = new WebSocket(
  `wss://api.example.com/api/open/chat/customer/ws?access_token=${tokenData.data.access_token}`
);

ws.onopen = () => {
  console.log('WebSocket连接成功');
};

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log('收到消息:', message);
};

ws.onerror = (error) => {
  console.error('WebSocket错误:', error);
};

ws.onclose = (event) => {
  console.log('WebSocket关闭:', event.code, event.reason);
};

Node.js


const WebSocket = require('ws');

// 1. 先获取Token (省略,同上)

// 2. 建立WebSocket连接
const ws = new WebSocket(
  `wss://api.example.com/api/open/chat/customer/ws?access_token=${access_token}`
);

ws.on('open', () => {
  console.log('WebSocket连接成功');
});

ws.on('message', (data) => {
  const message = JSON.parse(data);
  console.log('收到消息:', message);
});

ws.on('error', (error) => {
  console.error('WebSocket错误:', error);
});

ws.on('close', (code, reason) => {
  console.log('WebSocket关闭:', code, reason);
});



4.2 消息格式


4.2.1 发送消息(客户端→服务端)


文本消息


{
  "type": "text",
  "content": "您好,我想咨询一下退款问题"
}

图片消息


{
  "type": "image",
  "content": "https://example.com/upload/image.jpg"
}

文件消息


{
  "type": "file",
  "content": "https://example.com/upload/document.pdf",
  "filename": "合同文件.pdf",
  "filesize": 1024000
}

4.2.2 接收消息(服务端→客户端)


普通消息


{
  "type": "message",
  "data": {
    "message_id": "msg_1703567890_abc",
    "session_id": "session_1703567890_xyz",
    "sender_type": "robot",
    "sender_id": "robot_001",
    "sender_name": "智能助手",
    "content_type": "text",
    "content": "您好,关于退款问题,您可以...",
    "timestamp": 1703567890,
    "created_at": "2026-01-14 10:30:00"
  }
}

系统通知


{
  "type": "system",
  "event": "session_timeout",
  "data": {
    "message": "会话即将超时,请尽快完成咨询",
    "remaining_seconds": 60
  }
}

客服转接通知


{
  "type": "system",
  "event": "agent_transfer",
  "data": {
    "message": "正在为您转接人工客服...",
    "agent_name": "客服小王",
    "agent_avatar": "https://example.com/avatar/agent001.jpg"
  }
}

会话结束通知


{
  "type": "system",
  "event": "session_end",
  "data": {
    "session_id": "session_1703567890_xyz",
    "message": "会话已结束,感谢您的咨询",
    "end_reason": "user_close"
  }
}

4.2.3 消息类型说明


sender_type 发送者类型:


说明
robot机器人客服
agent人工客服
system系统消息

content_type 内容类型:


说明
text文本消息
image图片消息
file文件消息
rich_text富文本消息(HTML)
card卡片消息

event 事件类型:


说明
session_start会话开始
session_end会话结束
session_timeout会话超时提醒
agent_transfer转接人工
agent_online客服上线
agent_offline客服离线
message_read消息已读



4.3 心跳机制


为保持WebSocket连接活跃,客户端应定期发送心跳包。


心跳包格式


客户端发送:


{
  "type": "ping"
}

服务端响应:


{
  "type": "pong",
  "timestamp": 1703567890
}

心跳示例


// 每30秒发送一次心跳
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 30000);

// 接收心跳响应
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  if (message.type === 'pong') {
    console.log('心跳正常');
  }
};



4.4 断线重连


网络波动可能导致WebSocket断开,建议实现自动重连机制。


重连策略


1. 指数退避算法: 1s → 2s → 4s → 8s → 16s → 最大30s

2. 最大重连次数: 建议10次

3. Token过期处理: 重连前检查Token是否过期,过期则重新获取


重连示例


class WebSocketClient {
  constructor(url, getToken) {
    this.url = url;
    this.getToken = getToken; // 获取Token的函数
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 10;
    this.reconnectDelay = 1000; // 初始1秒
    this.maxReconnectDelay = 30000; // 最大30秒
  }

  async connect() {
    try {
      // 获取最新Token
      const token = await this.getToken();

      this.ws = new WebSocket(`${this.url}?access_token=${token}`);

      this.ws.on('open', () => {
        console.log('WebSocket连接成功');
        this.reconnectAttempts = 0;
        this.reconnectDelay = 1000;
      });

      this.ws.on('message', (data) => {
        const message = JSON.parse(data);
        this.handleMessage(message);
      });

      this.ws.on('error', (error) => {
        console.error('WebSocket错误:', error);
      });

      this.ws.on('close', (code, reason) => {
        console.log('WebSocket关闭:', code, reason);
        this.reconnect();
      });

    } catch (error) {
      console.error('连接失败:', error);
      this.reconnect();
    }
  }

  reconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('达到最大重连次数,停止重连');
      return;
    }

    this.reconnectAttempts++;
    console.log(`${this.reconnectDelay}ms后进行第${this.reconnectAttempts}次重连...`);

    setTimeout(() => {
      this.connect();
    }, this.reconnectDelay);

    // 指数退避
    this.reconnectDelay = Math.min(
      this.reconnectDelay * 2,
      this.maxReconnectDelay
    );
  }

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.error('WebSocket未连接');
    }
  }

  handleMessage(message) {
    // 处理接收到的消息
    console.log('收到消息:', message);
  }

  close() {
    if (this.ws) {
      this.ws.close();
    }
  }
}

// 使用示例
const client = new WebSocketClient(
  'wss://api.example.com/api/open/chat/customer/ws',
  async () => {
    // Token获取逻辑
    const tokenData = await getVisitorToken();
    return tokenData.access_token;
  }
);

client.connect();



4.5 完整通信流程


┌─────────┐                  ┌─────────┐                  ┌─────────┐
│  客户端  │                  │  服务端  │                  │   AI    │
└────┬────┘                  └────┬────┘                  └────┬────┘
     │                            │                            │
     │ 1. 获取Token(带签名)        │                            │
     ├──────────────────────────>│                            │
     │                            │                            │
     │ 2. 返回access_token        │                            │
     │<──────────────────────────┤                            │
     │                            │                            │
     │ 3. WebSocket连接(带Token)  │                            │
     ├──────────────────────────>│                            │
     │                            │                            │
     │ 4. 连接成功                │                            │
     │<──────────────────────────┤                            │
     │                            │                            │
     │ 5. 发送消息                │                            │
     ├──────────────────────────>│ 6. 转发给AI               │
     │                            ├──────────────────────────>│
     │                            │                            │
     │                            │ 7. AI回复                 │
     │ 8. 推送AI消息              │<──────────────────────────┤
     │<──────────────────────────┤                            │
     │                            │                            │
     │ 9. 心跳ping                │                            │
     ├──────────────────────────>│                            │
     │                            │                            │
     │ 10. 心跳pong               │                            │
     │<──────────────────────────┤                            │
     │                            │                            │



5. 错误码说明


5.1 HTTP状态码


状态码说明
200请求成功
400请求参数错误
401未授权(签名或Token验证失败)
403禁止访问(跨商户访问等)
404资源不存在
500服务器内部错误

5.2 业务错误码


错误码说明解决方案
10001参数缺失检查必填参数是否传递
10002参数格式错误检查参数类型和格式
20001签名验证失败检查签名算法和密钥是否正确
20002签名已过期检查timestamp是否在5分钟内
20003open_app_key无效检查商户凭证是否正确
20004nonce重复使用新的随机字符串
30001Token无效或已过期重新获取Token
30002Token商户不匹配使用正确商户的Token
30003访客信息不存在检查visitor_id是否有效
40001app_key无效检查应用密钥是否正确
40002app_key不属于当前商户使用本商户的app_key
40003用户信息验证失败user_id和phone必须同时传递
40004用户信息不一致同一user_id的phone必须保持一致
50001会话不存在检查session_id是否有效
50002会话已结束该会话已关闭,无法继续操作
60001系统繁忙稍后重试

5.3 错误响应格式


{
  "code": 20001,
  "msg": "签名验证失败: 签名不匹配",
  "data": null
}



6. 完整 SDK 示例


6.1 Java SDK


6.1.1 签名工具类


package com.example.openapi;

import com.alibaba.fastjson.JSON;
import org.apache.commons.codec.digest.DigestUtils;

import java.util.*;

public class OpenAPISignUtil {

    /**
     * 生成签名
     * @param params 请求参数
     * @param secretKey 商户密钥
     * @param signType 签名类型(MD5/SHA256)
     * @return 签名字符串
     */
    public static String generateSign(Map<String, Object> params, String secretKey, String signType) {
        // 1. 添加密钥
        Map<String, Object> signParams = new TreeMap<>(params);
        signParams.put("open_secret_key", secretKey);

        // 2. 按字典序排序并拼接
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> entry : signParams.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();

            // 跳过sign字段
            if ("sign".equals(key) || value == null) {
                continue;
            }

            // 对象类型需要序列化
            String valueStr = (value instanceof String)
                ? (String) value
                : JSON.toJSONString(value);

            if (sb.length() > 0) {
                sb.append("&");
            }
            sb.append(key).append("=").append(valueStr);
        }

        String signStr = sb.toString();

        // 3. 计算签名
        if ("SHA256".equalsIgnoreCase(signType)) {
            return DigestUtils.sha256Hex(signStr);
        } else {
            return DigestUtils.md5Hex(signStr);
        }
    }

    /**
     * 生成随机nonce
     */
    public static String generateNonce() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 获取当前时间戳(秒)
     */
    public static String getTimestamp() {
        return String.valueOf(System.currentTimeMillis() / 1000);
    }
}

6.1.2 开放平台客户端


package com.example.openapi;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import okhttp3.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class OpenAPIClient {

    private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");

    private String baseUrl;
    private String openAppKey;
    private String openSecretKey;
    private OkHttpClient httpClient;

    public OpenAPIClient(String baseUrl, String openAppKey, String openSecretKey) {
        this.baseUrl = baseUrl;
        this.openAppKey = openAppKey;
        this.openSecretKey = openSecretKey;

        this.httpClient = new OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .build();
    }

    /**
     * 获取访客Token
     */
    public JSONObject getVisitorToken(String appKey, String scene, String deviceType,
                                      Map<String, Object> userInfo) throws IOException {
        String url = baseUrl + "/api/open/visitor/token";

        Map<String, Object> params = new HashMap<>();
        params.put("app_key", appKey);
        params.put("scene", scene);
        params.put("device_type", deviceType);
        if (userInfo != null) {
            params.put("user_info", userInfo);
        }

        return postWithSign(url, params, null);
    }

    /**
     * 结束会话
     */
    public JSONObject endSession(String sessionId, String accessToken) throws IOException {
        String url = baseUrl + "/api/open/chat/session/end";

        Map<String, Object> params = new HashMap<>();
        params.put("session_id", sessionId);

        return postWithSign(url, params, accessToken);
    }

    /**
     * 设置会话已解决
     */
    public JSONObject setSessionSolve(String sessionId, int isSolve, String accessToken) throws IOException {
        String url = baseUrl + "/api/open/chat/session/solve";

        Map<String, Object> params = new HashMap<>();
        params.put("session_id", sessionId);
        params.put("is_solve", isSolve);

        return postWithSign(url, params, accessToken);
    }

    /**
     * 会话评价
     */
    public JSONObject rateSession(String sessionId, int rating, String comment,
                                  String[] tags, String accessToken) throws IOException {
        String url = baseUrl + "/api/open/chat/session/rating";

        Map<String, Object> params = new HashMap<>();
        params.put("session_id", sessionId);
        params.put("rating", rating);
        if (comment != null) {
            params.put("comment", comment);
        }
        if (tags != null) {
            params.put("tags", tags);
        }

        return postWithSign(url, params, accessToken);
    }

    /**
     * 标记消息已读
     */
    public JSONObject markMessageRead(String messageId, String accessToken) throws IOException {
        String url = baseUrl + "/api/open/chat/mark_read";

        Map<String, Object> params = new HashMap<>();
        params.put("message_id", messageId);

        return postWithSign(url, params, accessToken);
    }

    /**
     * 获取常见问题
     */
    public JSONObject getCommonQuestions(String accessToken) throws IOException {
        String url = baseUrl + "/api/open/chat/questions?access_token=" + accessToken;

        Map<String, Object> params = new HashMap<>();

        return getWithSign(url, params);
    }

    /**
     * POST请求(带签名)
     */
    private JSONObject postWithSign(String url, Map<String, Object> params, String accessToken) throws IOException {
        // 生成签名参数
        String timestamp = OpenAPISignUtil.getTimestamp();
        String nonce = OpenAPISignUtil.generateNonce();

        // 签名计算(包含body参数)
        Map<String, Object> signParams = new HashMap<>(params);
        signParams.put("timestamp", timestamp);
        signParams.put("nonce", nonce);
        String sign = OpenAPISignUtil.generateSign(signParams, openSecretKey, "MD5");

        // 构建请求
        RequestBody body = RequestBody.create(JSON.toJSONString(params), JSON_TYPE);
        Request.Builder builder = new Request.Builder()
            .url(url)
            .post(body)
            .addHeader("Content-Type", "application/json")
            .addHeader("open_app_key", openAppKey)
            .addHeader("timestamp", timestamp)
            .addHeader("nonce", nonce)
            .addHeader("sign", sign)
            .addHeader("sign_type", "MD5");

        if (accessToken != null) {
            builder.addHeader("Authorization", "Bearer " + accessToken);
        }

        Request request = builder.build();

        try (Response response = httpClient.newCall(request).execute()) {
            String responseBody = response.body().string();
            return JSON.parseObject(responseBody);
        }
    }

    /**
     * GET请求(带签名)
     */
    private JSONObject getWithSign(String url, Map<String, Object> params) throws IOException {
        // 生成签名参数
        String timestamp = OpenAPISignUtil.getTimestamp();
        String nonce = OpenAPISignUtil.generateNonce();

        Map<String, Object> signParams = new HashMap<>(params);
        signParams.put("timestamp", timestamp);
        signParams.put("nonce", nonce);
        String sign = OpenAPISignUtil.generateSign(signParams, openSecretKey, "MD5");

        // 构建请求
        Request request = new Request.Builder()
            .url(url)
            .get()
            .addHeader("open_app_key", openAppKey)
            .addHeader("timestamp", timestamp)
            .addHeader("nonce", nonce)
            .addHeader("sign", sign)
            .addHeader("sign_type", "MD5")
            .build();

        try (Response response = httpClient.newCall(request).execute()) {
            String responseBody = response.body().string();
            return JSON.parseObject(responseBody);
        }
    }
}

6.1.3 使用示例


public class Main {
    public static void main(String[] args) {
        // 初始化客户端
        OpenAPIClient client = new OpenAPIClient(
            "https://api.example.com",
            "MERCHANT_123_abc456",
            "SECRET_xyz789uvw456rst123"
        );

        try {
            // 1. 获取Token(匿名模式)
            JSONObject tokenData = client.getVisitorToken(
                "app_abc123",
                "web",
                "pc",
                null
            );
            System.out.println("Token: " + tokenData);

            String accessToken = tokenData.getJSONObject("data").getString("access_token");

            // 2. 获取常见问题
            JSONObject questions = client.getCommonQuestions(accessToken);
            System.out.println("常见问题: " + questions);

            // 3. 结束会话
            JSONObject endResult = client.endSession("session_123", accessToken);
            System.out.println("结束会话: " + endResult);

            // 4. 会话评价
            JSONObject rateResult = client.rateSession(
                "session_123",
                5,
                "服务很好",
                new String[]{"响应及时", "专业"},
                accessToken
            );
            System.out.println("评价结果: " + rateResult);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}



6.2 PHP SDK


6.2.1 签名工具类


<?php

class OpenAPISignUtil {

    /**
     * 生成签名
     * @param array $params 请求参数
     * @param string $secretKey 商户密钥
     * @param string $signType 签名类型(MD5/SHA256)
     * @return string 签名字符串
     */
    public static function generateSign($params, $secretKey, $signType = 'MD5') {
        // 1. 添加密钥
        $params['open_secret_key'] = $secretKey;

        // 2. 移除sign字段和空值
        unset($params['sign']);
        $params = array_filter($params, function($value) {
            return $value !== null && $value !== '';
        });

        // 3. 按key排序
        ksort($params);

        // 4. 拼接字符串
        $signStr = '';
        foreach ($params as $key => $value) {
            // 数组或对象需要JSON序列化
            if (is_array($value) || is_object($value)) {
                $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            }
            $signStr .= $key . '=' . $value . '&';
        }
        $signStr = rtrim($signStr, '&');

        // 5. 计算签名
        if (strtoupper($signType) === 'SHA256') {
            return hash('sha256', $signStr);
        } else {
            return md5($signStr);
        }
    }

    /**
     * 生成随机nonce
     */
    public static function generateNonce() {
        return md5(uniqid(mt_rand(), true));
    }

    /**
     * 获取当前时间戳(秒)
     */
    public static function getTimestamp() {
        return (string)time();
    }
}

6.2.2 开放平台客户端


<?php

class OpenAPIClient {

    private $baseUrl;
    private $openAppKey;
    private $openSecretKey;

    public function __construct($baseUrl, $openAppKey, $openSecretKey) {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->openAppKey = $openAppKey;
        $this->openSecretKey = $openSecretKey;
    }

    /**
     * 获取访客Token
     */
    public function getVisitorToken($appKey, $scene = 'web', $deviceType = 'pc', $userInfo = null) {
        $url = $this->baseUrl . '/api/open/visitor/token';

        $params = [
            'app_key' => $appKey,
            'scene' => $scene,
            'device_type' => $deviceType,
        ];

        if ($userInfo !== null) {
            $params['user_info'] = $userInfo;
        }

        return $this->postWithSign($url, $params);
    }

    /**
     * 结束会话
     */
    public function endSession($sessionId, $accessToken) {
        $url = $this->baseUrl . '/api/open/chat/session/end';

        $params = [
            'session_id' => $sessionId,
        ];

        return $this->postWithSign($url, $params, $accessToken);
    }

    /**
     * 设置会话已解决
     */
    public function setSessionSolve($sessionId, $isSolve, $accessToken) {
        $url = $this->baseUrl . '/api/open/chat/session/solve';

        $params = [
            'session_id' => $sessionId,
            'is_solve' => $isSolve,
        ];

        return $this->postWithSign($url, $params, $accessToken);
    }

    /**
     * 会话评价
     */
    public function rateSession($sessionId, $rating, $comment = null, $tags = null, $accessToken) {
        $url = $this->baseUrl . '/api/open/chat/session/rating';

        $params = [
            'session_id' => $sessionId,
            'rating' => $rating,
        ];

        if ($comment !== null) {
            $params['comment'] = $comment;
        }
        if ($tags !== null) {
            $params['tags'] = $tags;
        }

        return $this->postWithSign($url, $params, $accessToken);
    }

    /**
     * 标记消息已读
     */
    public function markMessageRead($messageId, $accessToken) {
        $url = $this->baseUrl . '/api/open/chat/mark_read';

        $params = [
            'message_id' => $messageId,
        ];

        return $this->postWithSign($url, $params, $accessToken);
    }

    /**
     * 获取常见问题
     */
    public function getCommonQuestions($accessToken) {
        $url = $this->baseUrl . '/api/open/chat/questions?access_token=' . $accessToken;

        return $this->getWithSign($url, []);
    }

    /**
     * POST请求(带签名)
     */
    private function postWithSign($url, $params, $accessToken = null) {
        // 生成签名参数
        $timestamp = OpenAPISignUtil::getTimestamp();
        $nonce = OpenAPISignUtil::generateNonce();

        // 签名计算(包含body参数)
        $signParams = array_merge($params, [
            'timestamp' => $timestamp,
            'nonce' => $nonce,
        ]);
        $sign = OpenAPISignUtil::generateSign($signParams, $this->openSecretKey, 'MD5');

        // 构建请求头
        $headers = [
            'Content-Type' => 'application/json',
            'open_app_key' => $this->openAppKey,
            'timestamp' => $timestamp,
            'nonce' => $nonce,
            'sign' => $sign,
            'sign_type' => 'MD5',
        ];

        if ($accessToken !== null) {
            $headers['Authorization'] = 'Bearer ' . $accessToken;
        }

        // 发送请求
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new Exception("HTTP Error: " . $httpCode);
        }

        return json_decode($response, true);
    }

    /**
     * GET请求(带签名)
     */
    private function getWithSign($url, $params) {
        // 生成签名参数
        $timestamp = OpenAPISignUtil::getTimestamp();
        $nonce = OpenAPISignUtil::generateNonce();

        $signParams = array_merge($params, [
            'timestamp' => $timestamp,
            'nonce' => $nonce,
        ]);
        $sign = OpenAPISignUtil::generateSign($signParams, $this->openSecretKey, 'MD5');

        // 构建请求头
        $headers = [
            'open_app_key' => $this->openAppKey,
            'timestamp' => $timestamp,
            'nonce' => $nonce,
            'sign' => $sign,
            'sign_type' => 'MD5',
        ];

        // 发送请求
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new Exception("HTTP Error: " . $httpCode);
        }

        return json_decode($response, true);
    }
}

6.2.3 使用示例


<?php

// 初始化客户端
$client = new OpenAPIClient(
    'https://api.example.com',
    'MERCHANT_123_abc456',
    'SECRET_xyz789uvw456rst123'
);

try {
    // 1. 获取Token(登录用户模式)
    $tokenData = $client->getVisitorToken(
        'app_abc123',
        'web',
        'pc',
        [
            'user_id' => 'customer_001',
            'phone' => '13800138000',
            'name' => '张三',
        ]
    );
    echo "Token: " . json_encode($tokenData, JSON_UNESCAPED_UNICODE) . "\n";

    $accessToken = $tokenData['data']['access_token'];

    // 2. 获取常见问题
    $questions = $client->getCommonQuestions($accessToken);
    echo "常见问题: " . json_encode($questions, JSON_UNESCAPED_UNICODE) . "\n";

    // 3. 结束会话
    $endResult = $client->endSession('session_123', $accessToken);
    echo "结束会话: " . json_encode($endResult, JSON_UNESCAPED_UNICODE) . "\n";

    // 4. 会话评价
    $rateResult = $client->rateSession(
        'session_123',
        5,
        '服务很好',
        ['响应及时', '专业'],
        $accessToken
    );
    echo "评价结果: " . json_encode($rateResult, JSON_UNESCAPED_UNICODE) . "\n";

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}



6.3 Python SDK


6.3.1 完整SDK实现


import hashlib
import json
import time
import uuid
import requests
from typing import Dict, Any, Optional, List


class OpenAPISignUtil:
    """签名工具类"""

    @staticmethod
    def generate_sign(params: Dict[str, Any], secret_key: str, sign_type: str = 'MD5') -> str:
        """生成签名"""
        # 1. 添加密钥
        sign_params = dict(params)
        sign_params['open_secret_key'] = secret_key

        # 2. 移除sign字段和空值
        sign_params.pop('sign', None)
        sign_params = {k: v for k, v in sign_params.items() if v is not None}

        # 3. 按key排序
        sorted_keys = sorted(sign_params.keys())

        # 4. 拼接字符串
        sign_str = '&'.join([
            f"{k}={json.dumps(sign_params[k], ensure_ascii=False, separators=(',', ':')) if isinstance(sign_params[k], (dict, list)) else sign_params[k]}"
            for k in sorted_keys
        ])
        sign_str += f"&key={secret_key}"

        print(f"待签名字符串: {sign_str}")

        # 5. MD5加密并转大写
        sign = hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
        return sign

    @staticmethod
    def generate_nonce() -> str:
        """生成随机nonce"""
        return uuid.uuid4().hex

    @staticmethod
    def get_timestamp() -> str:
        """获取当前时间戳(秒)"""
        return str(int(time.time()))


class OpenAPIClient:
    """开放平台客户端"""

    def __init__(self, base_url: str, open_app_key: str, open_secret_key: str):
        self.base_url = base_url.rstrip('/')
        self.open_app_key = open_app_key
        self.open_secret_key = open_secret_key
        self.session = requests.Session()
        self.session.headers.update({'Content-Type': 'application/json'})

    def get_visitor_token(self, app_key: str, scene: str = 'web',
                         device_type: str = 'pc', user_info: Optional[Dict] = None) -> Dict:
        """获取访客Token"""
        url = f"{self.base_url}/api/open/visitor/token"

        params = {
            'app_key': app_key,
            'scene': scene,
            'device_type': device_type,
        }

        if user_info:
            params['user_info'] = user_info

        return self._post_with_sign(url, params)

    def end_session(self, session_id: str, access_token: str) -> Dict:
        """结束会话"""
        url = f"{self.base_url}/api/open/chat/session/end"

        params = {'session_id': session_id}

        return self._post_with_sign(url, params, access_token)

    def set_session_solve(self, session_id: str, is_solve: int, access_token: str) -> Dict:
        """设置会话已解决"""
        url = f"{self.base_url}/api/open/chat/session/solve"

        params = {
            'session_id' => session_id,
            'is_solve' => is_solve,
        }

        return self._post_with_sign(url, params, access_token)

    def rate_session(self, session_id: str, rating: int, comment: Optional[str] = None,
                    tags: Optional[List[str]] = None, access_token: str = None) -> Dict:
        """会话评价"""
        url = f"{self.base_url}/api/open/chat/session/rating"

        params = {
            'session_id' => session_id,
            'rating' => rating,
        }

        if comment:
            params['comment'] = comment
        if tags:
            params['tags'] = tags

        return self._post_with_sign(url, params, access_token)

    def mark_message_read(self, message_id: str, access_token: str) -> Dict:
        """标记消息已读"""
        url = f"{self.base_url}/api/open/chat/mark_read"

        params = {'message_id' => messageId}

        return self._post_with_sign(url, params, access_token)

    def get_common_questions(self, access_token: str) -> Dict:
        """获取常见问题"""
        url = f"{self.base_url}/api/open/chat/questions?access_token={access_token}"

        return self._get_with_sign(url, {})

    def _post_with_sign(self, url: str, params: Dict, access_token: Optional[str] = None) -> Dict:
        """POST请求(带签名)"""
        # 生成签名参数
        timestamp = OpenAPISignUtil.get_timestamp()
        nonce = OpenAPISignUtil.generate_nonce()

        # 签名计算(包含body参数)
        sign_params = dict(params)
        sign_params.update({
            'timestamp' => timestamp,
            'nonce' => nonce,
        })
        sign = OpenAPISignUtil.generate_sign(sign_params, $this->openSecretKey, 'MD5');

        # 构建请求头
        headers = {
            'Content-Type': 'application/json',
            'open_app_key': $this->openAppKey,
            'timestamp': timestamp,
            'nonce': nonce,
            'sign': sign,
            'sign_type': 'MD5',
        };

        if (access_token !== null) {
            headers['Authorization'] = 'Bearer ' . access_token;
        }

        # 发送请求
        response = await this.httpClient.post(url, { headers });
        return response.data;
  }
}

// 使用示例
async function main() {
  // 初始化客户端
  const client = new OpenAPIClient(
    'https://api.example.com',
    'MERCHANT_123_abc456',
    'SECRET_xyz789uvw456rst123'
  );

  try {
    // 1. 获取Token(登录用户模式)
    const tokenData = await client.getVisitorToken(
      'app_abc123',
      'web',
      'pc',
      {
        user_id: 'customer_001',
        phone: '13800138000',
        name: '张三',
      }
    );
    console.log('Token:', JSON.stringify(tokenData, null, 2));

    const accessToken = tokenData.data.access_token;

    // 2. 获取常见问题
    const questions = await client.getCommonQuestions(accessToken);
    console.log('常见问题:', JSON.stringify(questions, null, 2));

    // 3. 结束会话
    const endResult = await client.endSession('session_123', accessToken);
    console.log('结束会话:', JSON.stringify(endResult, null, 2));

    // 4. 会话评价
    const rateResult = await client.rateSession(
      'session_123',
      5,
      '服务很好',
      ['响应及时', '专业'],
      accessToken
    );
    console.log('评价结果:', JSON.stringify(rateResult, null, 2));

  } catch (error) {
    console.error('错误:', error.message);
  }
}

// 导出模块
module.exports = {
  OpenAPISignUtil,
  OpenAPIClient,
};

// 如果直接运行此文件
if (require.main === module) {
  main();
}



6.4 Node.js SDK


6.4.1 完整SDK实现


const crypto = require('crypto');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');

class OpenAPISignUtil {
  /**
   * 生成签名
   */
  static generateSign(params, secretKey, signType = 'MD5') {
    // 1. 添加密钥
    const signParams = { ...params, open_secret_key: secretKey };

    // 2. 移除sign字段和空值
    delete signParams.sign;
    Object.keys(signParams).forEach(key => {
      if (signParams[key] === null || signParams[key] === undefined) {
        delete signParams[key];
      }
    });

    // 3. 按key排序
    const sortedKeys = Object.keys(signParams).sort();

    // 4. 拼接字符串
    const signStr = sortedKeys.map(key => {
      const value = signParams[key];
      const valueStr = (typeof value === 'object')
        ? JSON.stringify(value)
        : String(value);
      return `${key}=${valueStr}`;
    }).join('&');

    // 5. 计算签名
    if (signType.toUpperCase() === 'SHA256') {
      return crypto.createHash('sha256').update(signStr).digest('hex');
    } else {
      return crypto.createHash('md5').update(signStr).digest('hex');
    }
  }

  /**
   * 生成随机nonce
   */
  static generateNonce() {
    return uuidv4().replace(/-/g, '');
  }

  /**
   * 获取当前时间戳(秒)
   */
  static getTimestamp() {
    return String(Math.floor(Date.now() / 1000));
  }
}

class OpenAPIClient {
  constructor(baseUrl, openAppKey, openSecretKey) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.openAppKey = openAppKey;
    this.openSecretKey = openSecretKey;

    this.httpClient = axios.create({
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }

  /**
   * 获取访客Token
   */
  async getVisitorToken(appKey, scene = 'web', deviceType = 'pc', userInfo = null) {
    const url = `${this.baseUrl}/api/open/visitor/token`;

    const params = {
      app_key: appKey,
      scene,
      device_type: deviceType,
    };

    if (userInfo) {
      params.user_info = userInfo;
    }

    return await this._postWithSign(url, params);
  }

  /**
   * 结束会话
   */
  async endSession(sessionId, accessToken) {
    const url = `${this.baseUrl}/api/open/chat/session/end`;

    const params = { session_id: sessionId };

    return await this._postWithSign(url, params, accessToken);
  }

  /**
   * 设置会话已解决
   */
  async setSessionSolve(sessionId, isSolve, accessToken) {
    const url = `${this.baseUrl}/api/open/chat/session/solve`;

    const params = {
      session_id: sessionId,
      is_solve: isSolve,
    };

    return await this._postWithSign(url, params, accessToken);
  }

  /**
   * 会话评价
   */
  async rateSession(sessionId, rating, comment = null, tags = null, accessToken) {
    const url = `${this.baseUrl}/api/open/chat/session/rating`;

    const params = {
      session_id: sessionId,
      rating,
    };

    if (comment) params.comment = comment;
    if (tags) params.tags = tags;

    return await this._postWithSign(url, params, accessToken);
  }

  /**
   * 标记消息已读
   */
  async markMessageRead(messageId, accessToken) {
    const url = `${this.baseUrl}/api/open/chat/mark_read`;

    const params = { message_id: messageId };

    return await this._postWithSign(url, params, accessToken);
  }

  /**
   * 获取常见问题
   */
  async getCommonQuestions(accessToken) {
    const url = `${this.baseUrl}/api/open/chat/questions?access_token=${accessToken}`;

    return await this._getWithSign(url, {});
  }

  /**
   * POST请求(带签名)
   */
  async _postWithSign(url, params, accessToken = null) {
    // 生成签名参数
    const timestamp = OpenAPISignUtil.getTimestamp();
    const nonce = OpenAPISignUtil.generateNonce();

    // 签名计算(包含body参数)
    const signParams = {
      ...params,
      timestamp,
      nonce,
    };
    const sign = OpenAPISignUtil.generateSign(signParams, this.openSecretKey, 'MD5');

    // 构建请求头
    const headers = {
      'Content-Type': 'application/json',
      'open_app_key': this.openAppKey,
      'timestamp': timestamp,
      'nonce': nonce,
      'sign': sign,
      'sign_type': 'MD5',
    };

    if (accessToken) {
      headers['Authorization'] = `Bearer ${accessToken}`;
    }

    // 发送请求
    const response = await this.httpClient.post(url, params, { headers });
    return response.data;
  }

  /**
   * GET请求(带签名)
   */
  async _getWithSign(url, params) {
    // 生成签名参数
    const timestamp = OpenAPISignUtil.getTimestamp();
    const nonce = OpenAPISignUtil.generateNonce();

    const signParams = {
      ...params,
      timestamp,
      nonce,
    };
    const sign = OpenAPISignUtil.generateSign(signParams, this.openSecretKey, 'MD5');

    // 构建请求头
    const headers = {
      'open_app_key': this.openAppKey,
      'timestamp': timestamp,
      'nonce': nonce,
      'sign': sign,
      'sign_type': 'MD5',
    };

    // 发送请求
    const response = await this.httpClient.get(url, { headers });
    return response.data;
  }
}

// 使用示例
async function main() {
  // 初始化客户端
  const client = new OpenAPIClient(
    'https://api.example.com',
    'MERCHANT_123_abc456',
    'SECRET_xyz789uvw456rst123'
  );

  try {
    // 1. 获取Token(登录用户模式)
    const tokenData = await client.getVisitorToken(
      'app_abc123',
      'web',
      'pc',
      {
        user_id: 'customer_001',
        phone: '13800138000',
        name: '张三',
      }
    );
    console.log('Token:', JSON.stringify(tokenData, null, 2));

    const accessToken = tokenData.data.access_token;

    // 2. 获取常见问题
    const questions = await client.getCommonQuestions(accessToken);
    console.log('常见问题:', JSON.stringify(questions, null, 2));

    // 3. 结束会话
    const endResult = await client.endSession('session_123', accessToken);
    console.log('结束会话:', JSON.stringify(endResult, null, 2));

    // 4. 会话评价
    const rateResult = await client.rateSession(
      'session_123',
      5,
      '服务很好',
      ['响应及时', '专业'],
      accessToken
    );
    console.log('评价结果:', JSON.stringify(rateResult, null, 2));

  } catch (error) {
    console.error('错误:', error.message);
  }
}

// 导出模块
module.exports = {
  OpenAPISignUtil,
  OpenAPIClient,
};

// 如果直接运行此文件
if (require.main === module) {
  main();
}



7. 最佳实践


7.1 安全建议


7.1.1 密钥管理


// ❌ 错误示例:硬编码密钥
const client = new OpenAPIClient(
  'https://api.example.com',
  'MERCHANT_123_abc456',
  'SECRET_xyz789uvw456rst123' // 危险!
);

// ✅ 正确示例:从环境变量读取
const client = new OpenAPIClient(
  process.env.API_BASE_URL,
  process.env.OPEN_APP_KEY,
  process.env.OPEN_SECRET_KEY
);

密钥安全原则:

  • ✅ 使用环境变量或配置中心管理密钥
  • ✅ 不要将密钥提交到代码仓库
  • ✅ 定期轮换密钥(建议每3-6个月)
  • ✅ 不同环境使用不同的密钥
  • ✅ 限制密钥的访问权限

7.1.2 HTTPS通信


// ✅ 生产环境必须使用HTTPS
const client = new OpenAPIClient(
  'https://api.example.com', // 使用HTTPS
  openAppKey,
  openSecretKey
);

通信安全原则:

  • ✅ 生产环境强制使用HTTPS
  • ✅ 验证SSL证书有效性
  • ✅ 避免在URL中传递敏感信息
  • ✅ 使用最新的TLS版本(TLS 1.2+)

7.1.3 签名验证


# ✅ 每次请求都生成新的nonce和timestamp
def make_request():
    timestamp = str(int(time.time()))
    nonce = uuid.uuid4().hex  # 每次都是新的随机值

    # 签名计算...



签名安全原则:

  • ✅ 每次请求使用新的nonce(防重放攻击)
  • ✅ 检查时间戳有效性(±5分钟)
  • ✅ 签名计算在服务端完成,不要在客户端暴露密钥
  • ✅ 记录签名失败日志,及时发现异常


7.2 性能优化


7.2.1 Token复用


class TokenManager {
  constructor(client, appKey) {
    this.client = client;
    this.appKey = appKey;
    this.tokenCache = null;
    this.expireTime = 0;
  }

  async getToken() {
    const now = Date.now() / 1000;

    // Token未过期,直接返回
    if (this.tokenCache && now < this.expireTime - 60) {
      return this.tokenCache;
    }

    // Token过期或不存在,重新获取
    const tokenData = await this.client.getVisitorToken(this.appKey);
    this.tokenCache = tokenData.data.access_token;
    this.expireTime = now + tokenData.data.expires_in;

    return this.tokenCache;
  }
}

// 使用示例
const tokenManager = new TokenManager(client, 'app_abc123');

// 多次调用会复用Token
const token1 = await tokenManager.getToken(); // 首次获取
const token2 = await tokenManager.getToken(); // 复用缓存
const token3 = await tokenManager.getToken(); // 复用缓存

Token管理原则:

  • ✅ Token有效期内复用,避免频繁请求
  • ✅ 提前刷新Token(过期前60秒刷新)
  • ✅ 缓存Token到内存或Redis
  • ✅ 多进程/多线程共享Token缓存

7.2.2 连接池复用


import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

class OpenAPIClient:
    def __init__(self, base_url, open_app_key, open_secret_key):
        self.base_url = base_url
        self.open_app_key = open_app_key
        self.open_secret_key = open_secret_key

        # 配置连接池和重试策略
        self.session = requests.Session()
        retry_strategy = Retry(
            total=3,  # 最多重试3次
            backoff_factor=1,  # 重试间隔:1s, 2s, 4s
            status_forcelist=[500, 502, 503, 504]  # 这些状态码自动重试
        )
        adapter = HTTPAdapter(
            pool_connections=10,  # 连接池大小
            pool_maxsize=20,      # 最大连接数
            max_retries=retry_strategy
        )
        self.session.mount('http://', adapter)
        self.session.mount('https://', adapter)

连接优化原则:

  • ✅ 使用HTTP连接池,避免频繁建立连接
  • ✅ 配置合理的超时时间(连接超时10s,读取超时30s)
  • ✅ 实现自动重试机制(5xx错误重试)
  • ✅ 使用Keep-Alive保持长连接

7.2.3 批量操作


// ✅ 批量标记消息已读(如果接口支持)
async function markMultipleMessagesRead(messageIds, accessToken) {
  const promises = messageIds.map(msgId =>
    client.markMessageRead(msgId, accessToken)
  );

  // 并发执行,但控制并发数量
  const results = await Promise.all(promises);
  return results;
}

// 控制并发数量的版本
async function markMessagesWithLimit(messageIds, accessToken, limit = 5) {
  const results = [];

  for (let i = 0; i < messageIds.length; i += limit) {
    const batch = messageIds.slice(i, i + limit);
    const batchResults = await Promise.all(
      batch.map(msgId => client.markMessageRead(msgId, accessToken))
    );
    results.push(...batchResults);
  }

  return results;
}

批量操作原则:

  • ✅ 合并多个请求,减少网络开销
  • ✅ 控制并发数量,避免服务端过载
  • ✅ 实现请求队列,平滑流量
  • ✅ 使用异步处理,提高吞吐量


7.3 错误处理


7.3.1 错误分类处理


async function callAPIWithErrorHandling(apiFunc) {
  try {
    const result = await apiFunc();
    return { success: true, data: result };

  } catch (error) {
    // 网络错误
    if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
      console.error('网络连接失败,请检查网络或稍后重试');
      return { success: false, error: 'NETWORK_ERROR', retry: true };
    }

    // HTTP错误
    if (error.response) {
      const { status, data } = error.response;

      // 401 未授权:Token过期或签名错误
      if (status === 401) {
        console.error('认证失败:', data.msg);
        // 清除Token缓存,重新获取
        tokenManager.clearToken();
        return { success: false, error: 'AUTH_ERROR', retry: true };
      }

      // 403 禁止访问:跨商户访问等
      if (status === 403) {
        console.error('禁止访问:', data.msg);
        return { success: false, error: 'FORBIDDEN', retry: false };
      }

      // 500 服务器错误
      if (status >= 500) {
        console.error('服务器错误:', data.msg);
        return { success: false, error: 'SERVER_ERROR', retry: true };
      }
    }

    // 业务错误
    if (error.response && error.response.data) {
      const { code, msg } = error.response.data;
      console.error(`业务错误[${code}]: ${msg}`);
      return { success: false, error: 'BUSINESS_ERROR', code, msg };
    }

    // 未知错误
    console.error('未知错误:', error);
    return { success: false, error: 'UNKNOWN_ERROR' };
  }
}

// 使用示例
const result = await callAPIWithErrorHandling(async () => {
  return await client.getCommonQuestions(accessToken);
});

if (result.success) {
  console.log('成功:', result.data);
} else if (result.retry) {
  console.log('错误可重试:', result.error);
  // 实现重试逻辑...
} else {
  console.log('错误不可重试:', result.error);
}

7.3.2 自动重试机制


import time
from functools import wraps

def retry_on_error(max_retries=3, delay=1, backoff=2):
    """错误重试装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            current_delay = delay

            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    retries += 1

                    # 判断是否可重试
                    if not is_retryable_error(e):
                        raise

                    if retries >= max_retries:
                        print(f"重试{max_retries}次后仍失败")
                        raise

                    print(f"请求失败,{current_delay}秒后重试({retries}/{max_retries})")
                    time.sleep(current_delay)
                    current_delay *= backoff  # 指数退避

        return wrapper
    return decorator

def is_retryable_error(error):
    """判断错误是否可重试"""
    # 网络错误可重试
    if isinstance(error, (ConnectionError, TimeoutError)):
        return True

    # HTTP 5xx错误可重试
    if hasattr(error, 'response') and error.response:
        status = error.response.status_code
        if 500 <= status < 600:
            return True

    # 401错误(Token过期)可重试
    if hasattr(error, 'response') and error.response:
        if error.response.status_code == 401:
            return True

    return False

# 使用示例
@retry_on_error(max_retries=3, delay=1, backoff=2)
def call_api():
    return client.get_common_questions(access_token)

# 调用会自动重试
result = call_api()

重试策略原则:

  • ✅ 只重试可恢复的错误(网络错误、5xx错误、401错误)
  • ✅ 使用指数退避算法(1s, 2s, 4s, 8s...)
  • ✅ 设置最大重试次数(建议3-5次)
  • ✅ 记录重试日志,便于问题排查


7.4 监控与日志


7.4.1 请求日志记录


class OpenAPIClient {
  async request(method, url, data, headers) {
    const requestId = generateRequestId();
    const startTime = Date.now();

    // 记录请求日志
    console.log(`[${requestId}] 请求开始`, {
      method,
      url,
      headers: this.maskSensitiveData(headers),
      body: this.maskSensitiveData(data),
      timestamp: new Date().toISOString()
    });

    try {
      const response = await axios({ method, url, data, headers });
      const duration = Date.now() - startTime;

      // 记录响应日志
      console.log(`[${requestId}] 请求成功`, {
        status: response.status,
        duration: `${duration}ms`,
        dataSize: JSON.stringify(response.data).length
      });

      return response;

    } catch (error) {
      const duration = Date.now() - startTime;

      // 记录错误日志
      console.error(`[${requestId}] 请求失败`, {
        status: error.response?.status,
        duration: `${duration}ms`,
        error: error.message,
        stack: error.stack
      });

      throw error;
    }
  }

  maskSensitiveData(data) {
    // 脱敏处理:隐藏密钥、Token等敏感信息
    const masked = { ...data };
    if (masked.open_secret_key) masked.open_secret_key = '***';
    if (masked.access_token) masked.access_token = masked.access_token.substring(0, 10) + '...';
    return masked;
  }
}

日志记录原则:

  • ✅ 记录每个请求的关键信息(URL、参数、响应时间)
  • ✅ 敏感信息脱敏(密钥、Token等)
  • ✅ 使用唯一请求ID关联日志
  • ✅ 记录错误堆栈,便于排查问题
  • ✅ 使用结构化日志格式(JSON)

7.4.2 性能监控


import time
from functools import wraps

class PerformanceMonitor:
    def __init__(self):
        self.metrics = {
            'total_requests': 0,
            'success_requests': 0,
            'failed_requests': 0,
            'total_duration': 0,
            'api_stats': {}
        }

    def record_request(self, api_name, duration, success):
        self.metrics['total_requests'] += 1
        self.metrics['total_duration'] += duration

        if success:
            self.metrics['success_requests'] += 1
        else:
            self.metrics['failed_requests'] += 1

        # 记录各API的统计
        if api_name not in self.metrics['api_stats']:
            self.metrics['api_stats'][api_name] = {
                'count': 0,
                'total_duration': 0,
                'success': 0,
                'failed': 0
            }

        stats = self.metrics['api_stats'][api_name]
        stats['count'] += 1
        stats['total_duration'] += duration
        if success:
            stats['success'] += 1
        else:
            stats['failed'] += 1

    def get_report(self):
        total = self.metrics['total_requests']
        if total == 0:
            return "暂无数据"

        avg_duration = self.metrics['total_duration'] / total
        success_rate = self.metrics['success_requests'] / total * 100;

        report = f"""
性能监控报告
========================
总请求数: {total}
成功请求: {self.metrics['success_requests']}
失败请求: {self.metrics['failed_requests']}
成功率: {success_rate:.2f}%
平均响应时间: {avg_duration:.2f}ms

各API统计:
"""
        for api_name, stats in self.metrics['api_stats'].items():
            avg = stats['total_duration'] / stats['count']
            rate = stats['success'] / stats['count'] * 100
            report += f"\n  {api_name}:":
            report += f"\n    调用次数: {stats['count']}"
            report += f"\n    平均耗时: {avg:.2f}ms"
            report += f"\n    成功率: {rate:.2f}%"

        return report

# 全局监控器
monitor = PerformanceMonitor()

def monitor_performance(api_name):
    """性能监控装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start_time = time.time()
            success = False

            try:
                result = func(*args, **kwargs)
                success = True
                return result
            finally:
                duration = (time.time() - start_time) * 1000  # 毫秒
                monitor.record_request(api_name, duration, success)

        return wrapper
    return decorator

# 使用示例
@monitor_performance('get_visitor_token')
def get_visitor_token(app_key):
    return client.get_visitor_token(app_key)

# 定期输出监控报告
print(monitor.get_report())

监控指标建议:

  • ✅ 请求总数、成功数、失败数
  • ✅ 平均响应时间、P95、P99响应时间
  • ✅ 各API的调用频率和成功率
  • ✅ 错误类型分布
  • ✅ Token刷新频率


8. 常见问题


8.1 认证相关


Q1: 签名验证失败,提示"签名不匹配"?


原因分析:

  • 签名算法实现错误
  • 参数编码问题(UTF-8)
  • 参数排序错误
  • 密钥配置错误

解决方案:


// ✅ 正确的签名计算步骤
function generateSign(params, secretKey) {
  // 1. 移除sign字段
  delete params.sign;

  // 2. 按key升序排序
  const sortedKeys = Object.keys(params).sort();

  // 3. 拼接字符串: key1=value1&key2=value2&key=secretKey
  let signStr = sortedKeys
    .map(key => `${key}=${params[key]}`)
    .join('&') + `&key=${secretKey}`;

  console.log('待签名字符串:', signStr);

  // 4. MD5加密并转大写
  const sign = crypto.createHash('md5')
    .update(signStr, 'utf8')
    .digest('hex')
    .toUpperCase();

  return sign;
}

// 调试技巧:打印待签名字符串
console.log('待签名字符串:', signStr);
console.log('计算的签名:', sign);

排查步骤:

1. 对比服务端和客户端的待签名字符串是否一致

2. 检查密钥是否正确(注意首尾空格)

3. 确认时间戳格式(10位秒级时间戳)

4. 验证nonce是否为有效的随机字符串




Q2: 提示"Token已过期"怎么办?


原因:

  • access_token有效期为30分钟
  • 超时未使用导致过期

解决方案:


class TokenManager {
  async getValidToken() {
    const now = Date.now() / 1000;

    // Token即将过期(提前60秒刷新)
    if (!this.token || now >= this.expireTime - 60) {
      await this.refreshToken();
    }

    return this.token;
  }

  async refreshToken() {
    const tokenData = await client.getVisitorToken(appKey);
    this.token = tokenData.data.access_token;
    this.expireTime = Date.now() / 1000 + tokenData.data.expires_in;
  }
}

最佳实践:

  • ✅ 提前刷新Token(过期前60秒)
  • ✅ 捕获401错误自动重试
  • ✅ 使用refresh_token刷新机制


Q3: 提示"Token商户与签名商户不匹配"?


原因:

  • 使用了其他商户的Token
  • Token和签名来自不同的商户账号

解决方案:

  • 确保Token和签名使用同一个商户的凭证
  • 检查是否混用了测试环境和生产环境的凭证
// ✅ 正确:使用同一商户凭证
const merchantAClient = new OpenAPIClient(
  API_URL,
  'MERCHANT_A_KEY',
  'MERCHANT_A_SECRET'
);

const tokenA = await merchantAClient.getVisitorToken('app_a');
await merchantAClient.endSession(sessionId, tokenA); // ✅ 正确

// ❌ 错误:跨商户使用Token
const merchantBClient = new OpenAPIClient(
  API_URL,
  'MERCHANT_B_KEY',
  'MERCHANT_B_SECRET'
);

await merchantBClient.endSession(sessionId, tokenA); // ❌ 错误:tokenA是商户A的



8.2 WebSocket相关


Q4: WebSocket连接失败怎么办?


常见原因:

1. Token无效或过期

2. 网络问题

3. 协议错误(ws vs wss)

4. 防火墙拦截


解决方案:


// ✅ 健壮的WebSocket连接
class RobustWebSocket {
  connect(url, token) {
    // 1. 确保使用正确的协议
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    const wsUrl = `${protocol}//${API_DOMAIN}/api/open/chat/customer/ws?access_token=${token}`;

    this.ws = new WebSocket(wsUrl);

    this.ws.onopen = () => {
      console.log('✅ WebSocket连接成功');
      this.reconnectAttempts = 0;
      this.reconnectDelay = 1000;
    };

    this.ws.onerror = (error) => {
      console.error('❌ WebSocket错误:', error);
    };

    this.ws.onclose = (event) => {
      console.log('WebSocket关闭:', event.code, event.reason);
      this.reconnect();
    };
  }

  reconnect() {
    if (this.reconnectAttempts >= 5) {
      console.error('重连失败次数过多,停止重连');
      return;
    }

    this.reconnectAttempts++;
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);

    console.log(`${delay}ms后尝试重连(${this.reconnectAttempts}/5)`);

    setTimeout(() => {
      this.connect(this.url, this.token);
    }, delay);
  }
}

排查步骤:

1. 检查Token是否有效:console.log(access_token)

2. 检查WebSocket URL格式是否正确

3. 查看浏览器控制台Network标签的WS连接状态

4. 测试网络连通性:ping api.example.com




Q5: WebSocket消息丢失怎么办?


原因:

  • 网络波动导致连接断开
  • 消息发送时连接未就绪
  • 服务端消息积压

解决方案:


class MessageQueue {
  constructor(ws) {
    this.ws = ws;
    this.queue = [];
    this.isReady = false;

    ws.onopen = () => {
      this.isReady = true;
      this.flushQueue();
    };

    ws.onclose = () => {
      this.isReady = false;
    };
  }

  send(message) {
    if (this.isReady && this.ws.readyState === WebSocket.OPEN) {
      // 连接正常,直接发送
      this.ws.send(JSON.stringify(message));
    } else {
      // 连接未就绪,加入队列
      this.queue.push(message);
      console.log('消息已加入队列,等待连接就绪');
    }
  }

  flushQueue() {
    // 连接就绪后,发送队列中的消息
    while (this.queue.length > 0) {
      const message = this.queue.shift();
      this.ws.send(JSON.stringify(message));
    }
  }
}

// 使用示例
const messageQueue = new MessageQueue(ws);
messageQueue.send({ type: 'text', content: '你好' });

最佳实践:

  • ✅ 检查WebSocket连接状态再发送
  • ✅ 实现消息队列机制
  • ✅ 记录发送失败的消息,待重连后重发
  • ✅ 实现消息确认机制(ACK)


8.3 业务相关


Q6: 如何区分匿名访客和登录用户?


说明:


// 场景1:匿名访客(不传user_info)
const anonymousToken = await client.getVisitorToken({
  app_key: 'app_abc123',
  scene: 'web',
  device_type: 'pc'
  // 不传user_info
});
// 系统会生成visitor_id,用于跟踪同一设备

// 场景2:登录用户(传user_info)
const userToken = await client.getVisitorToken({
  app_key: 'app_abc123',
  scene: 'app',
  device_type: 'mobile',
  user_info: {
    user_id: 'customer_001',    // ⚠️ 必填
    phone: '13800138000',     // ⚠️ 必填
    name: '张三'
  }
});
// 系统会关联真实用户,支持历史查询

区别:

特性匿名访客登录用户
user_info不传必传(user_id+phone)
用户识别visitor_id(设备)user_id(真实用户)
会话历史按设备查询按用户查询
用户画像不支持支持
CRM集成不支持支持



Q7: visitor_id 需要客户端保存吗?


答案: 建议保存


// ✅ 首次获取Token时保存visitor_id
const tokenData = await client.getVisitorToken({ app_key });
const visitorId = tokenData.data.visitor_id;

// 保存到本地存储
localStorage.setItem('visitor_id', visitorId);

// ✅ 下次请求时传递visitor_id
const savedVisitorId = localStorage.getItem('visitor_id');
const newTokenData = await client.getVisitorToken({
  app_key,
  visitor_id: savedVisitorId  // 传递保存的visitor_id
});

好处:

  • ✅ 同一设备的会话历史可关联
  • ✅ 用户体验更好(保留咨询记录)
  • ✅ 数据统计更准确(去重)


Q8: 如何处理会话超时?


场景: 用户长时间未操作,会话可能超时


解决方案:


// 1. 监听会话超时消息
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);

  if (message.type === 'session_timeout') {
    alert('会话已超时,请重新开始咨询');

    // 关闭连接
    ws.close();

    // 重新获取Token
    const newToken = await client.getVisitorToken(appKey);

    // 重新建立连接
    connectWebSocket(newToken.data.access_token);
  }
};

// 2. 实现心跳保活
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 30000); // 每30秒发送一次心跳

最佳实践:

  • ✅ 监听超时消息,及时提示用户
  • ✅ 实现心跳机制,保持连接活跃
  • ✅ 记录会话状态,超时后可恢复


Q9: 如何实现多轮对话?


说明: WebSocket连接保持期间,所有消息自动关联到同一会话


// 建立连接后,持续发送消息即可
ws.onopen = () => {
  // 第1轮对话
  ws.send(JSON.stringify({
    type: 'text',
    content: '我想咨询产品价格'
  }));

  // 收到回复后,继续第2轮
  setTimeout(() => {
    ws.send(JSON.stringify({
      type: 'text',
      content: '有优惠活动吗?'
    }));
  }, 5000);

  // 第3轮...
};

// 服务端会自动识别同一会话的多轮对话
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log('客服回复:', message.content);

  // 显示消息...
};

要点:

  • ✅ 保持WebSocket连接不断开
  • ✅ 多次send()会自动关联到同一会话
  • ✅ 不需要手动传递session_id
  • ✅ 会话结束时主动调用结束接口


8.4 调试技巧


Q10: 如何调试签名问题?


步骤:


# 1. 打印待签名字符串
function debugSign(params, secretKey) {
  delete params.sign;

  const sortedKeys = Object.keys(params).sort();
  const signStr = sortedKeys
    .map(key => `${key}=${params[key]}`)
    .join('&') + `&key=${secretKey}`;

  console.log('=== 签名调试信息 ===');
  console.log('1. 原始参数:', JSON.stringify(params, null, 2));
  console.log('2. 排序后的key:', sortedKeys);
  console.log('3. 待签名字符串:', signStr);

  const sign = crypto.createHash('md5')
    .update(signStr, 'utf8')
    .digest('hex')
    .toUpperCase();

  console.log('4. 计算的签名:', sign);
  console.log('==================');

  return sign;
}

// 2. 使用在线工具验证
// 访问: https://tool.oschina.net/encrypt (MD5加密)
// 输入待签名字符串,选择MD5,对比结果

// 3. 联系技术支持
// 提供调试信息给技术支持,协助排查



Q11: 如何测试WebSocket连接?


方法1: 使用浏览器控制台


// 在浏览器控制台直接测试
const ws = new WebSocket('wss://api.example.com/api/open/chat/customer/ws?access_token=YOUR_TOKEN');

ws.onopen = () => console.log('✅ 连接成功');
ws.onerror = (err) => console.error('❌ 连接错误:', err);
ws.onmessage = (event) => console.log('📩 收到消息:', event.data);

// 发送测试消息
ws.send(JSON.stringify({ type: 'text', content: '测试消息' }));

方法2: 使用Postman

1. 创建WebSocket Request

2. 输入URL: wss://api.example.com/api/open/chat/customer/ws?access_token=YOUR_TOKEN

3. 点击Connect

4. 发送JSON消息测试


方法3: 使用在线工具

  • WebSocket测试工具: http://www.websocket.org/echo.html
  • 或使用: https://websocketking.com/


Q12: 生产环境如何排查问题?


排查清单:


## 1. 检查凭证配置
- [ ] open_app_key是否正确
- [ ] open_secret_key是否正确
- [ ] 环境(测试/生产)是否匹配

## 2. 检查网络连通性
- [ ] 能否访问API域名: `curl https://api.example.com`
- [ ] 防火墙是否拦截
- [ ] 是否配置了IP白名单

## 3. 检查日志
- [ ] 查看客户端请求日志
- [ ] 查看服务端错误日志
- [ ] 查看网络层日志(Nginx/LB)

## 4. 对比测试环境
- [ ] 测试环境是否正常
- [ ] 对比测试和生产的差异
- [ ] 切换到测试环境验证

## 5. 联系技术支持
提供以下信息:
- 商户ID或open_app_key
- 接口名称和调用时间
- 完整的错误信息和请求日志
- 可复现的测试代码



附录


A. 签名算法完整实现


A.1 Java版本


import java.security.MessageDigest;
import java.util.*;

public class SignUtils {
    public static String generateSign(Map<String, String> params, String secretKey) {
        // 1. 移除sign字段
        params.remove("sign");

        // 2. 按key升序排序
        List<String> keys = new ArrayList<>(params.keySet());
        Collections.sort(keys);

        // 3. 拼接字符串
        StringBuilder sb = new StringBuilder();
        for (String key : keys) {
            sb.append(key).append("=").append(params.get(key)).append("&");
        }
        sb.append("key=").append(secretKey);

        String signStr = sb.toString();
        System.out.println("待签名字符串: " + signStr);

        // 4. MD5加密并转大写
        return md5(signStr).toUpperCase();
    }

    private static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(input.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

A.2 PHP版本


<?php
function generateSign($params, $secretKey) {
    // 1. 移除sign字段
    unset($params['sign']);

    // 2. 按key升序排序
    ksort($params);

    // 3. 拼接字符串
    $signStr = '';
    foreach ($params as $key => $value) {
        $signStr .= $key . '=' . $value . '&';
    }
    $signStr .= 'key=' . $secretKey;

    echo "待签名字符串: " . $signStr . "\n";

    // 4. MD5加密并转大写
    return strtoupper(md5($signStr));
}
?>

A.3 Python版本


import hashlib
import json

def generate_sign(params, secret_key):
    """生成签名"""
    # 1. 移除sign字段
    if 'sign' in params:
        del params['sign']

    # 2. 按key升序排序
    sorted_keys = sorted(params.keys())

    # 3. 拼接字符串
    sign_str = '&'.join([f"{key}={params[key]}" for key in sorted_keys])
    sign_str += f"&key={secret_key}"

    print(f"待签名字符串: {sign_str}")

    # 4. MD5加密并转大写
    sign = hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
    return sign



B. 状态码对照表


HTTP状态码业务code说明处理建议
200200成功正常处理
401401签名验证失败检查签名算法和密钥
401402Token无效或过期刷新Token
403403跨商户访问检查Token和签名的商户是否一致
400400参数错误检查请求参数
404404资源不存在检查URL路径
500500服务器错误稍后重试或联系技术支持



C. 联系我们


如有技术问题,请联系:


  • 技术支持邮箱: support@example.com
  • 技术支持电话: 400-xxx-xxxx
  • 工作时间: 周一至周五 9:00-18:00

提交问题时请提供:

1. 商户ID或open_app_key

2. 接口名称和调用时间

3. 完整的错误信息和请求日志

4. 可复现的测试代码




文档版本: v2.0.0

最后更新: 2026-01-14

维护团队: AI客服系统开发组