数字员工聊天组件

数字员工聊天组件概述

简介

数字员工聊天组件 是一个基于 React 开发的聊天组件,提供完整的对话交互能力。通过 OpenSDK 体系对外开放,第三方可以快速集成到自己的业务系统中。

核心特性


效果演示:

预览页地址:https://ark.wps.cn/demo/pages/web-sdk

image-20251124105951453

1762341415248

开发必读

前置准备


快速开始

如果想要更加便捷的配置 直接查看完整示例

引入OpenSDK

您可以直接下载opensdK到您自己的项目或者直接引入在线CDN

OpenSDK地址:https://qn.cache.wpscdn.cn/open_static/libs/open-sdk/0.0.2/open-sdk.0.0.2.umd.js

在HTML页面中引入OpenSDK

html
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AgentSpace Chat SDK Demo</title>
  <style>
    body {
      width: 100vw;
      height: 100vh;
      overflow: hidden;
      background-color: #f7f8f9 !important;
    }

    #agentspace-container {
      height: 100vh;
      width: 100vw;
    }
  </style>
</head>

<body>
  <div id="root" class="height-100vh width-100vw">
    <div id="agentspace-container" class="height-100vh width-100vw"></div>
  </div>
    //引入opensdk
    <script src="https://qn.cache.wpscdn.cn/open_static/libs/open-sdk/0.0.2/open-sdk.0.0.2.umd.js"></script>

    <script>
      ....业务方需要实现的代码... (见下)
      startApp();
    </script>
</body>
</html>

引入数字员工聊天组件

您可以直接下载数字员工组件UMD包到您自己的项目或者直接引入在线CDN

agent_chat地址:https://qn.cache.wpscdn.cn/open_static/libs/widgets/agentspace_chat/1.0.8/kso.component.agentspace_chat.1.0.8.umd.js

React项目导入

  1. 确保项目中依赖react>=18.0.0,react-dom>=18.0.0
  2. 将依赖挂载到指定的全局对象window.AgentSpaceWebSDKDeps中
  3. 依赖挂载后通过script加载SDK的UMD包
javascript
// 示例导入代码
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';
import * as ReactJSXRuntime from 'react/jsx-runtime';

// 单例加载 UMD 脚本:确保在使用 OpenSDK.create/mount 前,组件库已完成加载
let agentspaceChatUmdLoadPromise: Promise<void> | null = null;

// 加载web SDK的UMD包
function loadAgentspaceChatUmd() {
  if (agentspaceChatUmdLoadPromise) return agentspaceChatUmdLoadPromise;

  agentspaceChatUmdLoadPromise = new Promise<void>((resolve) => {
    // UMD 包在浏览器环境下会从window读取这三个依赖,需要在script加载前挂载
     (window as any).AgentSpaceWebSDKDeps = {
      React:React,
      ReactDOM: {
        ...ReactDOM,
        createRoot:ReactDOMClient.createRoot, // React18的createRoot在react-dom/client上
      },
      ReactJSXRuntime:ReactJSXRuntime,
    };
    // 通过script加载组件库
    const script = document.createElement('script');
    // 通过script加载,从CDN导入SDK
    script.src = 'https://qn.cache.wpscdn.cn/open_static/libs/widgets/agentspace_chat/1.0.8/kso.component.agentspace_chat.1.0.8.umd.js';
    script.onload = () => resolve();
    document.head.appendChild(script);
  });
  return agentspaceChatUmdLoadPromise;
}

// 加载组件
const tryRenderWidget = async () => {
    if (hasRendered) return;
    try {
      // 确保 UMD 组件库已加载完成,再创建实例/渲染
      await loadAgentspaceChatUmd();

      // 鉴权初始化流程
      ......

      // 创建 agentspaceChat 实例
      const agentspaceChatInstance = window.OpenSDK.create('kso-agentspace-chat', {
        agentId: "your-agentId",
      });

      await agentspaceChatInstance.mount(containerRef.current);

      console.log('[渲染组件] 组件渲染成功');

    } catch (error) {
      console.error('[渲染组件] 渲染失败:', error);
    }
};

鉴权配置

在创建组件实例前,需要先完成授权配置:

javascript
// 定义所需的权限范围
const defaultScopes = [
  'kso.component.agentspace_chat',   // 数字员工聊天组件权限(必需)
  'kso.wiki.readwrite' // 知识库管理和查询全选(必需)
];

// 从 localStorage 获取已保存的权限,或使用默认值
const savedScopes = localStorage.getItem('merged_scopes');
let scopes = savedScopes ? JSON.parse(savedScopes) : defaultScopes;

// OAuth2 授权函数
async function authorize(scopes) {
  OpenSDK.OAuth2.authorize({
    appId: 'YOUR_APP_ID',                                   // 必填:您的应用 ID
    redirect_uri: 'https://agentspace.wps.cn/sdk-callback', // 必填:回调地址(需要在需要在开放平台配置授权回调地址)
    scope: scopes.join(','),                        
    mode: OpenSDK.OAuth2.Mode.POPUP,               	 // 必填:授权模式(POPUP 或 REDIRECT)
    state: 'agent-space-chat',                       // 可选:自定义状态参数
  });
}

初始化流程

javascript
// React,ReactDOM,ReactJSXRuntime挂载到AgentSpaceWebSDKDeps中

window.AgentSpaceWebSDKDeps = {
  React: React,
  ReactDOM: ReactDOM,
  ReactJSXRuntime: jsxRuntime,
};

javascript
/**
 * 主应用逻辑
 */

let agentspaceChatUmdLoadPromise = null;

