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

news2024/12/23 14:03:58

前言

在原先使用openAI的接口分别实现过微信聊天,语音对话等功能的基础上,我又将矛头指向了公众号,最近在github中找到了一个挺好玩的案例:公众号机器人,于是打算分享一下整个搭建过程

准备工作

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

搭建过程

openAI

挂vpn进入openAI官网的apikey页面

创建openAI的apikey,点击创建或使用原先的key

复制apikey,找个地方保存一下

 

AirCode

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

应用名可以自取,比如ChatGPT-BOT之类的,环境使用node16,点击创建按钮

 

创建后我们会得到以下工作台,针对常用的工具,做个说明

接着我们将代码复制到代码编辑器中

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: "问题太难了 出错了. (uДu〃).",
    };
  }
}

// 处理文本回复消息
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";
};

当然也可以直接访问仓库进行复制

点击一键复制即可

 

然后我们根据下图在工作区安装依赖,分别有:xml2js,sha1,axios;这里也可以在第二步输入多个依赖一次性安装

 安装完成后控制台有以下提示语句就说明成功了

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

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

公众号

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

进入到配置后将上文的环境变量及云函数的请求地址输入到以下表单即可,输入完成后点击提交按钮,等待一会儿会自动跳转到上一页

最后,点击启用就可以在公众号中使用了

效果

上面的配置都完成后,我们就可以关注公众号和它聊天了

案例的拓展性很强,后续可以实现一些更复杂的玩法,比如传照片,语音等等

当然我的这个例子也是起到抛砖引玉的作用,更多的玩法可以试试结合之前的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实现公众号对话的操作。

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

欢迎关注我的公众号试用(余额有限,用完即止)

本文源码:https://gitee.com/DieHunter/chatGPT-wechat-public

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/615320.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

三、基尔霍夫定理

目录 基本概念 基尔霍夫电流定理&#xff08;KCL&#xff09; 基尔霍夫电压定理&#xff08;KVL&#xff09; 总结 基本概念 1.支路 定义1&#xff1a;电路中每一个两端元件就称为一条支路 定义2&#xff1a;电路中通过同一电流的分支 2.结点 定义1&#xff1a;元件的连接…

spring实例化bean之循环依赖

serviceA里注入了serviceB&#xff0c;serviceB里又注入了serviceA&#xff0c;这样在实例化serviceA时&#xff0c;在doCreateBean时的populateBean时会先实例化serviceB&#xff0c;然后实例化serviceB&#xff0c;在serviceB的doCreateBean方法的populateBean又会去找servci…

什么是Linux shell—一个简单的案例

一句话概括&#xff1a;简单来说脚本就是将需要执行的命令保存到文本中&#xff0c;按照顺序执行&#xff08;由上往下执行&#xff09;&#xff0c;shell脚本:shell脚本就是一些命令的集合。 一、创建第一个Shell脚本&#xff1a;输出helloworld 1&#xff0e;脚本格式 脚本…

(转载)基于遗传模拟退火的聚类算法(matlab实现)

1 理论基础 1.1 模糊聚类分析 模糊聚类是目前知识发现以及模式识别等诸多领域中的重要研究分支之一。随着研究范围的拓展&#xff0c;不管是科学研究还是实际应用&#xff0c;都对聚类的结果从多方面提出了更高的要求。模糊C-均值聚类(FCM)是目前比较流行的一种聚类方法。该…

【不单调的代码】还在嫌弃Ubuntu终端?快来试试做些Ubuntu终端的花式玩法。

&#x1f38a;专栏【​​​​​​​不单调的代码】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【Love Story】 &#x1f970;大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 注意&#xff1a; 本文是在…

【Protobuf速成指南】Protobuf快速上手

文章目录 1.0版本一、编写.proto文件1.文件规范&#xff1a;2.注释方式&#xff1a;3.指定proto3语法&#xff1a;4.package申明符5.定义message6.编写消息字段①类型对照表②唯一编号 二、编译.proto文件1. 编译指令2.源码分析 三、序列化和反序列化的使用四、小结 1.0版本 本…

TCP连接管理与UDP协议

一、TCP的连接管理 1.TCP包头 2.连接的建立——“三次握手” TCP 建立连接的过程叫做握手。 采用三报文握手&#xff1a;在客户和服务器之间交换三个 TCP 报文段&#xff0c;以防止已失效的连接请求报文段突然又传送到了&#xff0c;因而产生 TCP 连接建立错误。 3.连接的释放…

【智能座舱】— 看上海车展,高端品牌变局,时代变天早开始,40项智能化创新技术解密~

