logo
/
企业微信 ChatGPT 应用
Get a copy
1.1k
开发一个企业微信应用,接入 ChatGPT 支持智能聊天
  • README.md
    arrow

    用 JavaScript 开发企业微信 ChatGPT 应用(含全部源码,免费托管,手把手教程)

    更新提示: 由于目前 AirCode 无法再提供配置应用固定 IP 服务, 在第三步:配置应用的 API 接收消息以及企业可信 IP 处请使用 Nginx 代理方式配置域名对应的 IP。

    本文将帮助你快速实现一个企业微信聊天应用,并且接入 ChatGPT。(效果截图如下)

    你将学会

    1. 创建企业微信应用,如何配置接收消息 URL、企业可信 IP、解密消息
    2. 使用 AirCode 的「Get a copy」(一键复刻应用)功能,实现应用的聊天能力
    3. 给聊天应用接入 ChatGPT 能力

    第一步:创建聊天应用

    1. 通过企业微信扫码登录企业微信管理后台

    1. 企业微信管理后台 "应用管理"下点击创建应用,在表单中选择应用 Logo 图片、输入应用名称 ChatGPT,并且选择可见范围(选择部门/成员)完成创建。

    1. 创建应用后会进入应用详情页,点击第二行 Secret 栏的“查看”链接,弹窗后点击“发送”,Secret 会发送到你的企业微信中,收到后请复制保留备用。

    1. 点击上一张截图顶部我的企业 tab 栏,左侧点击 企业信息 查看最底下一栏企业 ID,请复制保留备用。

    第二步:"Get a copy" 创建 AirCode 应用

    1. 通过 AirCode 源码链接(当前页)右上角的「Get a copy」按钮快速生成一个自己的企业微信 ChatGPT 应用 AirCode 应用。

    1. 如果没有登录,需先登录 AirCode,可以直接使用 GitHub 或 Google 授权登录,登录之后会重新弹窗创建当前应用。

    1. 在弹出的对话框中,使用默认应用名称或输入新的应用名称,并点击 Create 完成创建。应用创建成功后会进入 /dashboard 页面,AirCode 需要一点时间来安装依赖(如下图 2 所示),请耐心等待。

    1. 将第一步创建聊天应用获得的企业 ID 以及接收到的 Secret,粘贴到刚创建的 AirCode 应用 /dashboard 页面的 Environments 环境变量中(上张截图右侧),在 CorpId 和 CorpSecret 栏的 value 处分别填入粘贴过来的企业 ID 和 应用 ChatGPT 的 Secret 的值。

    第三步:配置应用的 API 接收消息以及企业可信 IP

    1. 企业微信后台 应用管理 栏下点击刚刚创建的 ChatGPT 应用,在功能栏 "接收消息" 模块中点击 "设置 API 接收"。

    1. 点击 Token 和 EncodingAESKey 输入框右侧的 “随机获取”按钮(先不要点击下方 "保存" 按钮,第一行的 URL 将在下一步获得) 能获取到后台随机生成的 Token 和 EncodingAESKey 值(可复制保留备用),将这两个值粘贴到刚应用环境变量(Environments)中 Token 和 EncodingAESKey 栏的 value 处。

    1. 配置好环境变量(Environments)后,点击页面上方的「Deploy 按钮」部署整个应用,使所有配置生效。等待 AirCode 部署成功后,将 chat.js 文件对应的调用链接复制粘贴至上一步 "接收消息服务器配置" 中的 URL 栏,并点击保存按钮,配置成功截图如下面图 3 所示:

    注意:由于企业微信验证 URL 会校验域名主体,当前 Demo 使用企业并未认证能正常配置,如果你的企业已完成认证,这里会因为无法通过 URL 域名校验无法保存出现如下报错,目前没有更好的方式能解决这个问题,如果你有更好的方案欢迎反馈。

    如果遇到下面图中域名主体校验不通过的情况,可以尝试找一台在当前企业认证域名下的服务器,利用反向代理工具例如 Nginx 将这个域名的请求转发到 AirCode,在这个 URL 输入框中配置你的域名转发 URL 链接来完成这步配置。

    1. 企业微信应用仅后台配置的 IP 可调用回复接口,AirCode 目前可以给指定的香港节点应用配置固定 IP,可以通过填写申请表单方式提交,在申请前请确保上一步「接收消息服务器配置」成功,并且正确填写邮箱和你当前的 AirCode 应用 ID (是一个 6 位长度字符串,不要填写应用名称或企业微信的应用 ID)。 由于配置固定 IP 需人工操作,提交后一个工作日内会进行配置,请耐心等待邮件反馈 IP 地址。在获得固定 IP 后在应用详情页 “开发者接口” 栏的 “企业可信IP” 模块里点击 “配置” 链接,在弹窗中粘贴该 IP 串,按确认键保存。

    AirCode 不再提供 IP 配置服务,请使用 Nginx 转发方式填写域名 IP。

    第四步:测试聊天应用

    1. 打开你的企业微信 - 工作台中(拉到最底下),点击你的应用 ChatGPT 进入聊天框。由于还没有配置 ChatGPT 能力,AirCode 应用会直接将你发送的消息返回,这时表示应用已经配置成功。

    如果遇到在后台配置 URL 报错或测试应用回复信息时无响应的情况,可以在 AirCode 右侧 Logs tab 下(如下图)查看日志(展开具体报错信息)排查原因。

    第五步:接入 ChatGPT 能力

    1. OpenAI 的控制台中,点「Create new secret key」生成并且复制这个新生成的 Key,粘贴到刚创建的 AirCode 应用的环境变量(Environments)中,粘贴到 OpenAIKey 的 value 中。如果没有 OpenAI 账号,可以到网络中搜索一下获取方式,提前购买准备好。

    1. 点击上方 Deploy 按钮再次部署让环境变量生效,在企业微信里给应用 ChatGPT 发送消息测试 ChatGPT 的回复。

    问题反馈

    更多阅读

  • api.js
    arrow
    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
    arrow
    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
    arrow
    • Node.js versionnode/v16
    • Function execution timeout80 s
  • Dependencies
    arrow
    • @wecom/crypto1.0.1
    • aircode0.3.2
    • axios1.3.4
    • openai3.2.1
    • xml2js0.4.23
  • Environments
    arrow
    arrow
    • OpenAIKey
    • EncodingAESKey
    • Token
    • CorpId
    • CorpSecret