// 加载第三方依赖
function loadScript(src) {
  return new Promise((resolve, reject) => {
    // 已存在则复用
    const existing = document.querySelector(`script[src="${src}"]`);
    if (existing) {
      if (existing.dataset.loaded === "1") return resolve();
      existing.addEventListener("load", resolve, { once: true });
      existing.addEventListener("error", reject, { once: true });
      return;
    }

    const s = document.createElement("script");
    s.src = src;
    s.async = true;
    s.addEventListener("load", () => {
      s.dataset.loaded = "1";
      resolve();
    });
    s.addEventListener("error", reject);
    document.head.appendChild(s);
  });
}

/**
 * 加载 agentspace_chat UMD
 */
function loadAgentspaceChatUmd() {
  if (agentspaceChatUmdLoadPromise) return agentspaceChatUmdLoadPromise;

  agentspaceChatUmdLoadPromise = (async () => {
    await loadScript(
      "https://qn.cache.wpscdn.cn/open_static/libs/widgets/agentspace_chat/1.0.8/kso.component.agentspace_chat.1.0.8.umd.js"
    );
  })();

  return agentspaceChatUmdLoadPromise;
}


let hasRender = false;

// localStorage.scopes || 使用默认值
const savedScopes = localStorage.getItem('merged_scopes');
let scopes = savedScopes ? JSON.parse(savedScopes) : defaultScopes;

async function startApp() {
  if (hasRender) return;
  OpenSDK.setDebug(true);
  // 检查app_config是否过期
  const app_config = localStorage.getItem('app_config');
  if (app_config) {
    const {
      app_id,
      signature,
      noncestr,
      timestamp,
      tag,
      url,
      app_config_timestamp,
    } = JSON.parse(app_config);
    console.log('app_config_timestamp', Date.now() - app_config_timestamp);
    if (Date.now() - app_config_timestamp < 7200000) {
      await OpenSDK.config({
        scopes,
        signature, // 签名
        appId: app_id, // 应用 appId
        timestamp, // 时间戳(毫秒)
        nonceStr: noncestr, // 随机字符串
        tag,
        url,
      });

      // 创建组件
      renderWidget();
      return;
    } else {
      localStorage.removeItem('app_config');
      // 缓存过期,重新开始授权流程
      // console.log('[授权流程] app_config 缓存已过期,重新授权')
      window.location.reload();
    }
  }

  // 无缓存或缓存过期,开始授权
  console.log('[授权流程] 开始授权');
  authorize(scopes);

  OpenSDK.addEventListener(OpenSDK.Events.OAuth2Message, async (event) => {
    // 安全起见,判断来源
    if (event.origin !== 'https://agentspace.wps.cn') return;
    const code = event.data.code;
    console.log('OAuth2Message code', code);

    // 步骤一:拿到临时授权码code,提交给服务端申请access_token
    // 应用授权,无需走OAuth授权流程,直接申请access_token
    // 用户授权,走OAuth授权流程,获取auth_code,然后申请access_token
    if (!code) {
      authorize(scopes);
      return;
    }

    // 步骤二:获取biz-server 申请的js_ticket相关签名信息
    const { app_id, signature, noncestr, timestamp, tag, url } = await fetch(
      `${host}/api/get_app_config?code=${code}`
    ).then(async (res) => {
      const resp = await res.json();
      if (resp?.code === 0) {
        return resp.data || {};
      } else {
        throw new Error('获取signature失败');
      }
    });
    // 先删除缓存的 app_config
    localStorage.removeItem('app_config');
    // 将获取的信息缓存存储,两个小时后过期
    localStorage.setItem(
      'app_config',
      JSON.stringify({
        app_id,
        signature,
        noncestr,
        timestamp,
        tag,
        url,
        app_config_timestamp: Date.now(),
      })
    );
    // 步骤三:OpenSDK设置签名信息
    await OpenSDK.config({
      scopes,
      signature, // 签名
      appId: app_id, // 应用 appId
      timestamp, // 时间戳(毫秒)
      nonceStr: noncestr, // 随机字符串
      tag,
      url,
    });
    // 步骤四:渲染组件
    renderWidget();
  });
}

创建组件实例

javascript
async function renderWidget() {
  // 加载 agentspace_chat UMD
  await loadAgentspaceChatUmd();

  if (OpenSDK.components.size === 0) return;
  const agentspaceChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: agentConfig.agentId,
  });
  agentspaceChat.mount(document.getElementById('agentspace-container'));
  hasRender = true;
  // 监听scopes范围不够,二次授权
  agentspaceChat.addEventListener(
    ...... 见3.5 监听事件
  );
}

监听事件

详见事件监听,此处展示必要的监听事件

javascript
  agentspaceChat.addEventListener(
    agentspaceChat.Events.OnAuthRequest,
    async (e) => {
      const detailScopeUser = e.detailScopeUser || []; 
      // detail接口返回的所有required scopes与本地配置的scopes进行对比,判断缺失的scopes
      const missingScopes = detailScopeUser.filter(
        (scope) => !scopes.includes(scope)
      );
      // 如果没有缺失的权限,无需重新授权
      if (missingScopes.length === 0) {
        console.log('[业务方 addEventListener] 权限验证通过,无需重新授权');
        return;
      }
      // 合并 scopes
      const mergedScopes = [...new Set([...scopes, ...missingScopes])];
      scopes = mergedScopes;
      // 保存合并后的scopes
      localStorage.setItem('merged_scopes', JSON.stringify(mergedScopes));
      // 清除缓存的 app_config,强制重新获取新的授权信息
      localStorage.removeItem('app_config');
        
      // 显示自定义授权提示 UI(不使用 window.confirm)实现见授权提示弹窗
      showAuthPrompt(
        missingScopes,
        mergedScopes,
        // 确认授权回调
        (mergedScopes) => authorize(mergedScopes),
        // 取消授权回调
        () => {
          console.log('[业务方 addEventListener] 用户取消授权');
        }
      );
    }
  );

