前言

在原先运用openAI的接口别离完成过微信谈天,语音对话等功能的基础上,我又将矛头指向了大众号,最近在github中找到了一个挺好玩的事例:大众号机器人,所以打算共享一下整个建立进程

准备工作

  • 微信大众号
  • AirCode账号
  • OpenAI的apikey
  • 源码
  • Vpn

建立进程

openAI

挂vpn进入openAI官网的apikey页面

无需服务器,5分钟在公众号中接入ChatGPT

创立openAI的apikey,点击创立或运用原先的key

无需服务器,5分钟在公众号中接入ChatGPT

仿制apikey,找个当地保存一下

无需服务器,5分钟在公众号中接入ChatGPT

AirCode

注册账号之类的就不说了,直接创立新的App

无需服务器,5分钟在公众号中接入ChatGPT

应用名能够自取,比方ChatGPT-BOT之类的,环境运用node16,点击创立按钮

无需服务器,5分钟在公众号中接入ChatGPT

创立后咱们会得到以下工作台,针对常用的工具,做个阐明

无需服务器,5分钟在公众号中接入ChatGPT

接着咱们将代码仿制到代码编辑器中

const { db } = require("aircode");
const axios = require("axios");
const sha1 = require("sha1");
const xml2js = require("xml2js");
const TOKEN = process.env.TOKEN || ""; // 微信服务器装备 Token
const OPENAI_KEY = process.env.OPENAI_KEY || ""; // OpenAI 的 Key
const OPENAI_MODEL = process.env.MODEL || "gpt-3.5-turbo"; // 运用的 AI 模型
const OPENAI_MAX_TOKEN = process.env.MAX_TOKEN || 1024; // 最大 token 的值
const LIMIT_HISTORY_MESSAGES = 50; // 限制前史会话最大条数
const CONVERSATION_MAX_AGE = 60 * 60 * 1000; // 同一会话答应最大周期,默许:1 小时
const ADJACENT_MESSAGE_MAX_INTERVAL = 10 * 60 * 1000; //同一会话相邻两条音讯的最大答应距离时间,默许:10 分钟
const UNSUPPORTED_MESSAGE_TYPES = {
  image: "暂不支撑图片音讯",
  voice: "暂不支撑语音音讯",
  video: "暂不支撑视频音讯",
  music: "暂不支撑音乐音讯",
  news: "暂不支撑图文音讯",
};
const WAIT_MESSAGE = `处理中 ... \n\n请稍等几秒后发送【1】查看回复`;
const NO_MESSAGE = `暂无内容,请稍后回复【1】再试`;
const CLEAR_MESSAGE = `✅ 回忆已清除`;
const HELP_MESSAGE = `感谢你的重视,大众号已接入ChatGPT,快来与我对话吧!
指令运用指南
Usage:
    1         查看上一次问题的回复
    /clear    清除上下文
    /help     获取更多帮助
  `;
