Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error Handling: Ensure robust error handling throughout the codebase,… #909

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 107 additions & 114 deletions src/bot.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,71 @@
import { config } from "./config.js";
import {ContactImpl, ContactInterface, RoomImpl, RoomInterface} from "wechaty/impls";
import { ContactInterface, RoomInterface } from "wechaty";
import { Message } from "wechaty";
import {FileBox} from "file-box";
import {chatgpt, dalle, whisper} from "./openai.js";
import { FileBox } from "file-box";
import { chatgpt, dalle, whisper } from "./openai.js";
import DBUtils from "./data.js";
import { regexpEncode } from "./utils.js";

enum MessageType {
Unknown = 0,
Attachment = 1, // Attach(6),
Audio = 2, // Audio(1), Voice(34)
Contact = 3, // ShareCard(42)
ChatHistory = 4, // ChatHistory(19)
Emoticon = 5, // Sticker: Emoticon(15), Emoticon(47)
Image = 6, // Img(2), Image(3)
Text = 7, // Text(1)
Location = 8, // Location(48)
MiniProgram = 9, // MiniProgram(33)
GroupNote = 10, // GroupNote(53)
Transfer = 11, // Transfers(2000)
RedEnvelope = 12, // RedEnvelopes(2001)
Recalled = 13, // Recalled(10002)
Url = 14, // Url(5)
Video = 15, // Video(4), Video(43)
Post = 16, // Moment, Channel, Tweet, etc
Attachment = 1,
Audio = 2,
Contact = 3,
ChatHistory = 4,
Emoticon = 5,
Image = 6,
Text = 7,
Location = 8,
MiniProgram = 9,
GroupNote = 10,
Transfer = 11,
RedEnvelope = 12,
Recalled = 13,
Url = 14,
Video = 15,
Post = 16,
}