自定义授权提示ui

showAuthPrompt( )

授权提示弹窗

动态更新配置

javascript
agentspaceChat.update({
  theme: 'dark',
  welcomeMessage: '配置已更新!我是您的智能助手,有什么可以帮助您的吗?',
  locale: 'en',
  placeholder: 'Please enter your question...',
});

发送消息

javascript
// 发送消息的参数类型
interface ISendMessageParams {
  content: string;  // 消息内容
  knowledges?: string[];  // 知识库id数组
}

// 示例
agentspaceChat?.sendMessage({content:'你好'})

卸载组件

javascript
agentspaceChat.unmount();

后端api接口示例

步骤1:用户授权,code 换 access_token, access_token 禁止返回前端保存,

开发者自行实现OAuth2授权,获取并维护access_token(示例见app.js和auth.js)

步骤2:业务方接口,获取用户信息,app,signature等

开发文档:https://365.kdocs.cn/3rd/open/documents/app-integration-dev/wps365/server/certification-authorization/user-authorization/flow.html

js
const index: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
    //步骤一 获取access_token 
  fastify.get('/get_app_config', async function (request, reply) {
    // 此处简化获取access_token,js_ticket逻辑
    const { code = '' } = (request.query as { code: string }) || {};
    let user_access_token = '';
    const resp = await getAccessToken({
      code,
      agent_ak: config.agent_ak,
      agent_sk: config.agent_sk,
    });

    if (resp?.code === 0) {
      user_access_token = resp?.data?.user_token || '';

      if (!user_access_token) {
        return reply
          .status(400)
          .send({ code: 400, message: '获取access_token失败' });
      }
    } else {
      return reply
        .status(400)
        .send({ code: 400, message: '获取access_token失败' });
    }

    console.log('授权信息', {
      user_access_token,
      full_response: resp,
    });

    let jsapi_ticket = {
      ticket: '',
      tag: '',
    };
	//步骤二 获取jsapi_ticket
    if (!jsapi_ticket.ticket) {
      const ticketResp: any = await getJSAPITicket(user_access_token);
      console.log('js_ticket', ticketResp);
      if (ticketResp?.code === 0) {
        jsapi_ticket = ticketResp.data;
      } else {
        return reply
          .status(400)
          .send({ code: 400, message: '获取jsapi_ticket失败' });
      }
    }
    // 请求页面的url,不包含query,search
    const url = `http://${request.headers.host}`;
    console.log('url', url);
    const noncestr = Math.random().toString(36).substring(2, 15);
    const timestamp = (Date.now() / 1e3) | 0;
    console.log('timestamp', timestamp);
    const signature = genSignature(
      jsapi_ticket.ticket,
      noncestr,
      timestamp,
      url
    );

    return MakeSuccess({
      app_id: config.agent_ak,
      signature,
      noncestr,
      timestamp,
      tag: jsapi_ticket?.tag,
      url,
    });
  });
};

getAccessToken和getJSAPITicket函数