大家好,欢迎阅读本期文章,我们将带您解读一份极具实用价值的汽车研究报告。本期将聚焦于2023年上海车展,解密未来座舱发展技术脉络 在本期文章中,我们将深度探讨这些前沿技术的应用,为您呈现未来汽车的全景图。我们相信,这将有助于您更加准确地选择适合自己的新能源汽车…

Kafka测试实战:从基础入门到高阶技巧(建议收藏)

Kafka是一种高吞吐量的分布式发布-订阅消息系统&#xff0c;它可以处理所有活动流数据。在进行Kafka的测试时&#xff0c;我们需要验证生产者能否成功发送消息&#xff0c;消费者能否成功消费消息。在本文中&#xff0c;我们将使用Python来进行Kafka的测试&#xff0c;并提供从…

pytorch实战 -- 数据加载和处理

Pytorch提供了许多工具来简化和希望数据加载&#xff0c;使代码更具可读性。这里将专门讲述transforms数据预处理方法&#xff0c;即数据增强。 数据增强又称为数据增广、数据扩增&#xff0c;它是对训练集进行变换&#xff0c;使训练集更丰富&#xff0c;从而让模型更具泛化能…

POWERBUILDER中高级学习提纲

Chengg0769 2012年 版权来自于&#xff1a; www.mis2erp.com http://blog.csdn.net/chengg0769 http://www.haojiaocheng.cc 转载请保留以上信息 这个提纲的来由&#xff1a; 当时&#xff0c;有个朋友说因伤疗养&#xff0c;想从过去做维护变为做开发&#xff0c;想学习…

从裸机启动开始运行一个C++程序(四)

先序文章请看 从裸机启动开始运行一个C程序&#xff08;三&#xff09; 从裸机启动开始运行一个C程序&#xff08;二&#xff09; 从裸机启动开始运行一个C程序&#xff08;一&#xff09; 跳转 前面我们介绍过&#xff0c;8086CPU总是在执行CS:IP所对应的内存位置的指令&…

签章那些事 -- 让你全面了解签章的流程

前言 随着通信、互联网技术的发展&#xff0c;人们接触到的信息纷繁复杂&#xff0c;信息的真真假假让人难以辨认。在严肃性场合&#xff0c;比如电子合同、电子证照等&#xff0c;必须有一种手段确保信息的完整性和真实性&#xff0c;这时签章就派上了用场。 签章的技术原理并…

高频面试八股文用法篇(四) 乐观锁和悲观锁的例子

目录 什么是乐观锁和悲观锁 乐观锁的实现方式主要有两种&#xff1a;CAS机制和版本号机制 1&#xff09;CAS&#xff08;Compare And Swap&#xff09; (2)版本号 乐观锁适用场景 乐观锁和悲观锁优缺点 功能限制 竞争激烈程度 什么是乐观锁和悲观锁 乐观锁&#xff1…

录音软件哪个好用?录音软件免费下载安装

案例&#xff1a;有没有好用的录音软件推荐&#xff1f; 【我想录制电脑上的音乐和音频会议&#xff0c;也想用电脑录制自己的歌声&#xff0c;有没有好用的电脑录音软件推荐&#xff1f;】 在日常生活和工作中&#xff0c;我们经常会遇到需要录音的场景&#xff0c;比如会议…

几行代码,轻松教你用Java 将 Word 文档转换为 HTML

Aspose.Words 是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。此外&#xff0c; Aspose API支持流行文件格式处…

基于windows环境利用VS下通过Linux环境下服务器进行UDP通信交流

目录 前言 Linux udpServer.cc udpServer.hpp makefile windows 细节1 -- 头文件引入 细节2 -- 固定写法 细节3 -- 结束后清理 细节4 -- socket返回值接受 细节5 -- 套接字创建(一样的写法) 细节6 -- 填写sockaddr_in结构体 细节7 -- 接发收数据 细节8 -- 报错信…

自学黑客(网络安全),一般人我还是劝你算了

一、自学网络安全学习的误区和陷阱 1.不要试图先成为一名程序员【以编程为基础的学习】再开始学习 我在之前的回答中&#xff0c;我都一再强调不要以编程为基础再开始学习网络安全&#xff0c;一般来说&#xff0c;学习编程不但学习周期长&#xff0c;而且实际向安全过渡后可…

操作系统复习2.2.4-作业/进程调度算法

算法 FCFS先来先服务、SJF短作业优先、HRRN高响应比优先、时间片轮转、优先级调度、多级反馈队列调度 FCFS先来先服务 公平&#xff0c;按照到达先后顺序进行服务 用于作业时&#xff0c;考虑哪个作业先到达后备队列 用于进程时&#xff0c;考虑哪个进程先到达就绪队列 非抢…