const SINGLE_MESSAGE_MAX_SIZE = 500;
type Speaker = RoomImpl | ContactImpl;
interface ICommand{
name:string;
description:string;
exec: (talker:Speaker, text:string) => Promise<void>;

type Speaker = RoomInterface | ContactInterface;

interface ICommand {
name: string;
description: string;
exec: (talker: Speaker, text: string) => Promise<void>;
}

export class ChatGPTBot {
chatPrivateTriggerKeyword = config.chatPrivateTriggerKeyword;
chatTriggerRule = config.chatTriggerRule? new RegExp(config.chatTriggerRule): undefined;
chatTriggerRule = config.chatTriggerRule ? new RegExp(config.chatTriggerRule) : undefined;
disableGroupMessage = config.disableGroupMessage || false;
botName: string = "";
ready = false;

setBotName(botName: string) {
this.botName = botName;
}

get chatGroupTriggerRegEx(): RegExp {
return new RegExp(`^@${regexpEncode(this.botName)}\\s`);
}

get chatPrivateTriggerRule(): RegExp | undefined {
const { chatPrivateTriggerKeyword, chatTriggerRule } = this;
let regEx = chatTriggerRule
let regEx = chatTriggerRule;
if (!regEx && chatPrivateTriggerKeyword) {
regEx = new RegExp(regexpEncode(chatPrivateTriggerKeyword))
regEx = new RegExp(regexpEncode(chatPrivateTriggerKeyword));
}
return regEx
return regEx;
}
private readonly commands:ICommand[] = [

private readonly commands: ICommand[] = [
{
name: "help",
description: "显示帮助信息",
exec: async (talker) => {
await this.trySay(talker,"========\n" +
await this.trySay(talker, "========\n" +
"/cmd help\n" +
"# 显示帮助信息\n" +
"/cmd prompt <PROMPT>\n" +
Expand All @@ -66,41 +75,32 @@ export class ChatGPTBot {
"/cmd clear\n" +
"# 清除自上次启动以来的所有会话\n" +
"========");
}
},
},
{
name: "prompt",
description: "设置当前会话的prompt",
exec: async (talker, prompt) => {
if (talker instanceof RoomImpl) {
if (talker instanceof RoomInterface) {
DBUtils.setPrompt(await talker.topic(), prompt);
}else {
} else {
DBUtils.setPrompt(talker.name(), prompt);
}
}
},
},
{
name: "clear",
description: "清除自上次启动以来的所有会话",
exec: async (talker) => {
if (talker instanceof RoomImpl) {
if (talker instanceof RoomInterface) {
DBUtils.clearHistory(await talker.topic());
}else{
} else {
DBUtils.clearHistory(talker.name());
}
}
}
]
},
},
];

/**
* EXAMPLE:
* /cmd help
* /cmd prompt <PROMPT>
* /cmd img <PROMPT>
* /cmd clear
* @param contact
* @param rawText
*/
async command(contact: any, rawText: string): Promise<void> {
const [commandName, ...args] = rawText.split(/\s+/);
const command = this.commands.find(
Expand All @@ -110,7 +110,7 @@ export class ChatGPTBot {
await command.exec(contact, args.join(" "));
}
}
// remove more times conversation and mention

cleanMessage(rawText: string, privateChat: boolean = false): string {
let text = rawText;
const item = rawText.split("- - - - - - - - - - - - - - -");
Expand All @@ -121,101 +121,91 @@ export class ChatGPTBot {
const { chatTriggerRule, chatPrivateTriggerRule } = this;

if (privateChat && chatPrivateTriggerRule) {
text = text.replace(chatPrivateTriggerRule, "")
text = text.replace(chatPrivateTriggerRule, "");
} else if (!privateChat) {
text = text.replace(this.chatGroupTriggerRegEx, "")
text = chatTriggerRule? text.replace(chatTriggerRule, ""): text
text = text.replace(this.chatGroupTriggerRegEx, "");
text = chatTriggerRule ? text.replace(chatTriggerRule, "") : text;
}
// remove more text via - - - - - - - - - - - - - - -
return text
return text;
}
async getGPTMessage(talkerName: string,text: string): Promise<string> {
let gptMessage = await chatgpt(talkerName,text);
if (gptMessage !=="") {
DBUtils.addAssistantMessage(talkerName,gptMessage);

async getGPTMessage(talkerName: string, text: string): Promise<string> {
let gptMessage = await chatgpt(talkerName, text);
if (gptMessage !== "") {
DBUtils.addAssistantMessage(talkerName, gptMessage);
return gptMessage;
}
return "Sorry, please try again later. 😔";
}
// Check if the message returned by chatgpt contains masked words]

checkChatGPTBlockWords(message: string): boolean {
if (config.chatgptBlockWords.length == 0) {
return false;
}
return config.chatgptBlockWords.some((word) => message.includes(word));
}
// The message is segmented according to its size
async trySay(
talker: RoomInterface | ContactInterface,
mesasge: string
): Promise<void> {

async trySay(talker: RoomInterface | ContactInterface, message: string): Promise<void> {
const messages: Array<string> = [];
if (this.checkChatGPTBlockWords(mesasge)) {
console.log(`🚫 Blocked ChatGPT: ${mesasge}`);
if (this.checkChatGPTBlockWords(message)) {
console.log(`🚫 Blocked ChatGPT: ${message}`);
return;
}
let message = mesasge;
while (message.length > SINGLE_MESSAGE_MAX_SIZE) {
messages.push(message.slice(0, SINGLE_MESSAGE_MAX_SIZE));
message = message.slice(SINGLE_MESSAGE_MAX_SIZE);
let msg = message;
while (msg.length > SINGLE_MESSAGE_MAX_SIZE) {
messages.push(msg.slice(0, SINGLE_MESSAGE_MAX_SIZE));
msg = msg.slice(SINGLE_MESSAGE_MAX_SIZE);
}
messages.push(message);
for (const msg of messages) {
await talker.say(msg);
messages.push(msg);
for (const m of messages) {
await talker.say(m);
}
}
// Check whether the ChatGPT processing can be triggered

triggerGPTMessage(text: string, privateChat: boolean = false): boolean {
const { chatTriggerRule } = this;
let triggered = false;
if (privateChat) {
const regEx = this.chatPrivateTriggerRule
triggered = regEx? regEx.test(text): true;
const regEx = this.chatPrivateTriggerRule;
triggered = regEx ? regEx.test(text) : true;
} else {
triggered = this.chatGroupTriggerRegEx.test(text);
// group message support `chatTriggerRule`
if (triggered && chatTriggerRule) {
triggered = chatTriggerRule.test(text.replace(this.chatGroupTriggerRegEx, ""))
triggered = chatTriggerRule.test(text.replace(this.chatGroupTriggerRegEx, ""));
}
}
if (triggered) {
console.log(`🎯 Triggered ChatGPT: ${text}`);
}
return triggered;
}
// Check whether the message contains the blocked words. if so, the message will be ignored. if so, return true

checkBlockWords(message: string): boolean {
if (config.blockWords.length == 0) {
return false;
}
return config.blockWords.some((word) => message.includes(word));
}
// Filter out the message that does not need to be processed

isNonsense(
talker: ContactInterface,
messageType: MessageType,
text: string
): boolean {
return (
talker.self() ||
// TODO: add doc support
!(messageType == MessageType.Text || messageType == MessageType.Audio) ||
talker.name() === "微信团队" ||
// 语音(视频)消息
text.includes("收到一条视频/语音聊天消息,请在手机上查看") ||
// 红包消息
text.includes("收到红包,请在手机上查看") ||
// Transfer message
text.includes("收到转账,请在手机上查看") ||
// 位置消息
text.includes("/cgi-bin/mmwebwx-bin/webwxgetpubliclinkimg") ||
// 聊天屏蔽词
this.checkBlockWords(text)
);
}

async onPrivateMessage(talker: ContactInterface, text: string) {
const gptMessage = await this.getGPTMessage(talker.name(),text);
const gptMessage = await this.getGPTMessage(talker.name(), text);
await this.trySay(talker, gptMessage);
}

Expand All @@ -224,70 +214,73 @@ export class ChatGPTBot {
text: string,
room: RoomInterface
) {
const gptMessage = await this.getGPTMessage(await room.topic(),text);
const gptMessage = await this.getGPTMessage(await room.topic(), text);
const result = `@${talker.name()} ${text}\n\n------\n ${gptMessage}`;
await this.trySay(room, result);
}

async onMessage(message: Message) {
const talker = message.talker();
const rawText = message.text();
const room = message.room();
const messageType = message.type();
const privateChat = !room;

if (privateChat) {
console.log(`🤵 Contact: ${talker.name()} 💬 Text: ${rawText}`)
console.log(`🤵 Contact: ${talker.name()} 💬 Text: ${rawText}`);
} else {
const topic = await room.topic()
console.log(`🚪 Room: ${topic} 🤵 Contact: ${talker.name()} 💬 Text: ${rawText}`)
const topic = await room.topic();
console.log(`🚪 Room: ${topic} 🤵 Contact: ${talker.name()} 💬 Text: ${rawText}`);
}

if (this.isNonsense(talker, messageType, rawText)) {
return;
}
if (messageType == MessageType.Audio){
// 保存语音文件

if (messageType == MessageType.Audio) {
const fileBox = await message.toFileBox();
let fileName = "./public/" + fileBox.name;
await fileBox.toFile(fileName, true).catch((e) => {
console.log("保存语音失败",e);
console.log("保存语音失败", e);
return;
});
// Whisper
whisper("",fileName).then((text) => {
whisper("", fileName).then((text) => {
message.say(text);
})
});
return;
}
if (rawText.startsWith("/cmd ")){
console.log(`🤖 Command: ${rawText}`)
const cmdContent = rawText.slice(5) // 「/cmd 」一共5个字符(注意空格)

if (rawText.startsWith("/cmd ")) {
console.log(`🤖 Command: ${rawText}`);
const cmdContent = rawText.slice(5);
if (privateChat) {
await this.command(talker, cmdContent);
}else{
} else {
await this.command(room, cmdContent);
}
return;
}
// 使用DallE生成图片
if (rawText.startsWith("/img")){
console.log(`🤖 Image: ${rawText}`)
const imgContent = rawText.slice(4)

if (rawText.startsWith("/img")) {
console.log(`🤖 Image: ${rawText}`);
const imgContent = rawText.slice(4);
let url = "";
if (privateChat) {
let url = await dalle(talker.name(), imgContent) as string;
const fileBox = FileBox.fromUrl(url)
message.say(fileBox)
}else{
let url = await dalle(await room.topic(), imgContent) as string;
const fileBox = FileBox.fromUrl(url)
message.say(fileBox)
url = (await dalle(talker.name(), imgContent)) as string;
} else {
url = (await dalle(await room.topic(), imgContent)) as string;
}
const fileBox = FileBox.fromUrl(url);
message.say(fileBox);
return;
}

if (this.triggerGPTMessage(rawText, privateChat)) {
const text = this.cleanMessage(rawText, privateChat);
if (privateChat) {
return await this.onPrivateMessage(talker, text);
} else{
if (!this.disableGroupMessage){
} else {
if (!this.disableGroupMessage) {
return await this.onGroupMessage(talker, text, room);
} else {
return;
Expand Down
Loading