ts
export async function getAccessToken(data: {
  code: string;
  agent_ak: string;
  agent_sk: string;
}): Promise<IResponse<any>> {
  if (!data.code) {
    return {
      code: 1,
      message: 'code is required',
      data: {},
    };
  }
  const userResp = await openApi.post<any, any>('/oauth2/token',{
      grant_type: 'authorization_code',
      client_id: data.agent_ak,
      client_secret: data.agent_sk,
      code: data.code,
      redirect_uri: 'https://agentspace.wps.cn/sdk-callback',
    },
    {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );
  if (userResp?.access_token) {
    return {
      code: 0,
      data: {
        user_token: userResp.access_token,
        user_auth: userResp,
      },
    };
  }
  return {
    code: 1,
    message: 'user authorization failed',
    data: {},
  };
}

export async function getJSAPITicket(
  access_token: string
): Promise<IResponse<any>> {
  const resp = await openApi.get('/oauth2/jsapi_ticket', {
    headers: {
      Authorization: `bearer ${access_token}`,
    },
  });
  return {
    code: 0,
    data: {
      ...(resp || {}),
    },
  };
}

事件监听

支持的事件类型

事件名称说明事件数据回调参数
Updated组件配置更新时触发{ info: string }info: 更新信息描述,如 "数字员工组件刷新完成"
OnChatStart对话开始时触发{ message: Message }message: 用户发送的消息对象(包含 id、text、sender、timestamp 等字段)
OnChatEnd对话结束时触发{}无参数(空对象)
OnError发生错误时触发{ error: Error | any }error: 错误对象或错误信息
OnMessageSend用户发送消息时触发{ message: Message }message: 发送的消息对象(包含消息内容、发送者等信息)
OnMessageReceive接收到 AI 回复时触发{ message: Message }message: 接收的消息对象(可能包含 content_blocks、wps_tools 等富内容)
OnSelected用户进行选择操作时触发any选择的内容(根据具体场景而定)
OnAuthRequest需要额外权限时触发(用于二次授权)AuthRequestEventdetailScopeUser: 所有必需权限数组

事件监听示例

javascript
// 1. Updated 事件
agentspaceChat.addEventListener(agentspaceChat.Events.Updated, (e) => {
  console.log('[Updated]', e.info); // "数字员工组件刷新完成"
});

// 2. OnChatStart 事件
agentspaceChat.addEventListener(agentspaceChat.Events.OnChatStart, (e) => {
  console.log('[OnChatStart]', e.message.text);
});

// 3. OnChatEnd 事件
agentspaceChat.addEventListener(agentspaceChat.Events.OnChatEnd, (e) => {
  console.log('[OnChatEnd] 对话结束');
});

// 4. OnError 事件
agentspaceChat.addEventListener(agentspaceChat.Events.OnError, (e) => {
  console.error('[OnError]', e.error);
});

// 5. OnMessageSend 事件
agentspaceChat.addEventListener(agentspaceChat.Events.OnMessageSend, (e) => {
  console.log('[OnMessageSend]', e.message.text, e.message.sender);
});

// 6. OnMessageReceive 事件
agentspaceChat.addEventListener(agentspaceChat.Events.OnMessageReceive, (e) => {
  console.log('[OnMessageReceive]', e.message.text);
  // 处理富内容
  if (e.message.content_blocks) {
    e.message.content_blocks.forEach(block => {
      console.log('内容块:', block.title);
    });
  }
});

// 7. OnSelected 事件
agentspaceChat.addEventListener(agentspaceChat.Events.OnSelected, (e) => {
  console.log('[OnSelected]', e);
});

// 8. OnAuthRequest 事件(重要)
agentspaceChat.addEventListener(agentspaceChat.Events.OnAuthRequest, (e) => {
  console.log('[业务方 addEventListener] detailScopeUser:',e.detailScopeUser );
});

移除事件监听

javascript
// 定义事件处理函数
const handleChatStart = (event) => {
  console.log('对话开始:', event);
};

// 添加监听
agentspaceChat.addEventListener(agentspaceChat.Events.OnChatStart, handleChatStart);

// 移除监听
agentspaceChat.removeEventListener(agentspaceChat.Events.OnChatStart, handleChatStart);

完整使用示例

您需要在config.js中进行配置,然后使用这个示例(React项目),依赖挂载可以参考React项目导入

index.html

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AgentSpace Chat SDK Demo</title>
    <style>
      body {
        width: 100vw;
        height: 100vh;
        overflow: hidden;
        background-color: #f7f8f9 !important;
      }
      #agentspace-container{
        height: 100vh;
        width: 100vw;
      }
    </style>
  </head>
  <body>
    <div id="root" class="height-100vh width-100vw">
      <div id="agentspace-container" class="height-100vh width-100vw"></div>
    </div>
    
    <script src="https://qn.cache.wpscdn.cn/open_static/libs/open-sdk/0.0.2/open-sdk.0.0.2.umd.js"></script>

    <!-- 授权弹框辅助函数【此处模拟业务方的实现】 -->
    <script src="./demo/chat-demo/auth-popup-helpers.js"></script>
    <script>
      // 初始化弹窗检测器
      initPopupBlockDetector();
    </script>
    
    <!-- 配置文件 -->
    <script src="./demo/chat-demo/config.js"></script>
    
    <!-- 授权逻辑 -->
    <script src="./demo/chat-demo/auth.js"></script>
    
    <!-- 主应用逻辑 -->
    <script src="./demo/chat-demo/app.js"></script>
    
    <!-- 启动应用 -->
    <script>
      startApp()
    </script>
  </body>
</html>

config.js

js
/**
 * 配置文件
 */

// biz-server 第三方服务地址,您需要替换为您的服务地址
const host = 'http://127.0.0.1:3000'; 

// 默认权限范围,配置您需要的权限
const defaultScopes = [
  'kso.component.agentspace_chat',
  'kso.wiki.readwrite'
];

// Agent 配置,您需要配置您的数字员工的agentid                
const agentConfig = {
  agentId: '*********',
};

// OAuth2 配置,您需要配置您的state 		
const oauthConfig = {
  appId: agentConfig.agentId,
  redirect_uri: 'https://agentspace.wps.cn/sdk-callback',
  state: 'agent-space-chat',
};

auth.js

js
/**
 * 授权相关函数
 */

// 发起授权
async function authorize(scopes) {
  // 用户授权演示
  // 以下信息,建议从业务服务端返回给前端
  OpenSDK.OAuth2.authorize({
    appId: oauthConfig.appId,
    redirect_uri: oauthConfig.redirect_uri,
    scope: scopes.join(','),
    mode: OpenSDK.OAuth2.Mode.POPUP,
    state: oauthConfig.state,
  });
}

// 暴露给全局
window.authorize = authorize;

app.js

js
/**
 * 主应用逻辑
 */

let agentspaceChatUmdLoadPromise = null;

// 加载第三方依赖
function loadScript(src) {
  return new Promise((resolve, reject) => {
    // 已存在则复用
    const existing = document.querySelector(`script[src="${src}"]`);
    if (existing) {
      if (existing.dataset.loaded === "1") return resolve();
      existing.addEventListener("load", resolve, { once: true });
      existing.addEventListener("error", reject, { once: true });
      return;
    }

    const s = document.createElement("script");
    s.src = src;
    s.async = true;
    s.addEventListener("load", () => {
      s.dataset.loaded = "1";
      resolve();
    });
    s.addEventListener("error", reject);
    document.head.appendChild(s);
  });
}

/**
 * - 加载 agentspace_chat UMD
 */
