基于AirCode、ChatGPT-3.5、Node.js、钉钉微服务机器人搭建。
本文帮助你快速实现一个钉钉对话机器人,并在其中接入 ChatGPT 的能力,可以直接问它问题,也可以在群聊天中 at 它,返回 ChatGPT 的回答。(以下为效果截图)
第一步:创建钉钉机器人
- 进入钉钉开发者后台,选择应用开发 > 企业内部开发,点击创建应用按钮,在弹出的对话框中输入名称、 简介等信息,完成应用创建。
- 在创建好的应用页面中,点击左侧菜单的应用功能 > 消息推送,并打开机器人配置。
- 在机器人配置的表单中,依次填入机器人名称、机器人图标、机器人简介、机器人描述和机器人消息预览图,并点击发布按钮完成发布。
注意:由于钉钉的安全策略,机器人名称中不要包含「ChatGPT」关键字,否则后续无法正常调用。 - 发布成功后,进入基础信息 > 应用信息,可以看到 AppKey 和 AppSecret,点击复制备用。
第二步:创建 AirCode 应用
- 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 服务的返回时间会越长。
服务端代码模板里面已经自带了,有些小伙伴需要自己部署的话我就贴下面
_utils.js
// @see https://docs.aircode.io/guide/functions/
const aircode = require('aircode');
const crypto = require('crypto');
const axios = require('axios');// 从环境变量中获取到钉钉的相关配置
const DING_APP_KEY = process.env.DING_APP_KEY || '';
const DING_APP_SECRET = process.env.DING_APP_SECRET || '';// 辅助方法,用于根据钉钉的规则生成签名,校验消息合法性
function generateSign(timestamp) {const stringToSign = timestamp + '\n' + DING_APP_SECRET;const hmac = crypto.createHmac('sha256', DING_APP_SECRET);hmac.update(stringToSign);const sign = hmac.digest().toString('base64');return sign;
}// 辅助方法,获取钉钉机器人的 AccessToken
async function getAccessToken() {if (!DING_APP_KEY || !DING_APP_SECRET) {throw new Error('没有正确设置 DING_APP_KEY 和 DING_APP_SECRET 环境变量,请进入 AirCode 中完成设置。')}// 先从数据库中获取 token 看下是否过期,这样不用每次都发起请求const TokenTable = aircode.db.table('token');const item = await TokenTable.where().sort({ expiredAt: -1 }).findOne();const now = Date.now();// 如果 token 还在有效期内,则直接返回if (item && item.expiredAt > now) {return item.token;}// 否则,请求钉钉获取 tokenconst { data } = await axios.post('https://api.dingtalk.com/v1.0/oauth2/accessToken',{appKey: DING_APP_KEY,appSecret: DING_APP_SECRET,},);const token = data.accessToken;const expiredAt = now + data.expireIn * 1000;// 将 token 存入数据库await TokenTable.save({ token, expiredAt });// 返回 tokenreturn token;
}// 辅助方法,用于钉钉发送单聊消息
async function sendPrivateMessage(userId, content) {const token = await getAccessToken();return axios.post('https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend',{robotCode: DING_APP_KEY,userIds: [ userId ],msgKey: 'sampleText',msgParam: JSON.stringify({ content }),},{headers: {'x-acs-dingtalk-access-token': token,},},);
}// 辅助方法,用于钉钉发送群聊消息
async function sendGroupMessage(conversationId, content) {const token = await getAccessToken();return axios.post('https://api.dingtalk.com/v1.0/robot/groupMessages/send',{robotCode: DING_APP_KEY,openConversationId: conversationId,msgKey: 'sampleText',msgParam: JSON.stringify({ content }),},{headers: {'x-acs-dingtalk-access-token': token,},},);
}// 辅助方法,回复用户的消息
async function reply(event, content) {// 如果没有配置钉钉的 Key 和 Secret,则通过直接返回的形式回复// 注意这种形式虽然简单,但可能因为超时而无法在钉钉中获得响应if (!DING_APP_KEY || !DING_APP_SECRET) {return {msgtype: 'text',text: { content },};}// 如果配置了 Key 和 Secret,则通过调用接口回复// 根据 conversationType 判断是群聊还是单聊if (event.conversationType === '1') {// 单聊await sendPrivateMessage(event.senderStaffId, content);} else {// 群聊await sendGroupMessage(event.conversationId, content);}return { ok: 1 };
}// 辅助方法,处理错误,生成错误消息
function handleError(error) {let errorMessage;if (error.response) {// 如果有 error.response,代表请求发出了,而服务器回复了错误const { status, statusText, data } = error.response;if (status === 401) {// 401 代表 OpenAI 认证失败了errorMessage = '你没有配置正确的 OpenAI API Key,请进入 AirCode 中配置正确的环境变量。'} else if (data.error && data.error.message) {// 如果 OpenAI 返回了错误消息,则使用 OpenAI 的errorMessage = data.error.message;} else {// 否则,使用默认的错误消息errorMessage = `Request failed with status code ${status}: ${statusText}`;}} else if (error.request) {// 如果有 error.request,代表请求发出了,但没有得到服务器响应errorMessage = 'OpenAI 服务器没有响应,可以前往 https://status.openai.com/ 查看其服务状态。';} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {// 网络错误,例如 DNS 解析错误或者建连失败errorMessage = `Network error: ${error.message}`;} else {errorMessage = error.message;}return errorMessage;
}module.exports = {generateSign,reply,handleError,
};
chat.js
// @see https://docs.aircode.io/guide/functions/
const aircode = require('aircode');
const { Configuration, OpenAIApi } = require('openai');
const {generateSign,reply,handleError,
} = require('./_utils');// 从环境变量中获取到钉钉和 OpenAI 的相关配置
const DING_APP_SECRET = process.env.DING_APP_SECRET || '';
const OPENAI_KEY = process.env.OPENAI_KEY || '';
const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-3.5-turbo';// 主方法
module.exports = async function(params, context) {if (context.method !== 'POST') {// 钉钉机器人消息是 POST 请求,所以忽略所有非 POST 请求return;}// 如果设置了 SECRET,则进行验证if (DING_APP_SECRET) {//从 Headers 中拿到 timestamp 和 sign 进行验证const { timestamp, sign } = context.headers;if (generateSign(timestamp) !== sign) {return;}}// 打印请求参数到日志,方便排查console.log('Received params:', params);const { msgtype, text, conversationId } = params;// 示例中,我们只支持文本消息if (msgtype !== 'text') {return reply(params, '目前仅支持文本格式的消息。');}// 如果没有配置 OPENAI_KEY,则提醒需要配置if (!OPENAI_KEY) {return reply(params, '恭喜你已经调通了机器人,现在请进入 AirCode 中配置 OPENAI_KEY 环境变量,完成 ChatGPT 连接。');}// 将用户的问题存入数据表中,后续方便进行排查,或者支持连续对话const { content } = text;const ChatsTable = aircode.db.table('chats');await ChatsTable.save({ conversationId, role: 'user', content });// 构建发送给 GPT 的消息体const messages = [{ role: 'system', content: 'You are a helpful assistant.' },{ role: 'user', content },];const openai = new OpenAIApi(new Configuration({ apiKey: OPENAI_KEY }));try {// 请求 GPT 获取回复const completion = await openai.createChatCompletion({model: OPENAI_MODEL,messages,});const responseMessage = completion.data.choices[0].message;// 将 ChatGPT 的响应也存入数据库await ChatsTable.save({ conversationId, ...responseMessage });// 回复钉钉用户消息return reply(params, responseMessage.content);} catch (error) {// 错误处理,首先打印错误到日志中,方便排查console.error(error.response || error);// 根据不同的情况来生成不同的错误信息const errorMessage = handleError(error);// 回复错误信息给用户return reply(params, `错误:${errorMessage}`);}
}