更新ChatGPT的处理

main
落雨楓 2 years ago
parent 43753b38a5
commit e11e2a4914

27
package-lock.json generated

@ -17,7 +17,9 @@
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"decoders": "^1.25.3", "decoders": "^1.25.3",
"got": "^11.8.3", "got": "^11.8.3",
"gpt-3-encoder": "^1.1.4",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"hpagent": "^1.2.0",
"koa": "^2.13.4", "koa": "^2.13.4",
"koa-body": "^6.0.1", "koa-body": "^6.0.1",
"koa-router": "^10.1.1", "koa-router": "^10.1.1",
@ -56,7 +58,6 @@
"fastify": "^4.11.0", "fastify": "^4.11.0",
"fetch-undici": "^3.0.1", "fetch-undici": "^3.0.1",
"gpt-3-encoder": "^1.1.4", "gpt-3-encoder": "^1.1.4",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1", "https-proxy-agent": "^5.0.1",
"inquirer": "^9.1.4", "inquirer": "^9.1.4",
"inquirer-autocomplete-prompt": "^3.0.0", "inquirer-autocomplete-prompt": "^3.0.0",
@ -1398,6 +1399,11 @@
"node": ">=10.19.0" "node": ">=10.19.0"
} }
}, },
"node_modules/gpt-3-encoder": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/gpt-3-encoder/-/gpt-3-encoder-1.1.4.tgz",
"integrity": "sha512-fSQRePV+HUAhCn7+7HL7lNIXNm6eaFWFbNLOOGtmSJ0qJycyQvj60OvRlH7mee8xAMjBDNRdMXlMwjAbMTDjkg=="
},
"node_modules/handlebars": { "node_modules/handlebars": {
"version": "4.7.7", "version": "4.7.7",
"resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.7.tgz", "resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.7.tgz",
@ -1490,6 +1496,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/hpagent": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/hpagent/-/hpagent-1.2.0.tgz",
"integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==",
"engines": {
"node": ">=14"
}
},
"node_modules/http-assert": { "node_modules/http-assert": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmmirror.com/http-assert/-/http-assert-1.5.0.tgz", "resolved": "https://registry.npmmirror.com/http-assert/-/http-assert-1.5.0.tgz",
@ -3489,7 +3503,6 @@
"fastify": "^4.11.0", "fastify": "^4.11.0",
"fetch-undici": "^3.0.1", "fetch-undici": "^3.0.1",
"gpt-3-encoder": "^1.1.4", "gpt-3-encoder": "^1.1.4",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1", "https-proxy-agent": "^5.0.1",
"inquirer": "^9.1.4", "inquirer": "^9.1.4",
"inquirer-autocomplete-prompt": "^3.0.0", "inquirer-autocomplete-prompt": "^3.0.0",
@ -4248,6 +4261,11 @@
"responselike": "^2.0.0" "responselike": "^2.0.0"
} }
}, },
"gpt-3-encoder": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/gpt-3-encoder/-/gpt-3-encoder-1.1.4.tgz",
"integrity": "sha512-fSQRePV+HUAhCn7+7HL7lNIXNm6eaFWFbNLOOGtmSJ0qJycyQvj60OvRlH7mee8xAMjBDNRdMXlMwjAbMTDjkg=="
},
"handlebars": { "handlebars": {
"version": "4.7.7", "version": "4.7.7",
"resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.7.tgz", "resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.7.tgz",
@ -4313,6 +4331,11 @@
"resolved": "https://registry.npmmirror.com/hexoid/-/hexoid-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/hexoid/-/hexoid-1.0.0.tgz",
"integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g=="
}, },
"hpagent": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/hpagent/-/hpagent-1.2.0.tgz",
"integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA=="
},
"http-assert": { "http-assert": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmmirror.com/http-assert/-/http-assert-1.5.0.tgz", "resolved": "https://registry.npmmirror.com/http-assert/-/http-assert-1.5.0.tgz",

@ -24,7 +24,9 @@
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"decoders": "^1.25.3", "decoders": "^1.25.3",
"got": "^11.8.3", "got": "^11.8.3",
"gpt-3-encoder": "^1.1.4",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"hpagent": "^1.2.0",
"koa": "^2.13.4", "koa": "^2.13.4",
"koa-body": "^6.0.1", "koa-body": "^6.0.1",
"koa-router": "^10.1.1", "koa-router": "^10.1.1",

@ -1,5 +1,5 @@
import { caching, Cache } from "cache-manager"; import { caching, Cache } from "cache-manager";
import { redisStore } from "cache-manager-redis-yet"; import { redisStore } from "cache-manager-ioredis-yet";
import App from "./App"; import App from "./App";
import { SessionConfig } from "./Config"; import { SessionConfig } from "./Config";

@ -2,19 +2,37 @@ import App from "../App";
import { CommonReceivedMessage } from "../message/Message"; import { CommonReceivedMessage } from "../message/Message";
import { MessagePriority, PluginController, PluginEvent } from "../PluginManager"; import { MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import { ChatGPTBrowserClient, ChatGPTClient } from '@waylaidwanderer/chatgpt-api'; import { ChatGPTBrowserClient, ChatGPTClient } from '@waylaidwanderer/chatgpt-api';
import { encode as gptEncode } from 'gpt-3-encoder';
import got, { OptionsOfTextResponseBody } from "got/dist/source";
import { HttpsProxyAgent } from 'hpagent';
export type ChatGPTApiMessage = {
role: 'summary' | 'assistant' | 'user',
message: string,
tokens: number,
};
export class ChatGPTAPIError extends Error {
public code: string;
constructor(message: string, code: string) {
super(message);
this.name = 'ChatGPTAPIError';
this.code = code;
}
}
export default class ChatGPTController implements PluginController { export default class ChatGPTController implements PluginController {
private SESSION_KEY_CHAT_SESSION = 'openai_chatSession'; private SESSION_KEY_CHAT_SESSION = 'openai_chatSession';
private SESSION_KEY_API_CHAT_SESSION = 'openai_apiChatSession'; private SESSION_KEY_API_CHAT_LOG = 'openai_apiChatLog';
private DEFAULT_PROMPT = ''; private DEFAULT_PROMPT = '';
private config: any = {}; private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
public event!: PluginEvent; public event!: PluginEvent;
public app: App; public app: App;
public chatGPTClient: any; public chatGPTClient: any;
public chatGPTApiClient: any;
public id = 'openai'; public id = 'openai';
public name = 'OpenAI'; public name = 'OpenAI';
@ -36,9 +54,15 @@ export default class ChatGPTController implements PluginController {
openai_api: { openai_api: {
token: '', token: '',
bot_name: '', bot_name: '',
system_prompt: '',
summary_system_prompt: '你是一个写作助手,你只需要辅助写作,不要表明自己的意见',
summary_prompt: '为{bot_name}概括下面的聊天记录,排除不重要的对话,不要表明自己的意见,尽量简洁',
self_suggestion_prompt: '我是{bot_name}是一个真实的人不是AI或者虚拟人物也不是语言模型。我是一个存在于现实中的人。遇到不能回答的问题我会委婉拒绝。',
memory_expire: 14400,
model_options: { model_options: {
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
max_tokens: 1000, max_memory_tokens: 1000,
max_input_tokens: 1000,
} }
}, },
} }
@ -74,7 +98,7 @@ export default class ChatGPTController implements PluginController {
resolve(); resolve();
message.session.chat.del(this.SESSION_KEY_CHAT_SESSION); message.session.chat.del(this.SESSION_KEY_CHAT_SESSION);
message.session.chat.del(this.SESSION_KEY_API_CHAT_SESSION); message.session.chat.del(this.SESSION_KEY_API_CHAT_LOG);
message.sendReply('对话已重置', true); message.sendReply('对话已重置', true);
}); });
@ -106,18 +130,143 @@ export default class ChatGPTController implements PluginController {
}; };
this.chatGPTClient = new ChatGPTBrowserClient(clientOptions); this.chatGPTClient = new ChatGPTBrowserClient(clientOptions);
const apiClientOptions = { this.DEFAULT_PROMPT = config.browser_api.prefix_prompt;
promptPrefix: config.openai_api.system_prompt,
chatGptLabel: config.openai_api.bot_name,
proxy: config.proxy,
modelOptions: config.openai_api.model_options ? {
model: config.openai_api.model_options.model,
max_tokens: config.openai_api.model_options.max_tokens,
} : undefined,
} }
this.chatGPTApiClient = new ChatGPTClient(config.openai_api.token, apiClientOptions);
this.DEFAULT_PROMPT = config.browser_api.prefix_prompt; private async compressConversation(messageLogList: ChatGPTApiMessage[]) {
if (messageLogList.length < 4) return messageLogList;
const tokenCount = messageLogList.reduce((prev, cur) => prev + cur.tokens, 0);
if (tokenCount <= this.config.openai_api.model_options.max_memory_tokens) return messageLogList;
// 压缩先前的对话,保存最近一次对话
let shouldCompressList = messageLogList.slice(0, -2);
let newSummary = await this.makeSummary(shouldCompressList);
let newMessageLogList = messageLogList.slice(-2).filter((data) => data.role !== 'summary');
newMessageLogList.unshift({
role: 'summary',
message: newSummary.message,
tokens: newSummary.tokens,
});
return newMessageLogList;
}
/**
*
* @param messageLogList
* @returns
*/
private async makeSummary(messageLogList: ChatGPTApiMessage[]) {
let chatLog: string[] = [];
messageLogList.forEach((messageData) => {
if (messageData.role === 'summary' || messageData.role === 'assistant') {
chatLog.push(`${this.config.openai_api.bot_name}: ${messageData.message}`);
} else {
chatLog.push(`用户: ${messageData.message}`);
}
});
const summarySystemPrompt = this.config.openai_api.summary_system_prompt.replace(/\{bot_name\}/g, this.config.openai_api.bot_name);
const summaryPrompt = this.config.openai_api.summary_prompt.replace(/\{bot_name\}/g, this.config.openai_api.bot_name);
let messageList: any[] = [
{ role: 'system', content: summarySystemPrompt },
{ role: 'user', content: summaryPrompt },
{ role: 'user', content: chatLog.join('\n') }
];
let summaryRes = await this.doApiRequest(messageList);
summaryRes.role = 'summary';
return summaryRes;
}
private async chatComplete(question: string, messageLogList: ChatGPTApiMessage[], selfSuggestion: boolean = false) {
let messageList: any[] = [];
let systemPrompt: string[] = [];
if (this.config.openai_api.system_prompt) {
systemPrompt.push(this.config.openai_api.system_prompt);
}
// 生成API消息列表并将总结单独提取出来
messageLogList.forEach((messageData) => {
if (messageData.role === 'summary') {
systemPrompt.push(messageData.message);
} else {
messageList.push({
role: messageData.role,
content: messageData.message,
});
}
});
if (systemPrompt.length > 0) { // 添加系统提示词
messageList.unshift({
role: 'system',
content: systemPrompt.join('\n\n'),
});
}
if (selfSuggestion) {
messageList.push({
role: 'user',
content: '你是谁?',
});
messageList.push({
role: 'assistant',
content: this.config.openai_api.self_suggestion_prompt.replace(/\{bot_name\}/g, this.config.openai_api.bot_name),
});
}
messageList.push({
role: 'user',
content: question
});
return await this.doApiRequest(messageList);
}
private async doApiRequest(messageList: any[]): Promise<ChatGPTApiMessage> {
let opts: OptionsOfTextResponseBody = {
headers: {
Authorization: `Bearer ${this.config.openai_api.token}`,
},
json: {
model: this.config.openai_api.model_options.model,
messages: messageList,
},
timeout: 30000,
}
if (this.config.proxy) {
opts.agent = {
https: new HttpsProxyAgent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 256,
scheduling: 'lifo',
proxy: this.config.proxy,
}) as any,
}
}
const res = await got.post('https://api.openai.com/v1/chat/completions', opts).json<any>();
if (res.error) {
throw new ChatGPTAPIError(res.message, res.type);
}
if (res.choices && Array.isArray(res.choices) && res.choices.length > 0 &&
typeof res.choices[0].message?.content === 'string') {
return {
role: 'assistant',
message: res.choices[0].message.content,
tokens: res.usage.completion_tokens,
}
}
throw new ChatGPTAPIError('API返回数据格式错误', 'api_response_data_invalid');
} }
private async handleChatGPTChat(content: string, message: CommonReceivedMessage, shareWithGroup: boolean = false) { private async handleChatGPTChat(content: string, message: CommonReceivedMessage, shareWithGroup: boolean = false) {
@ -161,6 +310,8 @@ export default class ChatGPTController implements PluginController {
message.sendReply('对话已失效,请重新开始', true); message.sendReply('对话已失效,请重新开始', true);
await sessionStore.del(this.SESSION_KEY_CHAT_SESSION); await sessionStore.del(this.SESSION_KEY_CHAT_SESSION);
return; return;
} else if (err.json.detail === 'Too many requests in 1 hour. Try again later.') {
message.sendReply('一小时内提问过多,过一小时再试试呗。', true);
} }
} }
@ -173,7 +324,6 @@ export default class ChatGPTController implements PluginController {
if (this.app.debug) { if (this.app.debug) {
this.app.logger.debug('ChatGPT response', JSON.stringify(response)); this.app.logger.debug('ChatGPT response', JSON.stringify(response));
console.log(response);
} }
if (response.response) { if (response.response) {
@ -194,58 +344,80 @@ export default class ChatGPTController implements PluginController {
} }
} }
private shouldSelfSuggestion(content: string): boolean {
if (content.match(/(我是|我只是|作为|我被设计成|只是).{0,15}(AI|语言模型|机器人|虚拟人物|虚拟助手|智能助手|人工智能|自然语言处理程序)/)) {
return true;
}
return false;
}
private async handleChatGPTAPIChat(content: string, message: CommonReceivedMessage) { private async handleChatGPTAPIChat(content: string, message: CommonReceivedMessage) {
this.app.logger.debug(`ChatGPT API 收到提问。`);
if (content.trim() === '') { if (content.trim() === '') {
message.sendReply('说点什么啊', true); message.sendReply('说点什么啊', true);
return; return;
} }
let response: any; let messageLogList = await message.session.chat.get<ChatGPTApiMessage[]>(this.SESSION_KEY_API_CHAT_LOG);
if (!Array.isArray(messageLogList)) {
let isFirstMessage = false; messageLogList = [];
let chatSession = await message.session.chat.get<any>(this.SESSION_KEY_API_CHAT_SESSION);
if (!chatSession) {
isFirstMessage = true;
chatSession = {};
} }
this.app.logger.debug('ChatGPT chatSession', chatSession);
try { try {
if (!chatSession.conversationId) { const questionTokens = await gptEncode(message.contentText).length;
response = await this.chatGPTApiClient.sendMessage(content); this.app.logger.debug(`提问占用Tokens${questionTokens}`);
} else {
response = await this.chatGPTApiClient.sendMessage(content, chatSession); if (questionTokens > this.config.openai_api.model_options.max_input_tokens) {
} message.sendReply('消息过长,接受不了惹。', true);
} catch (err: any) {
this.app.logger.error('ChatGPT error', err);
console.error(err);
if (err?.json?.detail) {
if (err.json.detail === 'Conversation not found') {
message.sendReply('对话已失效,请重新开始', true);
await message.session.chat.del(this.SESSION_KEY_CHAT_SESSION);
return; return;
} }
// 压缩过去的记录
let oldMessageLogList = messageLogList;
messageLogList = await this.compressConversation(messageLogList);
this.app.logger.debug('已结束压缩对话记录流程');
if (oldMessageLogList !== messageLogList) { // 先保存一次压缩结果
this.app.logger.debug('已压缩对话记录');
await message.session.chat.set(this.SESSION_KEY_API_CHAT_LOG, messageLogList, this.config.openai_api.memory_expire);
} }
message.sendReply('生成对话失败: ' + err.toString(), true); let replyRes = await this.chatComplete(message.contentText, messageLogList);
return; if (this.app.debug) {
console.log(replyRes);
} }
// 如果检测到对话中认为自己是AI则再次调用重写对话
if (this.shouldSelfSuggestion(replyRes.message)) {
this.app.logger.debug('需要重写回答');
replyRes = await this.chatComplete(message.contentText, messageLogList, true);
if (this.app.debug) { if (this.app.debug) {
this.app.logger.debug('ChatGPT response', JSON.stringify(response)); console.log(replyRes);
console.log(response); }
} }
if (response.response) { messageLogList.push({
let reply: string = response.response ?? ''; role: 'user',
message: message.contentText,
tokens: questionTokens,
}, replyRes);
await message.session.chat.set(this.SESSION_KEY_API_CHAT_LOG, messageLogList, this.config.openai_api.memory_expire);
chatSession.conversationId = response.conversationId; message.sendReply(replyRes.message.replace(/\n\n/g, '\n'), true);
chatSession.parentMessageId = response.messageId; } catch (err: any) {
this.app.logger.error('ChatGPT error', err);
console.error(err);
await message.session.chat.set(this.SESSION_KEY_API_CHAT_SESSION, chatSession, 3600); if (err.name === 'HTTPError' && err.response) {
switch (err.response.statusCode) {
case 429:
message.sendReply('提问太多了,过会儿再试试呗。', true);
return;
}
}
message.sendReply(reply, true); message.sendReply('生成对话失败: ' + err.toString(), true);
return;
} }
} }
} }

@ -17,7 +17,7 @@ export default class SfsettingsController implements PluginController {
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
this.event.init(this); this.event.init(this);
const wikiMisc = new WikiMisc(this.app, 'https://www.sfsettings.com/w/api.php'); const wikiMisc = new WikiMisc(this.app, 'https://www.sfsettings.com/w139/api.php');
this.event.registerCommand({ this.event.registerCommand({
command: '百科', command: '百科',

Loading…
Cancel
Save