function loadAgentspaceChatUmd() {
  if (agentspaceChatUmdLoadPromise) return agentspaceChatUmdLoadPromise;

  agentspaceChatUmdLoadPromise = (async () => {

     // UMD 包在浏览器环境下会从window读取这三个依赖,需要在script加载前挂载
     (window as any).AgentSpaceWebSDKDeps = {
      React:React, // React依赖
      ReactDOM: {
        ...ReactDOM,
        createRoot:ReactDOMClient.createRoot, // React18的createRoot在react-dom/client上
      },  // ReactDOM依赖
      ReactJSXRuntime:ReactJSXRuntime,  // ReactJSXRuntime
    };
    
    await loadScript(
      "https://qn.cache.wpscdn.cn/open_static/libs/widgets/agentspace_chat/1.0.8/kso.component.agentspace_chat.1.0.8.umd.js"
    );
  })();

  return agentspaceChatUmdLoadPromise;
}


let hasRender = false;

// localStorage.scopes || 使用默认值
const savedScopes = localStorage.getItem('merged_scopes');
let scopes = savedScopes ? JSON.parse(savedScopes) : defaultScopes;

async function startApp() {
  if (hasRender) return;
  OpenSDK.setDebug(true);
  // 检查app_config是否过期
  const app_config = localStorage.getItem('app_config');
  if (app_config) {
    const {
      app_id,
      signature,
      noncestr,
      timestamp,
      tag,
      url,
      app_config_timestamp,
    } = JSON.parse(app_config);
    console.log('app_config_timestamp', Date.now() - app_config_timestamp);
    if (Date.now() - app_config_timestamp < 7200000) {
      await OpenSDK.config({
        scopes,
        signature, // 签名
        appId: app_id, // 应用 appId
        timestamp, // 时间戳(毫秒)
        nonceStr: noncestr, // 随机字符串
        tag,
        url,
      });

      // 创建组件
      renderWidget();
      return;
    } else {
      localStorage.removeItem('app_config');
      // 缓存过期,重新开始授权流程
      // console.log('[授权流程] app_config 缓存已过期,重新授权')
      window.location.reload();
    }
  }

  // 无缓存或缓存过期,开始授权
  console.log('[授权流程] 开始授权');
  authorize(scopes);

  OpenSDK.addEventListener(OpenSDK.Events.OAuth2Message, async (event) => {
    // 安全起见,判断来源
    if (event.origin !== 'https://agentspace.wps.cn') return;
    const code = event.data.code;
    console.log('OAuth2Message code', code);

    // 步骤一:拿到临时授权码code,提交给服务端申请access_token
    // 应用授权,无需走OAuth授权流程,直接申请access_token
    // 用户授权,走OAuth授权流程,获取auth_code,然后申请access_token
    if (!code) {
      authorize(scopes);
      return;
    }

    // 步骤二:获取biz-server 申请的js_ticket相关签名信息
    const { app_id, signature, noncestr, timestamp, tag, url } = await fetch(
      `${host}/api/get_app_config?code=${code}`
    ).then(async (res) => {
      const resp = await res.json();
      if (resp?.code === 0) {
        return resp.data || {};
      } else {
        throw new Error('获取signature失败');
      }
    });
    // 先删除缓存的 app_config
    localStorage.removeItem('app_config');
    // 将获取的信息缓存存储,两个小时后过期
    localStorage.setItem(
      'app_config',
      JSON.stringify({
        app_id,
        signature,
        noncestr,
        timestamp,
        tag,
        url,
        app_config_timestamp: Date.now(),
      })
    );
    // 步骤三:OpenSDK设置签名信息
    await OpenSDK.config({
      scopes,
      signature, // 签名
      appId: app_id, // 应用 appId
      timestamp, // 时间戳(毫秒)
      nonceStr: noncestr, // 随机字符串
      tag,
      url,
    });
    // 步骤四:渲染组件
    renderWidget();
  });
}

async function renderWidget() {

  // 加载 agentspace_chat UMD
  await loadAgentspaceChatUmd();
  if (OpenSDK.components.size === 0) return;

  const agentspaceChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: agentConfig.agentId,
    initialExpanded: false,
  });
  agentspaceChat.mount(document.getElementById('agentspace-container'));

  hasRender = true;

  // 第三方开发者监听事件
  agentspaceChat.addEventListener(agentspaceChat.Events.Updated, (e) => {
    console.log('[3rd] Updated:', e);
  });
  agentspaceChat.addEventListener(agentspaceChat.Events.OnSelected, (e) => {
    console.log('[3rd] OnSelected:', e);
  });

  // 监听scopes范围不够,二次授权
  agentspaceChat.addEventListener(
    agentspaceChat.Events.OnAuthRequest,
    async (e) => {
      const detailScopeUser = e.detailScopeUser || []; // detail接口返回的所有required scopes

      console.log(
        '[业务方 addEventListener] detailScopeUser:',
        detailScopeUser
      );

      // 使用本地配置的scopes进行对比,判断缺失的scopes
      const missingScopes = detailScopeUser.filter(
        (scope) => !scopes.includes(scope)
      );

      // 如果没有缺失的权限,无需重新授权
      if (missingScopes.length === 0) {
        console.log('[业务方 addEventListener] 权限验证通过,无需重新授权');
        return;
      }

      // 合并 scopes
      const mergedScopes = [...new Set([...scopes, ...missingScopes])];
      scopes = mergedScopes;

      // 保存合并后的scopes
      localStorage.setItem('merged_scopes', JSON.stringify(mergedScopes));
      // 清除缓存的 app_config,强制重新获取新的授权信息
      localStorage.removeItem('app_config');

      console.log('[业务方 addEventListener] 合并后的scopes:', mergedScopes);

      // 显示自定义授权提示 UI(不使用 window.confirm)
      showAuthPrompt(
        missingScopes,
        mergedScopes,
        // 确认授权回调
        (mergedScopes) => authorize(mergedScopes),
        // 取消授权回调
        () => {
          console.log('[业务方 addEventListener] 用户取消授权');
        }
      );
    }
  );
}