const Message = db.table("messages");
const Event = db.table("events");
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function toXML(payload, content) {
  const timestamp = Date.now();
  const { ToUserName: fromUserName, FromUserName: toUserName } = payload;
  return `
  <xml>
    <ToUserName><![CDATA[${toUserName}]]></ToUserName>
    <FromUserName><![CDATA[${fromUserName}]]></FromUserName>
    <CreateTime>${timestamp}</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[${content}]]></Content>
  </xml>
  `;
}
async function processCommandText({ sessionId, question }) {
  // 清理前史会话
  if (question === "/clear") {
    const now = new Date();
    await Message.where({ sessionId }).set({ deletedAt: now }).save();
    return CLEAR_MESSAGE;
  } else {
    return HELP_MESSAGE;
  }
}
// 构建 prompt
async function buildOpenAIPrompt(sessionId, question) {
  let prompt = [];
  // 获取最近的前史会话
  const now = new Date();
  // const earliestAt = new Date(now.getTime() - CONVERSATION_MAX_AGE)
  const historyMessages = await Message.where({
    sessionId,
    deletedAt: db.exists(false),
    //  createdAt: db.gt(earliestAt),
  })
    .sort({ createdAt: -1 })
    .limit(LIMIT_HISTORY_MESSAGES)
    .find();
  let lastMessageTime = now;
  let tokenSize = 0;
  for (const message of historyMessages) {
    // 假如前史会话记载大于 OPENAI_MAX_TOKEN 或 两次会话距离超过 10 分钟,则停止增加前史会话
    const timeSinceLastMessage = lastMessageTime
      ? lastMessageTime - message.createdAt
      : 0;
    if (
      tokenSize > OPENAI_MAX_TOKEN ||
      timeSinceLastMessage > ADJACENT_MESSAGE_MAX_INTERVAL
    ) {
      break;
    }
    prompt.unshift({ role: "assistant", content: message.answer });
    prompt.unshift({ role: "user", content: message.question });
    tokenSize += message.token;
    lastMessageTime = message.createdAt;
  }
  prompt.push({ role: "user", content: question });
  return prompt;
}
// 获取 OpenAI API 的回复
async function getOpenAIReply(prompt) {
  const data = JSON.stringify({
    model: OPENAI_MODEL,
    messages: prompt,
  });
  const config = {
    method: "post",
    maxBodyLength: Infinity,
    url: "https://api.openai.com/v1/chat/completions",
    headers: {
      Authorization: `Bearer ${OPENAI_KEY}`,
      "Content-Type": "application/json",
    },
    data: data,
    timeout: 50000,
  };
  try {
    const response = await axios(config);
    console.debug(`[OpenAI response] ${response.data}`);
    if (response.status === 429) {
      return {
        error: "问题太多了,我有点晕厥,请稍后再试",
      };
    }
    // 去除多余的换行
    return {
      answer: response.data.choices[0].message.content.replace("\n\n", ""),
    };
  } catch (e) {
    console.error(e.response.data);
    return {
      error: "问题太难了 出错了. (uu〃).",
    };
  }
}
// 处理文本回复音讯
async function replyText(message) {
  const { question, sessionId, msgid } = message;
  // 查看是否是重试操作
  if (question === "1") {
    const now = new Date();
    // const earliestAt = new Date(now.getTime() - CONVERSATION_MAX_AGE)
    const lastMessage = await Message.where({
      sessionId,
      deletedAt: db.exists(false),
      //  createdAt: db.gt(earliestAt),
    })
      .sort({ createdAt: -1 })
      .findOne();
    if (lastMessage) {
      return `${lastMessage.question}\n------------\n${lastMessage.answer}`;
    }
    return NO_MESSAGE;
  }
  // 发送指令
  if (question.startsWith("/")) {
    return await processCommandText(message);
  }
  // OpenAI 回复内容
  const prompt = await buildOpenAIPrompt(sessionId, question);
  const { error, answer } = await getOpenAIReply(prompt);
  console.debug(
    `[OpenAI reply] sessionId: ${sessionId}; prompt: ${prompt}; question: ${question}; answer: ${answer}`
  );
  if (error) {
    console.error(
      `sessionId: ${sessionId}; question: ${question}; error: ${error}`
    );
    return error;
  }
  // 保存音讯
  const token = question.length + answer.length;
  const result = await Message.save({ token, answer, ...message });
  console.debug(`[save message] result: ${result}`);
  return answer;
}
// 验证是否是重复推送事情
async function checkEvent(payload) {
  const eventId = payload?.MsgId;
  const count = await Event.where({ eventId }).count();
  if (count != 0) {
    return true;
  }
  await Event.save({ eventId, payload });
  return false;
}
// 处理微信事情音讯
module.exports = async function (params, context) {
  const requestId = context.headers["x-aircode-request-id"];
  // 签名验证
  if (context.method === "GET") {
    const _sign = sha1(
      new Array(TOKEN, params.timestamp, params.nonce).sort().join("")
    );
    if (_sign !== params.signature) {
      context.status(403);
      return "Forbidden";
    }
    return params.echostr;
  }
  // 解析 XML 数据
  let payload = params;
  xml2js.parseString(params, { explicitArray: false }, function (err, result) {
    if (err) {
      console.error(`[${requestId}] parse xml error: `, err);
      return;
    }
    payload = result.xml;
  });
  console.debug(`[${requestId}] payload: `, payload);
  // 事情
  if (payload.MsgType === "event") {
    // 大众号订阅
    if (payload.Event === "subscribe") {
      return toXML(payload, HELP_MESSAGE);
    }
  }
  // 验证是否为重复推送事情
  const duplicatedEvent = await checkEvent(payload);
  if (duplicatedEvent) {
    console.error(`[${requestId}] duplicate payload: `, payload);
    return "";
  }
  // 文本
  if (payload.MsgType === "text") {
    const newMessage = {
      msgid: payload?.MsgId,
      question: payload.Content.trim(),
      username: payload.FromUserName,
      sessionId: payload.FromUserName,
    };
    // 修复恳求响应超时问题:假如 5 秒内 AI 没有回复,则回来等待音讯
    const responseText = await Promise.race([
      replyText(newMessage),
      sleep(4000.0).then(() => WAIT_MESSAGE),
    ]);
    return toXML(payload, responseText);
  }
  // 暂不支撑的音讯类型
  if (payload.MsgType in UNSUPPORTED_MESSAGE_TYPES) {
    const responseText = UNSUPPORTED_MESSAGE_TYPES[payload.MsgType];
    return toXML(payload, responseText);
  }
  return "success";
};

