服务场景
IMSDK主要服务于渠道部署后的页面不能满足业务需求,需要定制聊天对话界面的场景。当渠道部署页面能满足需求时,建议直接使用渠道部署页面,减少开发工作。当机器人没有接入通义模型且不存在人工服务的情况下,也可以直接使用Chat - 会话会话接口进行对接。
集成流程
部署页面支持人工服务、机器人的回复等非一问一答的业务场景,会出现一问多答或者多问多答的情况,服务端需要可以直接发送消息给聊天窗页面,所以需要使用websocket进行连接。渠道建立websocket连接时,需要后端提供的token以保障服务安全性,整体对接流程如下。
im-sdk依赖渠道部署,需要先在智能对话机器人平台新建机器人、关联知识,配置渠道部署并将机器人及关联知识发布到正式环境,使用渠道部署后的链接验证正式环境的问答功能是否正常。
后端通过InitIMConnect接口,提供获取token的API给前端。
前端
对话界面中引入im-sdk-v2.js等依赖的前端资源;
调用后端提供的获取token的api,拿到AppKey、Token和ImDomain等参数信息;
初始化im实例:使用new window.IMSDK方法初始化实例,传入token等参数;
调用im实例的start方法建立连接;
当用户输入文字后,调用im实例的sendMessage方法将消息内容发送给后端;
当服务端发送消息时,会调用初始化im实例的参数中的onMessage方法,通过onMessage拿到消息后将消息展示到页面上;
使用ChatUI或者ChatUI-pro展示服务端发送的消息和控制用户输入后发送消息给服务器。
后端
im建联数据获取
后端通过pop接口调用,获取im建联需要参数数据:
名称 | 类型 | 必填 | 描述 | 示例值及参考API |
ImDomain | String | 是 | - | 示例:alimeim.aliyuncs.com |
Token | String | 是 | - | IM鉴权参数 |
AppKey | String | 是 | - | 应用key |
接口详情
方法名:InitIMConnect
请求方式:GET/POST
请求参数:
名称 | 类型 | 必填 | 描述 | 示例值及参考API |
FROM | String | 是 | 渠道部署fromid | 示例值: XSDDAG |
UserAccessToken | String | 否 | 用户token,通过调用pop接口生成 | 示例值: amJIWEtUR2xGTW16R01UZWF4TTVGc1hxbDRtaEdTNWVmMklFeVYzT2dOSjFWUUtuN0xDSjcxM3B5aWtFQjJoUUNvV3pZeXlDc28yZE9Yb3lhenJjN2pIeEtmbVh0anlYd242UUw0d2tSN05qVUQwTTJnV2tFN3VwdDZ1YzJzaTFTeWpoSksrb3FTYlptWGtuUkw4dzJYK0txV0c4djR4eUtmK0piSWdVNFFPdnJOc3c4RWhQeUdaTTRtN3E3L1Q5 |
AgentKey | String | 否 | 业务空间key,不设置则访问默认业务空间,key值在主账号业务管理页面获取 | 示例值: ac627989eb4f8a98ed05fd098bbae5_p_beebot_public |
maven版本
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>chatbot20220408</artifactId>
<version>1.0.5</version>
</dependency>
示例代码
import com.aliyun.chatbot20220408.models.InitIMConnectResponse;
public class Sample {
public static void main(String[] args) throws Exception {
/*
* 阿里云账号AccessKey拥有所有API的访问权限,建议您使用RAM用户进行API访问或日常运维。
* 强烈建议不要把AccessKey ID和AccessKey Secret保存到工程代码里,否则可能导致AccessKey泄露,威胁您账号下所有资源的安全。
* 调用接口前请先配置身份认证,具体操作请参见https://help.aliyun.com/document_detail/378659.html。
* 本示例使用了阿里云Credentials工具托管AccessKey,来实现API访问的身份验证。
*/
com.aliyun.credentials.Client credentialClient = new com.aliyun.credentials.Client();
/*
* 初始化client
*/
com.aliyun.chatbot20220408.Client client = new com.aliyun.chatbot20220408.Client(
new com.aliyun.teaopenapi.models.Config()
// 配置云产品服务接入地址(endpoint)。
.setEndpoint("chatbot.cn-shanghai.aliyuncs.com")
// 使用Credential配置凭证。
.setCredential(credentialClient)
);
/*
* 构建请求
*/
com.aliyun.chatbot20220408.models.InitIMConnectRequest initIMConnectRequest = new com.aliyun.chatbot20220408.models.InitIMConnectRequest()
.setFrom("xxxxx");
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
try {
// 复制代码运行请自行打印 API 的返回值
InitIMConnectResponse response = client.initIMConnectWithOptions(initIMConnectRequest, runtime);
System.out.println(response.getBody().getData());
} catch (Exception error) {
// 如有需要,请打印 error
error.printStackTrace();
}//TeaException error = new TeaException(_error.getMessage(), _error);
}
}
接口返回数据:
{
"RequestId": "48365A93-F0D1-1279-B9C4-5B1AA6B2ED5C",
"Data": {
"AppKey": "WDg2VfNv",
"Token": "amJIWEtUR2xGTW16R01UZWF4TTVGc1hxbDRtaEdTNWVmMklFeVYzT2dOSjFWUUtuN0xDSjcxM3B5aWtFQjJoUUNvV3pZeXlDc28yZE9Yb3lhenJjN2pIeEtmbVh0anlYd242UUw0d2tSN05qVUQwTTJnV2tFN3VwdDZ1YzJzaTFTeWpoSksrb3FTYlptWGtuUkw4dzJYK0txV0c4djR4eUtmK0piSWdVNFFPdnJOc3c4RWhQeUdaTTRtN3E3L1Q5",
"ImDomain": "alimeim.aliyuncs.com"
},
"Success": true
}
前端
如何引入
<script src="https://g.alicdn.com/ume/chatbot/6.0.13/im-sdk-v2.js"></script>
初始化
初始化时需要传入一个config对象
字段名 | 字段类型 | 是否必填 | 默认值 | 说明 |
url | String | 是 | - | ws地址 规范:wss://(固定) + ImDomain(后端接口获取) + /alime/im(固定) 例如:wss://alimeim.aliyuncs.com/alime/im |
token | String | 是 | - | IM鉴权参数 后端通过token生成接口给前端;注意每次生成的token仅可使用一次 |
appKey | String | 是 | - | 应用key |
uid | String | 否 | - | 用户id |
tid | String | 否 | - | 租户id |
sid | String | 否 | - | 会话标识 |
getToken | Function | 是 | - | 获取token的函数 返回Promise对象 (resolve(token)),断线重连时候使用; |
例如:
const im = new window.IMSDK({
url: 'wss://alimeim.aliyuncs.com/alime/im',
token: 'amJIWEtUR2xGTW16R01UZWF4TTVGc1hxbDRtaEdTNWVmMklFeVYzT2dOSjFWUUtuN0xDSjcxM3B5aWtFQjJoUUNvV3pZeXlDc28yZE9Yb3lhenJjN2pIeEtmbVh0anlYd242UUw0d2tSN05qVUQwTTJnV2tFN3VwdDZ1YzJzaTFTeWpoSksrb3FTYlptWGtuUkw4dzJYK0txV0c4djR4eUtmK0piSWdVNFFPdnJOc3c4RWhQeUdaTTRtN3E3L1Q5',
appKey: 'WDg2VfNv',
getToken: () => {
return new Promise((resolve) => {
resolve('token');
});
}
});
建立连接
im.start();
设置IM消息通用参数
im.setCommonParams({
senderId: 666666,
senderNick: 'xxx',
});
通用参数列表可参考本文档API>setCommonParams(params)部分。
示例
<script src="https://g.alicdn.com/ume/chatbot/6.0.13/im-sdk-v2.js"></script>
<script>
const im = new window.IMSDK({
config: {
/**
* ws地址
*/
url: "wss://xxxxxx/alime/im",
/**
* IM鉴权参数,接入方自行实现token生成和校验逻辑接入方后端提供token生成接口给前端
*/
token: "T3ZEcEdWK3hOTXNRNklUejhTaWN6bllsTzVRTEIzSWRSbWJjSGxWTW5VMS9zZ1BKWDEzMEZFbUlMWEY4czhRRE5IcFZGVC9sUURLczlwa2c1ZC9vQ0pBWW9uNzRQRDJoVFFjMjcwTlZwWllsMTJYb0JiWk0wRlhpTzBmdWtyMFJmVEs4MGFJQjVWeWtHY2NCalFqTkthSVVzVlpGalRsQm53VWtiNmxRanBjPQ==",
/**
* 应用key
*/
appKey: "8xx4xxxx",
/**
* 获取token的函数,返回Promise对象,注意需要通过接口拿到最新的token
*/
getToken: () => {
return new Promise((resolve) => {
resolve("newToken-xxxx");
});
},
},
handlers: {
/**
* 接收到业务消息
*/
onMessage: (data: ChatItem) => {
/**
* data示例: {
"header": {
"localMsgId": "167057154135437653",
"msgId": "991965825848146256",
"version": "1.3.1"
},
"body": {
"senderRole": 3,
"senderId": "2022033141787377",
"ghost": false,
"message": [
{
"content": {
"text": "您好,我是xx,很高兴为您服务。"
},
"context": {
"msg_non_disturb": "false",
"@class": "java.util.HashMap",
"sdkType": "SERVER_JAVA_SDK"
},
"extraInfo": {
"traceId": "ac10646b16705715412861986d0082",
"@class": "java.util.HashMap",
"from": "LMm8SngXnv",
"sid": "2022120911WXc_LrRJ000000052345"
},
"msgType": "text"
}
]
}
}
*/
console.log('receive message:', data);
},
/**
* 关闭IM连接
* code为关闭原因:
* CONNECTION_KICK_OUT:重复登录,此时建议提示用户有重复登录
* CONNECTION_CLOSED:重连5次依然失败,此时建议提示用户系统出错,手动重试
* CHEARTBEAT_TIMEOUT:心跳超时断开,未收到pong消息,此时SDK会自动重连,接入方无特殊需求则不用处理
* CSERVER_CONNECTION_CLOSED:服务器正在重启引起的连接断开,此时SDK会自动重连,接入方无特殊需求则不用处理
* CCONNECTION_TIMEOUT:会话超时连接断开,用户超过一定时长未说话,用户再次发消息会自动重连
*/
onClose: (err: string) => {
console.error(err);
}
}
});
// 建立连接
im.start();
/**
* @method 设置消息通用参数 [可选]
* @parma {CommonParamsProps} params 发消息者参数
* @returns
*/
im.setCommonParams({
senderId: userId,
senderNick: nick,
senderRole: 1,
});
</script>
消息协议
消息格式
{
"header": {
"localMsgId": "167057154135437653", // 本地消息id
"msgId": "991965825848146256", // 消息id,如果两条消息相同,会更新消息
"version": "1.3.1" // 消息协议版本
},
"body": {
"senderRole": 3, // //信息来源: 1、用户 3、客服 4、机器人
"senderId": "2022033141787377", // 发送者ID
"ghost": false, // 是否存储历史消息
"message": [
{
"content": { // 内容对象
"text": "您好,我是xx,很高兴为您服务。"
},
"context": { // 业务透传对象,由下行消息传递
"msg_non_disturb": "false",
"@class": "java.util.HashMap",
"sdkType": "SERVER_JAVA_SDK"
},
"extraInfo": { // 消息扩展字段
"traceId": "ac10646b16705715412861986d0082",
"@class": "java.util.HashMap",
"from": "LMm8SngXnv",
"sid": "2022120911WXc_LrRJ000000052345"
},
"msgType": "text" // 消息类型
}
]
}
}
消息类型枚举
对于上行消息来说,大部分情况下是文本消息,下行消息里文本消息、卡片消息、富文本消息居多,其他消息相对来说比较少见。文档为了看起来清晰,content直接以JSON格式列举,实际收到的为字符串,需要自己解析成json。
消息类型可能会新增,请接入方妥善处理兜底逻辑。
//1、文本消息
{
"msgType":"text", //文本消息
"content":{
"text":"Hello world" //文本具体内容
}
}
//1.1、特殊无答案的情况
{
"msgType":"text",
"context":{
"text":""
}
}
//2、卡片消息
{
"msgType":"card", //卡片消息
"content":{
"code":"${code}", //卡片模版code
"cardId":"${cardId}", //卡片id code/cardId二者选一
"data":{}, //卡片数据
"params":{} //1.5新增,动态类卡片使用
}
}
//3、富文本消息
{
"msgType":"richtext", //富文本消息
"content":{
"contentType":"Html", //文本格式类型 <Html|Markdown>
"text":"${text}" //富文本内容
}
}
//4、图片消息
{
"msgType":"image", //图片消息
"content":{
"picUrl":"https://www.alimebot.com/1.jpg" //图片地址
}
}
//5、表单信息
{
"msgType":"form", //表单信息
"content":{
"formData":"${formData}", //表单内容
"text":"${text}" //业务文本
}
}
//6、订单消息
{
"msgType":"order", //订单消息
"content":{
"source":"${source}",
"parentId":"${parentId}",
"id":"${id}"
}
}
//7、商品消息
{
"msgType":"item", //商品消息
"content":{
"source":"${source}", //商品来源
"id":"${id}" //商品id
}
}
//8、文件消息
{
"msgType":"file", //文件消息
"content":{
"fileName":"可对客-e收汇系统指引20200220", //文件名称
//文件地址
"fileUrl":"//xspace-img-cn.alicdn.com/consult/1587094983552_H48RpMMec4DBZo9TVo1vkJUv.pdf",
"fileByteSize":2171616, //文件大小
"fileType":"pdf" //文件类型
}
}
//9、音频消息
{
"msgType":"audio", //音频消息
"content":{
"url":"${url}", //音频地址
"duration":"${duration}" //音频时长
}
}
//10、视频消息
{
"msgType":"video", //视频消息
"content":{
"url":"${url}", //视频地址
"duration":"${duration}", //视频时长
"cover":"${cover}" //封面
}
}
//11、指令消息
{
"msgType":"cmd",
"content":{
"code":"AGENT_SESSION_START",//code列表见"指令消息code列表"
"params":{},
"text":""//展示文案
}
}
指令消息(cmd消息)code列表
CMD消息,可根据自己需要过滤有效信息。
AGENT_SESSION_START("进入人工"),
AGENT_SESSION_FAIL("进入人工失败"),
AGENT_JOIN("人工小二进线"),
AGENT_SERVICE_LEAVE("工作台主动断开服务"),
AGENT_LEAVE_NOTICE("人工会话关闭倒计时提醒"),
AGENT_CLIENT_LEAVE("会员主动关闭"),
AGENT_SWITCH("小二转交"),
AGENT_QUEUE("人工进线排队"),
AGENT_NO_SERVICE("小二未上班"),
AGENT_NOT_WORKTIME("小二未在工作时间"),
CLIENT_QUIT_QUEUE("退出人工进线排队"),
CLIENT_CANCEL_RESUME("取消重连"),
CLIENT_RESUME("断线重连"),
CLIENT_LEAVE_SESSION("退出人工"),
CLIENT_JOIN_SESSION("点击转人工"),
AGENT_ENTRANCE_DISPLAY("转人工入口"),
AGENT_DISCONNECT("客服已经断开"),
SWITCH_ORDER_TIP("切换订单提示");
API
getIM()
用途:获取IM实例
参数:无
返回:IM实例
setCommonParams(params)
用途:设置消息通用参数。
使用场景:比如获取地理位置信息是异步的,那么可以在拿到lbs信息之后再设置这个通用参数,这样后续消息发送都会带上。
参数:params里的参数列表如下
字段名 | 字段类型 | 是否必填 | 默认值 | 说明 |
senderId | Number | 是 | - | 发消息者的id |
senderNick | String | 是 | - | 发消息者nick |
senderRole | Number | 是 | - | 消息发送者,1表示会员 |
device | String | 否 | - | 设备信息 |
lbs | Object | 否 | - | 地理位置信息 province city district longitude latitude |
返回:无
示例:
im.setCommonParams({
senderId: 'senderId',
senderNick: "senderNick",
senderRole: 1,
});
connect(sync)
用途:建立IM连接。
参数:
字段名 | 字段类型 | 是否必填 | 默认值 | 说明 |
sync | Boolean | 否 | false | 是否同步离线消息 |
返回:无。
readyState()
用途:获取IM的状态,状态。
参数:无。
返回
类型:number
枚举值:
0:CONNECTING,表示正在连接
1:OPEN,表示连接成功,可以通信了
2:CLOSING,表示连接正在关闭
3:CLOSED,表示连接已经关闭,或者打开连接失败
sendMessage(msg, ghost)
用途:发送消息
参数:msg为对象,具体字段如下
字段名 | 字段类型 | 是否必填 | 默认值 | 说明 |
msgType | String | 是 | - | 消息类型 |
content | Object | 是 | - | 消息体 |
context | Object | 否 | - | 消息上下文 |
extraInfo | Object | 否 | - | 消息额外信息 |
字段名 | 字段类型 | 是否必填 | 默认值 | 说明 |
ghost | Boolean | 否 | false | 是否存储发送的这条消息 |
返回:无
im.sendMessage({
msgType: 'text',
content: {
text: '我是发送的消息',
},
});
close()
用途:关闭IM连接
参数:无
返回:无
reconnect(code)
用途:重新建立连接
参数:
字段名 | 字段类型 | 是否必填 | 默认值 | 说明 |
code | String | 是 | - | 重连原因 |
返回:无
handlers
onAck
接收到心跳
const im = new window.IMSDK({
handlers: {
onAck: data => {
/**
data示例:{"localMsgId":"167057253604245697","msgId":"991974171800073352"}
*/
console.log('ack', data);
},
}
});
onMessage
接收到业务消息
const im = new window.IMSDK({
handlers: {
onMessage: data => {
console.log('message', data);
},
}
});
onOpen
websocket连接建立成功
const im = new window.IMSDK({
handlers: {
onOpen: () => {
console.log('open');
},
}
});
onConnect
IM建立连接成功,和open
有区别,connect
是和IM系统建立连接成功的事件
const im = new window.IMSDK({
handlers: {
onConnect: () => {
console.log('connect');
},
}
});
onClose
IM连接关闭
const im = new window.IMSDK({
handlers: {
onClose: code => {
console.log('close', code);
},
}
});
code
为关闭原因
CONNECTION_KICK_OUT:重复登录,此时建议提示用户有重复登录
CONNECTION_CLOSED:重连5次依然失败,此时建议提示用户系统出错,手动重试
HEARTBEAT_TIMEOUT:心跳超时断开,未收到pong消息,此时SDK会自动重连,接入方无特殊需求则不用处理
SERVER_CONNECTION_CLOSED:服务器正在重启引起的连接断开,此时SDK会自动重连,接入方无特殊需求则不用处理
CONNECTION_TIMEOUT:会话超时连接断开,用户超过一定时长未说话,用户再次发消息会自动重连
onError
IM连接出错
const im = new window.IMSDK({
handlers: {
onError: err => {
console.log('error', err);
},
}
});
onReconnect
IM正在重连
const im = new window.IMSDK({
handlers: {
onReconnect: ({ code, err }) => {
console.log('reconnect', code, err);
});
}
});
ChatUI代码示例
搭配ChatUI实现的对话界面
import React, { useEffect } from 'react';
import { render } from 'react-dom';
import Chat, { Bubble, useMessages } from 'chat-ui';
// 初始消息列表,可选
const initialMessage = [
{
type: 'text',
content: { text: '亲,我是IM测试机器人' },
},
];
// 默认快捷短语,可选
const defaultQuickReplies = [
{
icon: 'message',
name: '联系人工服务',
isNew: true,
isHighlight: true,
},
{
name: '短语1',
isNew: true,
},
{
name: '短语2',
isHighlight: true,
},
{
name: '短语3',
},
];
function App() {
// 消息列表
const { messages, appendMsg } = useMessages(initialMessage);
useEffect(() => {
const im = new window.IMSDK({
config: {
url: 'wss://alimeim.aliyuncs.com/alime/im',
token: '4890',
appKey: 'QA97fCuA',
getToken: () => {
return new Promise(resolve => {
resolve('4890');
});
},
},
handlers: {
onOpen: () => {
console.log('open');
},
onClose: err => {
console.log('close', err);
},
onError: err => {
console.log('error', err);
},
onReconnect: ({ code, err }) => {
console.log('reconnect', code, err);
},
onMessage: data => {
appendMsg({
type: 'text',
content: {
text: JSON.stringify(data),
},
});
console.log('message', data);
}
}
});
im.start();
im.setCommonParams({
senderId: 123456654321,
senderNick: 'xx',
});
console.log(im);
}, []);
// 发送回调
function handleSend(type, val) {
if (type === 'text' && val.trim()) {
im.sendMessage({
msgType: 'text',
content: {
text: val,
},
});
appendMsg({
type: 'text',
content: { text: val },
position: 'right',
});
}
}
// 快捷短语回调,可根据 item 数据做出不同的操作,这里以发送文本消息为例
function handleQuickReplyClick(item) {
handleSend('text', item.name);
}
function renderMessageContent(msg) {
const { type, content } = msg;
// 根据消息类型来渲染
switch (type) {
case 'text':
return <Bubble content={content.text} />;
case 'image':
return (
<Bubble type="image">
<img src={content.picUrl} alt="" />
</Bubble>
);
default:
return null;
}
}
return (
<Chat
messages={messages}
renderMessageContent={renderMessageContent}
quickReplies={defaultQuickReplies}
onQuickReplyClick={handleQuickReplyClick}
onSend={handleSend}
/>
);
}
render(<App />, document.getElementById('root'));
ChatSDK代码示例
ChatSDK的示例可通过链接`https://chatbot.console.aliyun.com/ChatSDK#/cards/im-sdk`进行查看体验,其中html和js的代码如下
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta charset="utf-8" />
<meta name="renderer" content="webkit" />
<meta name="force-rendering" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0, viewport-fit=cover"
/>
<link
rel="stylesheet"
href="https://g.alicdn.com/code/npm/@ali/chatui-sdk/6.1.9/ChatSDK.css"
/>
</head>
<body>
<div id="root"></div>
<script src="https://g.alicdn.com/code/npm/@ali/chatui-sdk/6.1.9/ChatSDK.js"></script>
<script src="https://g.alicdn.com/code/npm/@ali/chatui-sdk/6.1.9/isvParser.js"></script>
<script src="https://g.alicdn.com/chatui/icons/0.2.7/index.js"></script>
<script src="https://g.alicdn.com/ume/chatbot/6.0.13/im-sdk-v2.js"></script>
<script src="./demo_im_sdk.js"></script>
</body>
</html>
// 对话机器人通讯建立
const url = 'wss://alimeim.aliyuncs.com/alime/im';
const token = location.href.match(/token=(\w+)/) ? location.href.match(/token=(\w+)/)[1] : '';
const appKey = location.href.match(/appKey=(\w+)/) ? location.href.match(/appKey=(\w+)/)[1] : '';
window.im = new window.IMSDK({
config: {
url: url,
// 替换为实际生成的token
token: token,
// 替换为自己的appKey
appKey: appKey,
getToken: () => {
// 需要替换为请求后端生成的token
return Promise.resolve('newToken');
},
},
handlers: {
onOpen: () => {
console.log('open');
},
onClose: err => {
console.log('close', err);
},
onError: err => {
console.log('error', err);
},
onReconnect: ({ code, err }) => {
console.log('reconnect', code, err);
},
// 接收到消息的处理
onMessage: data => {
const msg = window.isvParser({ data });
console.log('onMessage:', data, msg);
window.bot.getCtx().updateOrAppendMessage(msg[0]);
},
},
});
window.im.start();
// 渲染对话页面
window.bot = new window.ChatSDK({
root: document.querySelector('#chatbot'),
config: {
navbar: {
title: '智能助理',
},
robot: {
avatar: '//gw.alicdn.com/tfs/TB1U7FBiAT2gK0jSZPcXXcKkpXa-108-108.jpg',
},
avatarWhiteList: ['all'],
messages: [
{
type: 'text',
content: {
text: '智能助理为您服务,请问有什么可以帮您?',
},
},
],
},
requests: {
send(msg) {
window.im.sendMessage({
msgType: 'text',
content: {
text: msg.content.text,
},
});
},
},
});
window.bot.run();
// 将ctx放到全局,方便外部进行操作调试
window.ctx = window.bot.getCtx();
Debug
通过浏览器调试窗口可查看websocket通讯的消息内容