auth-popup-helpers.js

js

/**
 * 授权弹框辅助函数 【此处模拟业务方的实现】
 * 包含:弹窗被阻止检测、授权提示UI等
 */

// 显示弹窗被阻止的提示
function showPopupBlockedWarning() {
  // 移除可能存在的旧提示
  const existingWarning = document.getElementById('popup-blocked-warning');
  if (existingWarning) {
    existingWarning.remove();
  }

  const warningBox = document.createElement('div');
  warningBox.id = 'popup-blocked-warning';
  warningBox.style.cssText = `
    position: fixed;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    background: #fff3cd;
    border: 1px solid #ffc107;
    color: #856404;
    padding: 15px 20px;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    z-index: 999999;
    max-width: 500px;
    font-size: 14px;
    animation: slideDown 0.3s ease-out;
  `;

  warningBox.innerHTML = `
    <style>
      @keyframes slideDown {
        from {
          transform: translateX(-50%) translateY(-20px);
          opacity: 0;
        }
        to {
          transform: translateX(-50%) translateY(0);
          opacity: 1;
        }
      }
    </style>
    <div style="display: flex; align-items: flex-start; gap: 12px;">
      <div style="font-size: 20px;">⚠️</div>
      <div style="flex: 1;">
        <div style="font-weight: bold; margin-bottom: 8px;">浏览器已阻止弹窗</div>
        <div style="margin-bottom: 8px;">授权窗口被浏览器拦截,请按以下步骤操作:</div>
        <ol style="margin: 8px 0 0 0; padding-left: 20px;">
          <li>点击地址栏右侧的 <strong>弹窗被阻止</strong> 图标</li>
          <li>选择 <strong>始终允许来自此网站的弹窗</strong></li>
          <li>刷新页面重新授权</li>
        </ol>
      </div>
      <button id="close-popup-warning" style="
        background: none;
        border: none;
        font-size: 18px;
        cursor: pointer;
        padding: 0;
        line-height: 1;
        color: #856404;
      ">✕</button>
    </div>
  `;

  document.body.appendChild(warningBox);

  // 关闭按钮
  document.getElementById('close-popup-warning').onclick = () => {
    warningBox.style.animation = 'slideDown 0.3s ease-out reverse';
    setTimeout(() => warningBox.remove(), 300);
  };

  // 10秒后自动关闭
  setTimeout(() => {
    if (document.getElementById('popup-blocked-warning')) {
      warningBox.style.animation = 'slideDown 0.3s ease-out reverse';
      setTimeout(() => warningBox.remove(), 300);
    }
  }, 10000);
}

// 显示授权提示 UI(不使用 window.confirm,浏览器会拦截,需要第三方实现一个弹框)
function showAuthPrompt(missingScopes, mergedScopes, onConfirm, onCancel) {
  // 移除可能存在的旧提示
  const existingPrompt = document.getElementById('auth-prompt-box');
  if (existingPrompt) {
    existingPrompt.remove();
  }

  // 提示框
  const promptBox = document.createElement('div');
  promptBox.id = 'auth-prompt-box';
  promptBox.style.cssText = `
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: white;
    padding: 20px;
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
    z-index: 99999;
    min-width: 300px;
  `;

  // 内容
  promptBox.innerHTML = `
    <div style="margin-bottom: 15px;">
      <strong>需要新增权限</strong>
    </div>
    <div style="margin-bottom: 15px; font-size: 14px; color: #666;">
      缺失权限:${missingScopes.join(', ')}
    </div>
    <div style="text-align: right;">
      <button id="auth-cancel-btn" style="margin-right: 10px; padding: 6px 15px; cursor: pointer;">取消</button>
      <button id="auth-confirm-btn" style="padding: 6px 15px; background: #1890ff; color: white; border: none; cursor: pointer;">授权</button>
    </div>
  `;
  document.body.appendChild(promptBox);

  // 按钮事件
  document.getElementById('auth-confirm-btn').onclick = () => {
    console.log('[授权提示] 用户点击授权');
    document.body.removeChild(promptBox);
    if (typeof onConfirm === 'function') {
      onConfirm(mergedScopes);
    }
  };

  document.getElementById('auth-cancel-btn').onclick = () => {
    console.log('[授权提示] 用户取消');
    document.body.removeChild(promptBox);
    if (typeof onCancel === 'function') {
      onCancel();
    }
  };
}

// 初始化弹窗检测器(拦截 window.open)
function initPopupBlockDetector() {
  const originalWindowOpen = window.open;

  window.open = function (...args) {
    const popup = originalWindowOpen.apply(this, args);

    // 检测弹窗是否被阻止
    if (!popup || popup.closed || typeof popup.closed === 'undefined') {
      console.error('[弹窗检测] 浏览器阻止了弹窗');
      showPopupBlockedWarning();
      return null;
    }

    return popup;
  };
}

// 导出到全局
window.showPopupBlockedWarning = showPopupBlockedWarning;
window.showAuthPrompt = showAuthPrompt;
window.initPopupBlockDetector = initPopupBlockDetector;

后端接口

后端接口实现

使用指南

传参说明

