forked from seandong/ChatGPT-Wechat
-
Notifications
You must be signed in to change notification settings - Fork 0
/
callback.js
265 lines (220 loc) · 7.47 KB
/
callback.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
// @see https://docs.aircode.io/guide/functions/
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;
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);
// 验证是否为重复推送事件
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 === 'event') {
// 公众号订阅
if (payload.Event === 'subscribe') {
return toXML(payload, HELP_MESSAGE);
}
}
// 暂不支持的消息类型
if (payload.MsgType in UNSUPPORTED_MESSAGE_TYPES) {
const responseText = UNSUPPORTED_MESSAGE_TYPES[payload.MsgType];
return toXML(payload, responseText);
}
return 'success'
}