ChatGpt如何接入钉钉、飞书、第三方应用-基于AIRCode
用 JavaScript 五分钟开发一个钉钉 ChatGPT 机器人
-
本文帮助你快速实现一个钉钉对话机器人,并在其中接入 ChatGPT 的能力,可以直接问它问题,也可以在群聊天中 at 它,返回 ChatGPT 的回答。(以下为效果截图)
通过本文你将学会
- 创建一个钉钉机器人
- 使用 AirCode 的「一键复制代码」功能,实现机器人聊天
- 将机器人接入 ChatGPT,实现智能对话
第一步:创建钉钉机器人
- 进入钉钉开发者后台,选择应用开发 > 企业内部开发,点击创建应用按钮,在弹出的对话框中输入名称、简介等信息,完成应用创建。
- 在创建好的应用页面中,点击左侧菜单的应用功能 > 消息推送,并打开机器人配置。
-
在机器人配置的表单中,依次填入机器人名称、机器人图标、机器人简介、机器人描述和机器人消息预览图,并点击发布按钮完成发布。
注意:由于钉钉的安全策略,机器人名称中不要包含「ChatGPT」关键字,否则后续无法正常调用。
- 发布成功后,进入基础信息 > 应用信息,可以看到 AppKey 和 AppSecret,点击复制备用。
第二步:创建 AirCode 应用
- 通过 AirCode 源码链接中右上角的「Get a copy」按钮快速生成一个自己的 AirCode Node.js 应用。 注意不要直接复制代码,如果是直接复制纯代码粘贴过去,需要再手工安装 NPM 依赖包。 如果没有登录,需先登录 AirCode。推荐使用 GitHub 登录,会快一些。
- 在弹出的对话框中,输入应用名称,并点击 Create 完成创建。
- 将钉钉开发者后台中机器人的 AppKey 和 AppSecret,粘贴到 AirCode 应用的环境变量(Environments)中。在 DING_APP_KEY 的 value 中填入 AppKey,在 DING_APP_SECRET 的 value 中填入 AppSecret。
- 点击顶部的 Deploy 按钮,部署整个应用,使配置生效。
第三步:配置机器人接口和权限
- 部署成功后,选择调用文件 chat.js,可以在编辑器函数名称下看到调用 URL,点击复制 URL。
- 进入钉钉开发者后台中刚刚创建的机器人页面,在应用功能 > 消息推送中,将调用 URL 填写到消息接收地址项,并点击发布。
- 进入基础信息 > 权限管理,在搜索框中输入「企业内机器人发送消息权限」,会看到列表中找到了对应的权限,点击右侧的申请权限按钮,完成权限配置。
第四步:测试聊天机器人
-
完成配置后,在钉钉的聊天窗口中可以搜到机器人进行私聊,或者将机器人加入到群中 at 机器人聊天。此时机器人已经可以对话了,但由于还没有配置 ChatGPT 能力,所以机器人会回复告知需要配置 OPENAI_KEY。
提示:如果你的机器人返回了类似于「系统正在维护,无法使用 @ 能力」的回复,说明你的机器人名称或简介中包含了「ChatGPT」关键字,被钉钉屏蔽了,更改一下名称或简介后,重新发布即可。
- 在 AirCode 中选中 chat.js 函数,并点击右侧 Debug 标签下的 Mock by online request 按钮,在弹出对话框中可以看到刚才收到的请求,点击 Use this to debug 则可以使用线上真实的请求数据来调试。
第五步:接入 ChatGPT 能力
- 登录到你的 OpenAI 控制台中(如果还没有账号,需要注册一个),进入 API Keys 页面,点击 Create new secret key 创建一个密钥。
- 在弹出的对话框中,点击复制图标,将这个 API Key 复制并保存下来。注意:正确的 API Key 都是以 sk- 开头的字符串。
- 进入刚才创建好的 AirCode 应用中,在 Environments 标签页,将复制的 API Key 的值填入 OPENAI_KEY 这一项的 value 中。
- 再次点击 Deploy 部署应用后,可以在钉钉中测试。目前 ChatGPT 服务比较慢,尤其是模型版本越高级、问题越复杂,ChatGPT 服务的返回时间会越长。
问题反馈
- 微信、钉钉、飞书等用户交流群,点击 https://docs-cn.aircode.io/help/
更多阅读
- 企业微信、钉钉、飞书、iOS Siri 接入 ChatGPT 手把手教程,全部源码,免费托管,点击 https://docs-cn.aircode.io/chatgpt/
-
_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
请登录后查看评论内容