typescript
{
  agentId:string; // 组件使用的智能体ID---必选参数
  welcomeMessage:string;  // 组件欢迎页显示文案
  className:string; // 组件最外层自定义样式
  agentChatClassName:IAgentChatClassNameType;  // 组件定制区域自定义样式
  initialExpanded:boolean;  // 是否初始化展开组件,默认为false
  floatingButtonTips:string;  // 最小化状态下的tooltip文案
  locale:ILocaleType;  // 组件语言配置
  presetQuestions:string[]; // 欢迎页默认问题配置
  agentExtraButtons:IExtraButton[]; // AI消息自定义按钮配置
  userExtraButtons:IExtraButton[];  // 用户消息自定义按钮配置
  onChatMessageChannel:string;  // 对话消息监听的唯一频道号
  customAiMessageComponenent:ReactNode; // 自定义AI消息组件
  onAgentChatClose:function(); // 组件关闭事件回调
  hideTitleActions:boolean; // 是否隐藏标题栏右侧操作按钮,默认为false
  hideMinimizeTooltip:boolean;  // 是否隐藏最小化状态下的tooltip显示,默认为false
  shouldShowUploadButton:boolean; // 是否显示知识库上传按钮,默认为true
}
typescript
interface IAgentChatClassNameType {
  chatTitle?:string; // 自定义头部样式,选填
  chatWelcome?:string;  // 自定义欢迎页样式,选填
  chatQuestion?:string; // 自定义默认问题样式,选填
  chatAction?:string; // 自定义快捷指令样式,选填
  chatActionMore?:string; // 自定义快捷指令“更多”,单个按钮样式,选填
  chatInput?:string;  // 自定义输入框样式,选填
}

enum ILocaleType {
  // 中文简体变体
  zh: 'zh',
  'zh-cn': 'zh',
  zh_cn: 'zh',
  chinese: 'zh',
  chs: 'zh',
  'zh-hans': 'zh',
  // 中文繁体变体
  tw: 'tw',
  'zh-tw': 'tw',
  zh_tw: 'tw',
  'zh-hk': 'tw',
  'zh-mo': 'tw',
  'zh-hant': 'tw',
  taiwanese: 'tw',
  cht: 'tw',
  // 英文变体
  en: 'en',
  'en-us': 'en',
  en_us: 'en',
  'en-gb': 'en',
  'en-au': 'en',
  'en-ca': 'en',
  english: 'en',
  eng: 'en',
}

interface IExtraButton {
  element:ReactNode;  // 自定义按钮组件,必填参数
  className?:string;  // 自定义按钮包裹层自定义样式,选填
  title?:string;  // 自定义按钮标题,选填
  text?:string; // 自定义按钮文本,选填
  action?: (data: ImessageData) => void;  // 自定义按钮回调事件,选填
}

interface ImessageData {
  message_id:string; // 消息唯一ID
}

代码示例

修改欢迎页配置

javascript
const AgentChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: "×××××××××", // 填入需要使用的智能体ID
    welcomeMessage:"欢迎来到对话页欢迎页界面,请开始你的操作."  // 修改欢迎页文案
    presetQuestions:["默认问题1","自定义问题2","test3"]  // 修改欢迎页默认问题
  });
AgentChat.mount(document.getElementById('agentspace-container')); // 组件渲染

welcome

修改组件语言配置

javascript
const AgentChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: "×××××××××", // 填入需要使用的智能体ID
    locale:"zh-tw" // 切换为中文繁体
  });
AgentChat.mount(document.getElementById('agentspace-container')); // 组件渲染

隐藏顶部操作按钮、最小化tooltip

javascript
const AgentChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: "×××××××××", // 填入需要使用的智能体ID
    hideTitleActions:true,  // 隐藏顶部操作按钮
    hideMinimizeTooltip:true, //  隐藏最小化tooltip显示
  });
AgentChat.mount(document.getElementById('agentspace-container')); // 组件渲染

添加关闭回调事件

javascript
const AgentChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: "×××××××××", // 填入需要使用的智能体ID
    onAgentChatClose:() => {
      // 自定义关闭事件
      console.log("agentspace-chat关闭!");
    }
  });
AgentChat.mount(document.getElementById('agentspace-container')); // 组件渲染

自定义组件样式

javascript
const AgentChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: "×××××××××", // 填入需要使用的智能体ID
    className:"p-4 m-4", // 组件整体自定义样式
    agentChatClassName:{
      chatTitle:'bg-white',
      chatWelcome:'mt-4',
      chatQuestion:'border rounded-md p-2 bg-[#f5f5f5] border-green-200',
      chatAction:'bg-whitet border rounded-md p-2 border-red-200 hover:bg-gray-200',
      chatActionMore:'border rounded-md p-2 border-red-500',
      chatInput:'text-red-500'
    }, //  组件区域自定义样式
  });
AgentChat.mount(document.getElementById('agentspace-container')); // 组件渲染

className

添加自定义按钮及回调事件

javascript
const AgentChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: "×××××××××", // 填入需要使用的智能体ID
    agentExtraButtons: [
      {
        element: <TestCustomButtonAgent />, // 自定义AI消息按钮组件
        className:'flex gap-[6px] rounded-md border-none px-1 py-1 text-xs font-normal text-[#000] hover:bg-[#f4f4f5]',
        action:(data: { message_id: string }) => {
          console.log('智能体测试按钮被点击', data);
        },
      },
    ], // 添加AI消息自定义按钮及回调事件
    userExtraButtons: [
      {
        element: <TestCustomButtonUser />,  // 自定义用户消息按钮组件
        className:'flex gap-[6px] rounded-md border-none px-1 py-1 text-xs font-normal text-[#000] hover:bg-[#f4f4f5]',
        action:(data: { message_id: string }) => {
          console.log('用户测试按钮被点击', data);
        },
      },
    ],
  });
