
/
企业微信 ChatGPT 应用
1.1k
开发一个企业微信应用,接入 ChatGPT 支持智能聊天
- README.md
用 JavaScript 开发企业微信 ChatGPT 应用(含全部源码,免费托管,手把手教程)
更新提示: 由于目前 AirCode 无法再提供配置应用固定 IP 服务, 在第三步:配置应用的 API 接收消息以及企业可信 IP 处请使用 Nginx 代理方式配置域名对应的 IP。
本文将帮助你快速实现一个企业微信聊天应用,并且接入 ChatGPT。(效果截图如下)
你将学会
- 创建企业微信应用,如何配置接收消息 URL、企业可信 IP、解密消息
- 使用 AirCode 的「Get a copy」(一键复刻应用)功能,实现应用的聊天能力
- 给聊天应用接入 ChatGPT 能力
第一步:创建聊天应用
- 通过企业微信扫码登录企业微信管理后台。
- 创建应用后会进入应用详情页,点击第二行 Secret 栏的“查看”链接,弹窗后点击“发送”,Secret 会发送到你的企业微信中,收到后请复制保留备用。
第二步:"Get a copy" 创建 AirCode 应用
- 通过 AirCode 源码链接(当前页)右上角的「Get a copy」按钮快速生成一个自己的企业微信 ChatGPT 应用 AirCode 应用。
- 如果没有登录,需先登录 AirCode,可以直接使用 GitHub 或 Google 授权登录,登录之后会重新弹窗创建当前应用。
- 在弹出的对话框中,使用默认应用名称或输入新的应用名称,并点击 Create 完成创建。应用创建成功后会进入 /dashboard 页面,AirCode 需要一点时间来安装依赖(如下图 2 所示),请耐心等待。
- 将第一步创建聊天应用获得的企业 ID 以及接收到的 Secret,粘贴到刚创建的 AirCode 应用 /dashboard 页面的 Environments 环境变量中(上张截图右侧),在 CorpId 和 CorpSecret 栏的 value 处分别填入粘贴过来的企业 ID 和 应用 ChatGPT 的 Secret 的值。
第三步:配置应用的 API 接收消息以及企业可信 IP
- 企业微信后台 应用管理 栏下点击刚刚创建的 ChatGPT 应用,在功能栏 "接收消息" 模块中点击 "设置 API 接收"。
- 点击 Token 和 EncodingAESKey 输入框右侧的 “随机获取”按钮(先不要点击下方 "保存" 按钮,第一行的 URL 将在下一步获得) 能获取到后台随机生成的 Token 和 EncodingAESKey 值(可复制保留备用),将这两个值粘贴到刚应用环境变量(Environments)中 Token 和 EncodingAESKey 栏的 value 处。
- 配置好环境变量(Environments)后,点击页面上方的「Deploy 按钮」部署整个应用,使所有配置生效。等待 AirCode 部署成功后,将 chat.js 文件对应的调用链接复制粘贴至上一步 "接收消息服务器配置" 中的 URL 栏,并点击保存按钮,配置成功截图如下面图 3 所示:
注意:由于企业微信验证 URL 会校验域名主体,当前 Demo 使用企业并未认证能正常配置,如果你的企业已完成认证,这里会因为无法通过 URL 域名校验无法保存出现如下报错,目前没有更好的方式能解决这个问题,如果你有更好的方案欢迎反馈。
如果遇到下面图中域名主体校验不通过的情况,可以尝试找一台在当前企业认证域名下的服务器,利用反向代理工具例如 Nginx 将这个域名的请求转发到 AirCode,在这个 URL 输入框中配置你的域名转发 URL 链接来完成这步配置。
企业微信应用仅后台配置的 IP 可调用回复接口,AirCode 目前可以给指定的香港节点应用配置固定 IP,可以通过填写申请表单方式提交,在申请前请确保上一步「接收消息服务器配置」成功,并且正确填写邮箱和你当前的 AirCode 应用 ID (是一个 6 位长度字符串,不要填写应用名称或企业微信的应用 ID)。 由于配置固定 IP 需人工操作,提交后一个工作日内会进行配置,请耐心等待邮件反馈 IP 地址。在获得固定 IP 后在应用详情页 “开发者接口” 栏的 “企业可信IP” 模块里点击 “配置” 链接,在弹窗中粘贴该 IP 串,按确认键保存。
AirCode 不再提供 IP 配置服务,请使用 Nginx 转发方式填写域名 IP。
第四步:测试聊天应用
- 打开你的企业微信 - 工作台中(拉到最底下),点击你的应用 ChatGPT 进入聊天框。由于还没有配置 ChatGPT 能力,AirCode 应用会直接将你发送的消息返回,这时表示应用已经配置成功。
如果遇到在后台配置 URL 报错或测试应用回复信息时无响应的情况,可以在 AirCode 右侧 Logs tab 下(如下图)查看日志(展开具体报错信息)排查原因。
第五步:接入 ChatGPT 能力
- 到 OpenAI 的控制台中,点「Create new secret key」生成并且复制这个新生成的 Key,粘贴到刚创建的 AirCode 应用的环境变量(Environments)中,粘贴到 OpenAIKey 的 value 中。如果没有 OpenAI 账号,可以到网络中搜索一下获取方式,提前购买准备好。
- 点击上方 Deploy 按钮再次部署让环境变量生效,在企业微信里给应用 ChatGPT 发送消息测试 ChatGPT 的回复。
问题反馈
- 微信、钉钉、飞书等用户交流群,点击 https://docs-cn.aircode.io/help/
更多阅读
- 企业微信、钉钉、飞书、iOS Siri 接入 ChatGPT 手把手教程,全部源码,免费托管,点击 https://docs-cn.aircode.io/chatgpt/
- api.js
1// @see https://docs.aircode.io/guide/functions/ 2const xml2js = require('xml2js'); 3const axios = require('axios'); 4 5const { Configuration, OpenAIApi } = require("openai"); 6const { decrypt } = require('@wecom/crypto'); 7 8const { EncodingAESKey, OpenAIKey, CorpSecret, CorpId } = process.env; 9 10// 转换 xml 格式数据 11const parseXML = (data) => { 12 13 const parser = new xml2js.Parser(); 14 15 return parser.parseStringPromise(data).then(function (result) { 16 17 console.dir(result); 18 return result; 19 }) 20 .catch(function (err) { 21 console.error(err); 22 }); 23 24}; 25 26// 从 buffer 格式数据获取消息内容 27const getMessageFromBuffer = async (buf) => { 28 29 try { 30 31 // 将 buffer 数据转换成 string 格式数据 32 const xmlData = buf.toString('utf8'); 33 const result = await parseXML(xmlData); 34 35 const { xml } = result; 36 const { Encrypt } = xml; 37 // 解密数据 38 const data = decrypt(EncodingAESKey, Encrypt[0]); 39 const { message } = data; 40 41 const content = await parseXML(message); 42 43 console.log('转换 xml 格式数据: ', content.xml); 44 return content.xml; 45 46 } catch (error) { 47 console.error('获取消息内容报错', error); 48 return { 49 message: `获取消息内容报错: ${error.message}`, 50 } 51 } 52} 53 54 55// 获取 access token https://developer.work.weixin.qq.com/document/path/91039 56const getAccessToken = async () => { 57 58 try { 59 const { data } = await axios(`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${CorpId}&corpsecret=${CorpSecret}`); 60 console.log('access token', data); 61 const { errcode, access_token, errmsg } = data; 62 63 if (errcode === 0 && errmsg === 'ok') { 64 return access_token; 65 } else { 66 console.error(`获取 access token 报错:: ${errmsg}`); 67 return { 68 code: errcode, 69 message: `获取 access token 报错: ${errmsg}`, 70 } 71 } 72 73 } catch (error) { 74 console.log('error', error); 75 return { 76 message: `获取 access token 未知报错: ${error.message}`, 77 } 78 } 79 80} 81 82// 回复信息 https://developer.work.weixin.qq.com/document/path/90236 83const sendMessage = async (accessToken, { touser, msgtype, agentid, text }) => { 84 85 try { 86 const { data } = await axios({ 87 method: 'post', 88 url: `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`, 89 data: { 90 touser, 91 msgtype, 92 agentid, 93 text, 94 }, 95 }); 96 const { errcode, errmsg } = data; 97 console.log('回复消息数据: ', data); 98 if (errcode === 0 && errmsg === 'ok') { 99 console.log('成功回复消息'); 100 } else { 101 console.error(`回复消息报错: ${errmsg}`); 102 return { 103 code: errcode, 104 message: `回复消息报错: ${errmsg}`, 105 } 106 } 107 108 } catch (error) { 109 return { 110 message: `回复消息未知报错: ${error.message}`, 111 } 112 } 113} 114 115// 使用 openAI 接口调用 ChatGPT 116const getOpenAIChatCompletion = async (question) => { 117 118 try { 119 120 const configuration = new Configuration({ 121 apiKey: OpenAIKey, 122 }); 123 124 const openai = new OpenAIApi(configuration); 125 126 const completion = await openai.createChatCompletion({ 127 // 使用当前 OpenAI 开放的最新 3.5 模型,如果后续 4 发布,则修改此处参数即可 128 // OpenAI models 参数列表 https://platform.openai.com/docs/models 129 model: "gpt-3.5-turbo", 130 messages: [{ role: "assistant", content: question }], 131 }); 132 133 console.log('ChatGPT completion 数据:', completion.data.choices[0].message.content.trim()); 134 return completion.data.choices[0].message.content.trim(); 135 136 } catch(error) { 137 138 console.error(`OpenAI 接口报错: ${error.message}`); 139 return { 140 code: 1, 141 message: `OpenAI 接口获取信息报错: ${error.message}` 142 } 143 } 144} 145 146module.exports = { 147 getAccessToken, 148 sendMessage, 149 getMessageFromBuffer, 150 getOpenAIChatCompletion, 151} 152
- chat.js
1// @see https://docs.aircode.io/guide/functions/ 2const aircode = require('aircode'); 3const { decrypt, getSignature } = require('@wecom/crypto'); 4 5const { getAccessToken, sendMessage, getOpenAIChatCompletion, getMessageFromBuffer } = require('./api.js'); 6 7let preMessageId = ''; 8 9module.exports = async function(params, context) { 10 11 const { method } = context; 12 const { Token, OpenAIKey, EncodingAESKey } = process.env; 13 14 // 检查环境变量是否成功设置 15 const envVars = { 16 CorpId: '请检查你的环境变量 CorpId 值, 请在 https://work.weixin.qq.com/wework_admin/frame#profile/enterprise 获取到企业ID', 17 Token: '请检查你的环境变量 Token 值,请在后台应用详情页接收消息模块配置生成', 18 EncodingAESKey: '请检查你的环境变量 EncodingAESKey 值,请在后台应用详情页接收消息模块配置生成', 19 CorpSecret: '请检查你的环境变量 CorpSecret 值, 请在 https://work.weixin.qq.com/wework_admin/frame#apps) 获取到应用 Secret', 20 }; 21 22 for (const [envVar, errorMessage] of Object.entries(envVars)) { 23 if (!process.env[envVar]) { 24 console.error(errorMessage); 25 return { 26 code: 1, 27 message: errorMessage, 28 }; 29 } 30 }; 31 32 if (method === 'GET') { 33 34 let { msg_signature, timestamp, nonce, echostr } = params; 35 36 // 企业微信后台接收消息服务器地址配置验证 37 if (echostr) { 38 39 try { 40 41 [msg_signature, timestamp, nonce, echostr] = [msg_signature, timestamp, nonce, echostr].map(decodeURIComponent); 42 43 const signature = getSignature(Token, timestamp, nonce, echostr); 44 45 if (signature !== msg_signature) { 46 return { 47 code: 1, 48 message: `接收消息服务器地址配置验证错误: ${signature} !== ${msg_signature}`, 49 } 50 } 51 52 const msg = decrypt(EncodingAESKey, echostr); 53 return msg['message']; 54 55 } catch (error) { 56 console.error('接收消息服务器地址配置验证错误: ', error.message); 57 return { 58 code: 1, 59 message: `接收消息服务器地址配置验证报错: ${error.message}`, 60 } 61 } 62 } 63 } 64 65 // 回复用户信息 66 if (method === 'POST') { 67 68 const isBuffer = Buffer.isBuffer(params); 69 70 if (isBuffer) { 71 72 const message = await getMessageFromBuffer(params); 73 74 if (message) { 75 const { FromUserName, MsgType, AgentID, Content, MsgId } = message; 76 77 // 企业微信会重复发送信息,这里如果 message ID 相同则不再处理回复 78 const msgId = MsgId[0]; 79 if (msgId === preMessageId) { 80 return 81 } 82 83 preMessageId = msgId; 84 85 const accessToken = await getAccessToken(); 86 let messageContent = Content[0]; 87 88 if (OpenAIKey) { 89 messageContent = await getOpenAIChatCompletion(messageContent); 90 } 91 92 if (!messageContent) { 93 return { 94 code: 1, 95 message: '获取 ChatGPT 回答失败', 96 }; 97 } 98 99 const replyMessage = { 100 touser: FromUserName[0], 101 msgtype: MsgType[0], 102 agentid: AgentID[0], 103 text: { 104 content: messageContent 105 }, 106 } 107 108 const res = await sendMessage(accessToken, replyMessage); 109 return { 110 code: 0, 111 message: '成功回复用户信息' 112 } 113 } 114 } else { 115 return { 116 code: 1, 117 message: `请跟随教程 https://aircode.cool/54fhemjpk2 进行配置,在企业微信应用聊天框输入信息调试代码, 或通过 Debug 功能使用 Buffer 类型数据进行调试`, 118 } 119 } 120 } 121};
- Runtime
- Node.js versionnode/v16
- Function execution timeout80 s
- Dependencies
- @wecom/crypto1.0.1
- aircode0.3.2
- axios1.3.4
- openai3.2.1
- xml2js0.4.23
- Environments
- OpenAIKey
- EncodingAESKey
- Token
- CorpId
- CorpSecret