ChatGpt如何接入钉钉、飞书、第三方应用

ChatGpt如何接入钉钉、飞书、第三方应用

ChatGpt如何接入钉钉、飞书、第三方应用-基于AIRCode

用 JavaScript 五分钟开发一个钉钉 ChatGPT 机器人

  • 本文帮助你快速实现一个钉钉对话机器人,并在其中接入 ChatGPT 的能力,可以直接问它问题,也可以在群聊天中 at 它,返回 ChatGPT 的回答。(以下为效果截图)

    ChatGpt如何接入钉钉、飞书、第三方应用插图

    通过本文你将学会

    1. 创建一个钉钉机器人
    2. 使用 AirCode 的「一键复制代码」功能,实现机器人聊天
    3. 将机器人接入 ChatGPT,实现智能对话

    第一步:创建钉钉机器人

    1. 进入钉钉开发者后台,选择应用开发 > 企业内部开发,点击创建应用按钮,在弹出的对话框中输入名称、简介等信息,完成应用创建。

    ChatGpt如何接入钉钉、飞书、第三方应用插图1

    1. 在创建好的应用页面中,点击左侧菜单的应用功能 > 消息推送,并打开机器人配置

    ChatGpt如何接入钉钉、飞书、第三方应用插图2

    1. 在机器人配置的表单中,依次填入机器人名称、机器人图标、机器人简介、机器人描述和机器人消息预览图,并点击发布按钮完成发布。

      注意:由于钉钉的安全策略,机器人名称中不要包含「ChatGPT」关键字,否则后续无法正常调用。

    ChatGpt如何接入钉钉、飞书、第三方应用插图3

    ChatGpt如何接入钉钉、飞书、第三方应用插图4

    1. 发布成功后,进入基础信息 > 应用信息,可以看到 AppKey 和 AppSecret,点击复制备用。

    ChatGpt如何接入钉钉、飞书、第三方应用插图5

    第二步:创建 AirCode 应用

    1. 通过 AirCode 源码链接中右上角的「Get a copy」按钮快速生成一个自己的 AirCode Node.js 应用。 注意不要直接复制代码,如果是直接复制纯代码粘贴过去,需要再手工安装 NPM 依赖包。 如果没有登录,需先登录 AirCode。推荐使用 GitHub 登录,会快一些。

    ChatGpt如何接入钉钉、飞书、第三方应用插图6

    ChatGpt如何接入钉钉、飞书、第三方应用插图7

    1. 在弹出的对话框中,输入应用名称,并点击 Create 完成创建。

    ChatGpt如何接入钉钉、飞书、第三方应用插图8

    1. 钉钉开发者后台中机器人的 AppKey 和 AppSecret,粘贴到 AirCode 应用的环境变量(Environments)中。在 DING_APP_KEY 的 value 中填入 AppKey,在 DING_APP_SECRET 的 value 中填入 AppSecret。

    ChatGpt如何接入钉钉、飞书、第三方应用插图9

    1. 点击顶部的 Deploy 按钮,部署整个应用,使配置生效。

    ChatGpt如何接入钉钉、飞书、第三方应用插图10

    第三步:配置机器人接口和权限

    1. 部署成功后,选择调用文件 chat.js,可以在编辑器函数名称下看到调用 URL,点击复制 URL。

    ChatGpt如何接入钉钉、飞书、第三方应用插图11

    1. 进入钉钉开发者后台中刚刚创建的机器人页面,在应用功能 > 消息推送中,将调用 URL 填写到消息接收地址项,并点击发布

    ChatGpt如何接入钉钉、飞书、第三方应用插图12

    1. 进入基础信息 > 权限管理,在搜索框中输入「企业内机器人发送消息权限」,会看到列表中找到了对应的权限,点击右侧的申请权限按钮,完成权限配置。

    ChatGpt如何接入钉钉、飞书、第三方应用插图13

    第四步:测试聊天机器人

    1. 完成配置后,在钉钉的聊天窗口中可以搜到机器人进行私聊,或者将机器人加入到群中 at 机器人聊天。此时机器人已经可以对话了,但由于还没有配置 ChatGPT 能力,所以机器人会回复告知需要配置 OPENAI_KEY。

      提示:如果你的机器人返回了类似于「系统正在维护,无法使用 @ 能力」的回复,说明你的机器人名称或简介中包含了「ChatGPT」关键字,被钉钉屏蔽了,更改一下名称或简介后,重新发布即可。

    ChatGpt如何接入钉钉、飞书、第三方应用插图14

    1. 在 AirCode 中选中 chat.js 函数,并点击右侧 Debug 标签下的 Mock by online request 按钮,在弹出对话框中可以看到刚才收到的请求,点击 Use this to debug 则可以使用线上真实的请求数据来调试。

    ChatGpt如何接入钉钉、飞书、第三方应用插图15

    第五步:接入 ChatGPT 能力

    1. 登录到你的 OpenAI 控制台中(如果还没有账号,需要注册一个),进入 API Keys 页面,点击 Create new secret key 创建一个密钥。

    ChatGpt如何接入钉钉、飞书、第三方应用插图16

    1. 在弹出的对话框中,点击复制图标,将这个 API Key 复制并保存下来。注意:正确的 API Key 都是以 sk- 开头的字符串。

    ChatGpt如何接入钉钉、飞书、第三方应用插图17

    1. 进入刚才创建好的 AirCode 应用中,在 Environments 标签页,将复制的 API Key 的值填入 OPENAI_KEY 这一项的 value 中。

    ChatGpt如何接入钉钉、飞书、第三方应用插图18

    1. 再次点击 Deploy 部署应用后,可以在钉钉中测试。目前 ChatGPT 服务比较慢,尤其是模型版本越高级、问题越复杂,ChatGPT 服务的返回时间会越长。

    ChatGpt如何接入钉钉、飞书、第三方应用插图10

    ChatGpt如何接入钉钉、飞书、第三方应用插图19

    问题反馈

    更多阅读

  •  
     

    _utils.js

    1// @see https://docs.aircode.io/guide/functions/
    2const aircode = require('aircode');
    3const crypto = require('crypto');
    4const axios = require('axios');
    5
    6// 从环境变量中获取到钉钉的相关配置
    7const DING_APP_KEY = process.env.DING_APP_KEY || '';
    8const DING_APP_SECRET = process.env.DING_APP_SECRET || '';
    9
    10// 辅助方法,用于根据钉钉的规则生成签名,校验消息合法性
    11function generateSign(timestamp) {
    12  const stringToSign = timestamp + '\n' + DING_APP_SECRET;
    13  const hmac = crypto.createHmac('sha256', DING_APP_SECRET);
    14  hmac.update(stringToSign);
    15  const sign = hmac.digest().toString('base64');
    16  return sign;
    17}
    18
    19// 辅助方法,获取钉钉机器人的 AccessToken
    20async function getAccessToken() {
    21  if (!DING_APP_KEY || !DING_APP_SECRET) {
    22    throw new Error(
    23      '没有正确设置 DING_APP_KEY 和 DING_APP_SECRET 环境变量,请进入 AirCode 中完成设置。'
    24    );
    25  }
    26
    27  // 先从数据库中获取 token 看下是否过期,这样不用每次都发起请求
    28  const TokenTable = aircode.db.table('token');
    29  const item = await TokenTable.where().sort({ expiredAt: -1 }).findOne();
    30  const now = Date.now();
    31
    32  // 如果 token 还在有效期内,则直接返回
    33  if (item && item.expiredAt > now) {
    34    return item.token;
    35  }
    36
    37  // 否则,请求钉钉获取 token
    38  const { data } = await axios.post(
    39    'https://api.dingtalk.com/v1.0/oauth2/accessToken',
    40    {
    41      appKey: DING_APP_KEY,
    42      appSecret: DING_APP_SECRET,
    43    }
    44  );
    45
    46  const token = data.accessToken;
    47  const expiredAt = now + data.expireIn * 1000;
    48
    49  // 将 token 存入数据库
    50  await TokenTable.save({ token, expiredAt });
    51
    52  // 返回 token
    53  return token;
    54}
    55
    56// 辅助方法,用于钉钉发送单聊消息
    57async function sendPrivateMessage(userId, content) {
    58  const token = await getAccessToken();
    59  return axios.post(
    60    'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend',
    61    {
    62      robotCode: DING_APP_KEY,
    63      userIds: [userId],
    64      msgKey: 'sampleText',
    65      msgParam: JSON.stringify({ content }),
    66    },
    67    {
    68      headers: {
    69        'x-acs-dingtalk-access-token': token,
    70      },
    71    }
    72  );
    73}
    74
    75// 辅助方法,用于钉钉发送群聊消息
    76async function sendGroupMessage(conversationId, content) {
    77  const token = await getAccessToken();
    78  return axios.post(
    79    'https://api.dingtalk.com/v1.0/robot/groupMessages/send',
    80    {
    81      robotCode: DING_APP_KEY,
    82      openConversationId: conversationId,
    83      msgKey: 'sampleText',
    84      msgParam: JSON.stringify({ content }),
    85    },
    86    {
    87      headers: {
    88        'x-acs-dingtalk-access-token': token,
    89      },
    90    }
    91  );
    92}
    93
    94// 辅助方法,回复用户的消息
    95async function reply(event, content) {
    96  // 如果没有配置钉钉的 Key 和 Secret,则通过直接返回的形式回复
    97  // 注意这种形式虽然简单,但可能因为超时而无法在钉钉中获得响应
    98  if (!DING_APP_KEY || !DING_APP_SECRET) {
    99    return {
    100      msgtype: 'text',
    101      text: { content },
    102    };
    103  }
    104
    105  // 如果配置了 Key 和 Secret,则通过调用接口回复
    106  // 根据 conversationType 判断是群聊还是单聊
    107  if (event.conversationType === '1') {
    108    // 单聊
    109    await sendPrivateMessage(event.senderStaffId, content);
    110  } else {
    111    // 群聊
    112    await sendGroupMessage(event.conversationId, content);
    113  }
    114  return { ok: 1 };
    115}
    116
    117// 辅助方法,处理错误,生成错误消息
    118function handleError(error) {
    119  let errorMessage;
    120
    121  if (error.response) {
    122    // 如果有 error.response,代表请求发出了,而服务器回复了错误
    123    const { status, statusText, data } = error.response;
    124
    125    if (status === 401) {
    126      // 401 代表 OpenAI 认证失败了
    127      errorMessage =
    128        '你没有配置正确的 OpenAI API Key,请进入 AirCode 中配置正确的环境变量。';
    129    } else if (data.error && data.error.message) {
    130      // 如果 OpenAI 返回了错误消息,则使用 OpenAI 的
    131      errorMessage = data.error.message;
    132    } else {
    133      // 否则,使用默认的错误消息
    134      errorMessage = `Request failed with status code ${status}: ${statusText}`;
    135    }
    136  } else if (error.request) {
    137    // 如果有 error.request,代表请求发出了,但没有得到服务器响应
    138    errorMessage =
    139      'OpenAI 服务器没有响应,可以前往 https://status.openai.com/ 查看其服务状态。';
    140  } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
    141    // 网络错误,例如 DNS 解析错误或者建连失败
    142    errorMessage = `Network error: ${error.message}`;
    143  } else {
    144    errorMessage = error.message;
    145  }
    146
    147  return errorMessage;
    148}
    149
    150module.exports = {
    151  generateSign,
    152  reply,
    153  handleError,
    154};
    155

    chat.js

    1// @see https://docs.aircode.io/guide/functions/
    2const aircode = require('aircode');
    3const { Configuration, OpenAIApi } = require('openai');
    4const { generateSign, reply, handleError } = require('./_utils');
    5
    6// 从环境变量中获取到钉钉和 OpenAI 的相关配置
    7const DING_APP_SECRET = process.env.DING_APP_SECRET || '';
    8const OPENAI_KEY = process.env.OPENAI_KEY || '';
    9// 当前使用的是 OpenAI 开放的最新 GPT-3.5 模型,如果后续 GPT-4 的 API 发布,修改此处参数即可
    10// OpenAI models 参数列表 https://platform.openai.com/docs/models
    11const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-3.5-turbo';
    12
    13// 主方法
    14module.exports = async function (params, context) {
    15  if (context.method !== 'POST') {
    16    // 钉钉机器人消息是 POST 请求,所以忽略所有非 POST 请求
    17    return;
    18  }
    19
    20  // 如果设置了 SECRET,则进行验证
    21  if (DING_APP_SECRET) {
    22    //从 Headers 中拿到 timestamp 和 sign 进行验证
    23    const { timestamp, sign } = context.headers;
    24    if (generateSign(timestamp) !== sign) {
    25      return;
    26    }
    27  }
    28
    29  // 打印请求参数到日志,方便排查
    30  console.log('Received params:', params);
    31
    32  const { msgtype, text, conversationId } = params;
    33
    34  // 示例中,我们只支持文本消息
    35  if (msgtype !== 'text') {
    36    return reply(params, '目前仅支持文本格式的消息。');
    37  }
    38
    39  // 如果没有配置 OPENAI_KEY,则提醒需要配置
    40  if (!OPENAI_KEY) {
    41    return reply(
    42      params,
    43      '恭喜你已经调通了机器人,现在请进入 AirCode 中配置 OPENAI_KEY 环境变量,完成 ChatGPT 连接。'
    44    );
    45  }
    46
    47  // 将用户的问题存入数据表中,后续方便进行排查,或者支持连续对话
    48  const { content } = text;
    49  const ChatsTable = aircode.db.table('chats');
    50  await ChatsTable.save({ conversationId, role: 'user', content });
    51
    52  // 构建发送给 GPT 的消息体
    53  const messages = [
    54    { role: 'system', content: 'You are a helpful assistant.' },
    55    { role: 'user', content },
    56  ];
    57
    58  const openai = new OpenAIApi(new Configuration({ apiKey: OPENAI_KEY }));
    59
    60  try {
    61    // 请求 GPT 获取回复
    62    const completion = await openai.createChatCompletion({
    63      model: OPENAI_MODEL,
    64      messages,
    65    });
    66
    67    const responseMessage = completion.data.choices[0].message;
    68
    69    // 将 ChatGPT 的响应也存入数据库
    70    await ChatsTable.save({ conversationId, ...responseMessage });
    71
    72    // 回复钉钉用户消息
    73    return reply(params, responseMessage.content);
    74  } catch (error) {
    75    // 错误处理,首先打印错误到日志中,方便排查
    76    console.error(error.response || error);
    77
    78    // 根据不同的情况来生成不同的错误信息
    79    const errorMessage = handleError(error);
    80
    81    // 回复错误信息给用户
    82    return reply(params, `错误:${errorMessage}`);
    83  }
    84};
    85

     

THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容