AgentChat.mount(document.getElementById('agentspace-container')); // 组件渲染

button

自定义AI消息组件渲染及对话数据获取

传入的自定义AI消息组件必须包含可接收message和onChatMessage的props,用于消息数据的获取和对话流式数据的监听

typescript
/**
 * 自定义 AI 消息组件 props定义
 */
interface ICustomAiMessageProps {
  message?: IMessage; // 会话消息数据
  onChatMessage?: (callback: (data: IChatMessage) => void) => () => void; // 对话数据监听
  // 其他props
  ......
}

/**
 * 自定义 AI 消息组件示例
 */
import { useState, useEffect } from 'react';
import type { IMessage, IChatMessage } from '@/types';

const TestCustomAiMessageComponent: React.FC<ICustomAiMessageProps> = ({
  message,
  onChatMessage,
}) => {
  const [streamContent, setStreamContent] = useState<string>('');

  // 监听流式消息
  useEffect(() => {
    if (!onChatMessage || message?.category === 'message') return;

    const unsubscribe = onChatMessage((data: IChatMessage) => {
      if (data.type === 'answer') {
        // 完整消息
        setStreamContent(data.content || '');
      } else if (data.type === 'answer_chunk') {
        // 流式片段
        setStreamContent((prev) => prev + (data.content_chunk || ''));
      }
    });

    return unsubscribe;
  }, [onChatMessage, message?.category]);

  // 决定显示内容
  const displayText = message?.category === 'message' 
    ? message?.text || '' 
    : streamContent || message?.text || '';

  return (
    <div
      style={{
        borderRadius: '6px',
        padding: '8px 12px',
        maxWidth: '100%',
        display: 'inline-block',
        whiteSpace: 'pre-wrap',
        wordBreak: 'break-word',
        color: '#2563eb',
        backgroundColor: '#eff6ff',
        border: '1px solid #bfdbfe',
        fontSize: '14px',
        lineHeight: '1.6',
      }}
    >
      {displayText}
    </div>
  );
};

export default TestCustomAiMessageComponent;


关键类型定义

typescript
// 事件数据类型定义
interface EventMessage {
  type: string;
  content: string;
  content_type: string;
  seq_id: number;
  step: number;
  parent_step: number;
  timestamp: string;
  run_id: string;
  extra_info?: Record<string, any>;
};

// 工具类型定义
interface Tools {
  id: string;
  name: string;
  version: string;
  display_name: string;
}

// 消息数据类型定义
interface IMessage {
  chat_id?: string; // 聊天会话ID
  flow_id: string;
  text: string; // 消息文本内容
  sender: string;
  sender_name: string;
  session_id: string; // 会话ID
  timestamp: string;
  files: Array<string>;
  id: string;
  frontend_id: string;
  edit: boolean;
  background_color: string;
  text_color: string;
  category?: string;
  properties?: any;
  content_blocks?: ContentBlock[];  // 内容块列表(结构化内容展示)
  event_messages?: EventMessage[];  // 消息相关事件列表
  wps_tools: Tools[]; // WPS 工具调用列表
  role?: string;
  knowledges?: string;
  isError?: boolean;
};

// 对话数据类型定义
interface IChatMessage {
  role: 'user' | 'assistant'; // 对话ID
  type: string;
  run_id: string;
  content: string;
  content_chunk: string;
  content_type: string;
  seq_id: number;
  step: number;
  parent_step: number;
  chat_id: string;
  session_id: string;
  timestamp: string;
  extra_info: Record<string, any>;
  message_id: string;
}

自定义AI消息组件消息数据获取和渲染说明

typescript
const TestCustomAiMessageComponent: React.FC<ICustomAiMessageProps> = ({
  message,
  onChatMessage,
}) => {
  // 其他状态处理和函数

  // 监听流式消息
  useEffect(() => {
    const unsubscribe = onChatMessage((data: IChatMessage) => {
       // 处理流式数据,固定判断为 message?.category ==='message',表达式为false时该组件在对话中
      if(message?.category ==='message') return;
      if (data.type === 'answer') {
        // 完整消息
        setStreamContent(data.content || '');
      } else if (data.type === 'answer_chunk') {
        // 流式片段
        setStreamContent((prev) => prev + (data.content_chunk || ''));
      }
    });

    return unsubscribe;
  }, [onChatMessage, message?.category]);

  // 根据固定判断 message?.category === 'message'和自定义状态进行选择,可以选择渲染消息历史数据,或者使用自定义数据进行渲染
  // 示例:处于对话状态中时渲染自定义状态的数据,否则渲染历史消息数据
  const displayText = message?.category === 'message' 
    ? message?.text || '' 
    : streamContent || message?.text || '';

  return (
    <div
      style={{
        borderRadius: '6px',
        padding: '8px 12px',
        maxWidth: '100%',
        display: 'inline-block',
        whiteSpace: 'pre-wrap',
        wordBreak: 'break-word',
        color: '#2563eb',
        backgroundColor: '#eff6ff',
        border: '1px solid #bfdbfe',
        fontSize: '14px',
        lineHeight: '1.6',
      }}
    >
      {displayText}
    </div>
  );
};

初始化定义及组件渲染

javascript
const AgentChat = OpenSDK.create('kso-agentspace-chat', {
    agentId: "×××××××××", // 填入需要使用的智能体ID
    onChatMessageChannel: 'test-chat-message-channel',  // 设置监听频道号,唯一字符串即可
    customAiMessageComponent: <TestCustomAiMessageComponent />,  // 传入自定义的AI消息组件
  });
AgentChat.mount(document.getElementById('agentspace-container')); // 组件渲染

message