准备账号
- 已微信认证的公众号(需要使用到客服消息):https://mp.weixin.qq.com/
- FastGpt账号,及知识库:https://fastgpt.run.
- Laf账号: https://laf.run/
Step1: Laf 准备
1. 进入Laf官网,注册账号
2. 新建应用,直接新建免费的进行测试
3. 复制代码
直接复制,先不需要改动任何内容
import cloud from '@lafjs/cloud';
import * as crypto from 'crypto';
// 公众号配置
const appid = 'wxb1833715d8f0809d'
const appsecret = 'fd76ce714a8083112100c2160b2f2c5d'
const wxToken = 'test';
// fastgpt配置
const apikey = "63f9a14228d2a688d8dc9e1b-skmzjonmv1gyno2iyky1z"
const modelId = "642adec15f01d67d4613efdb"
// 创建数据库连接并获取Message集合
const db = cloud.database();
const _ = db.command
const Message = db.collection('messages')
// 处理接收到的微信公众号消息
export async function main(event) {
// const res = await cloud.fetch.post(` https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${await getAccess_token()}`, {
// button: [
// {
// "type": "click",
// "name": "清空记录",
// "key": "CLEAR"
// },
// ]
// })
const { signature, timestamp, nonce, echostr } = event.query;
// 验证消息是否合法,若不合法则返回错误信息
if (!verifySignature(signature, timestamp, nonce, wxToken)) {
return 'Invalid signature';
}
// 如果是首次验证,则返回 echostr 给微信服务器
if (echostr) {
return echostr;
}
// -------------- 正文开始
const payload = event.body.xml;
const sessionId = payload.fromusername[0]
console.log(payload)
// 点击了清空记录
if (payload.msgtype[0] === 'event' && payload.eventkey[0] === 'CLEAR') {
console.log(1111)
await Message.where({ sessionId: sessionId }).remove({ multi: true })
await replyBykefu('记录已清空', sessionId)
return 'clear record'
}
// 仅做文本消息例子
if (payload.msgtype[0] !== 'text') return 'no text'
const newMessage = {
msgid: payload.msgid[0],
question: payload.content[0].trim(),
username: payload.fromusername[0],
sessionId,
createdAt: Date.now()
}
await replyText(newMessage, payload.fromusername[0])
return 'success'
}
// 处理文本回复消息
async function replyText(message, touser) {
const { question, sessionId, msgid } = message;
// 重复的内容,不回复
const { data: msg } = await Message.where({ msgid: message.msgid }).getOne()
if (msg) return
console.log("收到用户消息", touser, message)
// 立即添加一条待回复记录
await Message.add(message);
// 回复提示
await replyBykefu("🤖机器人正在思考🤔中...", sessionId)
await changesState(sessionId)
const reply = await getFastGptReply(question, sessionId);
const { answer } = reply;
await Message.where({ msgid: message.msgid }).update({
answer,
});
// return answer;
await replyBykefu(answer, touser)
}
// 获取微信公众号ACCESS_TOKEN
async function getAccess_token() {
const shared_access_token = await cloud.shared.get("mp_access_token")
if (shared_access_token) {
if (shared_access_token.exp > Date.now()) {
return shared_access_token.access_token
}
}
// ACCESS_TOKEN不存在或者已过期
// 获取微信公众号ACCESS_TOKEN
const mp_access_token = await cloud.fetch.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`)
cloud.shared.set("mp_access_token", {
access_token: mp_access_token.data.access_token,
exp: Date.now() + 7100 * 1000
})
return mp_access_token.data.access_token
}
// 公众号客服回复文本消息
export async function replyBykefu(message, touser) {
// 判断是否为中文字符
function isChinese(char) {
return /[\u4e00-\u9fa5]/.test(char) // 判断是否是中文字符
}
// 拆分文本长度
function splitText(text) {
let result = []
let len = text.length
let index = 0
while (index < len) {
let part = ''
let charCount = 0
while (charCount < 800 && index < len) {
let char = text[index]
charCount++
part += char
if (isChinese(char)) charCount++ // 中文字符计数+1
index++
}
result.push(part)
}
return result
}
// 定义休眠函数
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) };
const access_token = await getAccess_token()
let text = splitText(message)
let len = splitText(message).length
try{
for (let i = 0; i < len; i++) {
let part = text[i] // 获取第 i 段
await sleep(1000)
// 回复消息
const res = await cloud.fetch.post(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${access_token}`, {
"touser": touser,
"msgtype": "text",
"text":
{
"content": part
}
})
}
}catch(err) {
console.log(err)
}
}
// 修改公众号回复状态
export async function changesState(touser) {
const access_token = await getAccess_token()
// 修改正在输入的状态
const res = await cloud.fetch.post(`https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=${access_token}`, {
"touser": touser,
"command": "Typing"
})
}
// 校验微信服务器发送的消息是否合法
export function verifySignature(signature, timestamp, nonce, token) {
const arr = [token, timestamp, nonce].sort();
const str = arr.join('');
const sha1 = crypto.createHash('sha1');
sha1.update(str);
return sha1.digest('hex') === signature;
}
// 返回组装 xml
export 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>
`
}
// 调用 fastgpt 回答
async function getFastGptReply(question, sessionId) {
const res = await db.collection('messages')
.where({ sessionId })
.get()
// 获取最多10组上下文
const list = res.data.slice(-10)
const prompts = list.map((item) => [{
obj: "Human",
value: item.question || ''
}, {
obj: "AI",
value: item.answer || ''
}]).concat({
obj: "Human",
value: question
}).flat()
const config = {
method: 'post', // 设置请求方法为POST
url: 'https://fastgpt.run/api/openapi/chat/chat', // 设置请求地址
headers: { // 设置请求头信息
apikey,
'Content-Type': 'application/json'
},
data: { // 设置请求体数据
modelId,
isStream: false,
prompts
}
}
try {
const ret = await cloud.fetch(config)
console.log("fastgpt响应", ret.data)
return { answer: ret.data.data || ret.data || '' }
} catch (e) {
console.log("出错了", e.response)
return {
error: "问题太难了 出错了. (uДu〃).",
}
}
}
5.点击发布
6.复制地址
Step2 公众号准备
务必是要认证后的公众号,否则无效。我没有认证的号,所以视频是用测试号展示的。下面截图是完整步骤:
1.给公众号设置laf地址
2. 获取wx appId和secret
3. 验证laf地址
把前面两步获取到的3个内容,填写到laf
Step3 FastGpt准备
1. 获取apikey
2. 获取modelId
3. 替换Laf变量
测试
给公众号发送一条消息,看是否有回复。
QA
发送消息后无响应
先去laf日志检查是否收到用户消息,有下面的提示代表正常。 可能需要点下搜索才能刷新出来。
如果收到了消息,但是没有回复,八成是公众号没有发送客服消息权限。对应是下图的权限