当然也能够直接访问库房进行仿制

无需服务器,5分钟在公众号中接入ChatGPT

点击一键仿制即可

无需服务器,5分钟在公众号中接入ChatGPT

然后咱们根据下图在工作区装置依靠,别离有:xml2js,sha1,axios;这里也能够在第二步输入多个依靠一次性装置

无需服务器,5分钟在公众号中接入ChatGPT

装置完成后控制台有以下提示句子就阐明成功了

无需服务器,5分钟在公众号中接入ChatGPT

紧接着,咱们在环境变量中加上刚刚仿制的OPENAI_KEY以及自己自定义一个TOKEN变量,待会在大众号中会用到,比方我这暂时用:test_token来举例

无需服务器,5分钟在公众号中接入ChatGPT

最终点击deploy按钮,仿制api恳求地址(第一次没有,需要先部署一次)

无需服务器,5分钟在公众号中接入ChatGPT

大众号

大众号中的操作就需要用到上面的api地址以及token了

无需服务器,5分钟在公众号中接入ChatGPT

进入到装备后将上文的环境变量及云函数的恳求地址输入到以下表单即可,输入完成后点击提交按钮,等待一瞬间会主动跳转到上一页

无需服务器,5分钟在公众号中接入ChatGPT

最终,点击启用就能够在大众号中运用了

无需服务器,5分钟在公众号中接入ChatGPT

作用

上面的装备都完成后,咱们就能够重视大众号和它谈天了

无需服务器,5分钟在公众号中接入ChatGPT

事例的拓展性很强,后续能够完成一些更杂乱的玩法,比方传相片,语音等等

当然我的这个例子也是起到抛砖引玉的作用,更多的玩法能够试试结合之前的NewBing或者最近比较盛行的claude玩玩,期待各路大佬的迭代

与源码的差异

这个事例是基于GitHub – seandong/ChatGPT-Wechat: 微信大众号 ChatGPT 机器人源码修正的,因为原代码的作者有一段时间没维护了,我在运用时发现了一些问题,做了修正:

1.解析 XML 数据时,payload可能为空,导致进程抛错阻断。解决方案是运用可选链 ?.

2.重复推送事情,导致取消订阅后再订阅时不会显示欢迎句子。解决方案:将订阅事情提前,不做重复判断

题外话

上述用到了AirCode云函数,除此之外推荐一些类似的网站,仅供参考

Glitch: The friendly community where everyone builds the web

laf 云开发

Qoddi.com – Premium Cloud App Hosting Platform

Cloud Application Platform | Heroku

Railway

更多有趣的网站能够经过我的书签获取

写在最终

本文与我们共享了如何运用AirCode建立一个大众号机器人的云函数,经过openAI的api完成大众号对话的操作。

以上就是文章全部内容了,假如觉得文章不错的话,还望三连支撑一下博主,感谢!

欢迎重视我的大众号:阿宇的编程之旅 试用(余额有限,用完即止)

本文源码:gitee.com/DieHunter/c…

参考代码:GitHub – seandong/ChatGPT-Wechat: 微信大众号 ChatGPT 机器人