mirror of
https://github.com/cs-lazy-tools/ChatGPT-On-CS.git
synced 2026-08-28 10:00:27 +08:00
feat: 更新版本
This commit is contained in:
+1
-1
@@ -65,7 +65,7 @@
|
||||
- Add experimental support for vscode debugging
|
||||
- Revert https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2365 as default for users, provide as opt in option
|
||||
|
||||
# 1.1.0
|
||||
# 1.2.0-beta.1
|
||||
|
||||
- Fix #2402
|
||||
- Simplify configs (https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2406)
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
## 下载地址
|
||||
|
||||
<a href="https://github.com/cs-lazy-tools/ChatGPT-On-CS/releases/download/v1.1.0/1.1.0.exe" style="display: inline-block; background-color: #008CBA; color: white; padding: 10px 20px; text-align: center; text-decoration: none; font-weight: bold; border-radius: 5px; margin: 4px 2px; cursor: pointer;">点击下载</a>
|
||||
<a href="https://github.com/cs-lazy-tools/ChatGPT-On-CS/releases/download/v1.2.0-beta.1/1.2.0-beta.1.exe" style="display: inline-block; background-color: #008CBA; color: white; padding: 10px 20px; text-align: center; text-decoration: none; font-weight: bold; border-radius: 5px; margin: 4px 2px; cursor: pointer;">点击下载</a>
|
||||
|
||||
如果网络环境导致下载不了,可以使用百度云盘下载:
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chatgpt-on-cs",
|
||||
"description": "多平台智能客服,允许使用 ChatGPT 作为客服机器人",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0-beta.1",
|
||||
"keywords": [
|
||||
"electron",
|
||||
"boilerplate",
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chatgpt-on-cs",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0-beta.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chatgpt-on-cs",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0-beta.1",
|
||||
"description": "多平台智能客服,允许使用 ChatGPT 作为客服机器人",
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
||||
@@ -284,6 +284,8 @@ class BKServer {
|
||||
keyword: item.keyword,
|
||||
reply: item.reply,
|
||||
mode: item.mode,
|
||||
fuzzy: item.fuzzy,
|
||||
has_regular: item.has_regular,
|
||||
app_name: ptf ? ptf.name : '全局',
|
||||
};
|
||||
|
||||
@@ -300,23 +302,42 @@ class BKServer {
|
||||
});
|
||||
|
||||
this.app.post('/api/v1/reply/create', async (req, res) => {
|
||||
const { platform_id: platformId, keyword, reply, mode } = req.body;
|
||||
const {
|
||||
platform_id: platformId,
|
||||
keyword,
|
||||
reply,
|
||||
mode,
|
||||
fuzzy,
|
||||
has_regular,
|
||||
} = req.body;
|
||||
await this.keywordReplyController.create({
|
||||
mode,
|
||||
platform_id: platformId,
|
||||
keyword,
|
||||
reply,
|
||||
fuzzy,
|
||||
has_regular,
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
this.app.post('/api/v1/reply/update', async (req, res) => {
|
||||
const { id, platform_id: platformId, keyword, reply, mode } = req.body;
|
||||
const {
|
||||
id,
|
||||
platform_id: platformId,
|
||||
keyword,
|
||||
reply,
|
||||
mode,
|
||||
fuzzy,
|
||||
has_regular,
|
||||
} = req.body;
|
||||
await this.keywordReplyController.update(id, {
|
||||
mode,
|
||||
platform_id: platformId,
|
||||
keyword,
|
||||
reply,
|
||||
fuzzy,
|
||||
has_regular,
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
@@ -474,6 +495,8 @@ class BKServer {
|
||||
replace: item.replace,
|
||||
app_id: item.app_id,
|
||||
app_name: ptf ? ptf.name : '全局',
|
||||
has_regular: item.has_regular,
|
||||
fuzzy: item.fuzzy,
|
||||
};
|
||||
|
||||
results.push(result);
|
||||
|
||||
@@ -68,6 +68,8 @@ export class KeywordReplyController {
|
||||
const keyword = row.getCell(1).text.trim();
|
||||
const reply = row.getCell(2).text.trim();
|
||||
const platform = row.getCell(3).text.trim();
|
||||
const fuzzy = row.getCell(4).text.trim() === '是';
|
||||
const has_regular = row.getCell(5).text.trim() === '是';
|
||||
let platformId = '';
|
||||
|
||||
if (platform && platformMap.has(platform)) {
|
||||
@@ -78,7 +80,8 @@ export class KeywordReplyController {
|
||||
keyword: String(keyword),
|
||||
reply: String(reply),
|
||||
platform_id: String(platformId),
|
||||
mode: 'fuzzy',
|
||||
fuzzy,
|
||||
has_regular,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -122,6 +125,8 @@ export class KeywordReplyController {
|
||||
{ key: 'keyword', width: 32 },
|
||||
{ key: 'reply', width: 100 },
|
||||
{ key: 'platform', width: 32 },
|
||||
{ key: 'fuzzy', width: 10 },
|
||||
{ key: 'has_regular', width: 10 },
|
||||
];
|
||||
|
||||
// 添加复杂的标题描述并设置换行和加粗
|
||||
@@ -149,12 +154,24 @@ export class KeywordReplyController {
|
||||
};
|
||||
worksheet.getRow(1).height = 150;
|
||||
|
||||
worksheet.addRow(['匹配关键词', '回复内容', '平台']);
|
||||
worksheet.addRow([
|
||||
'匹配关键词',
|
||||
'回复内容',
|
||||
'平台',
|
||||
'模糊匹配',
|
||||
'支持正则',
|
||||
]);
|
||||
|
||||
// 添加数据行
|
||||
autoReplies.forEach((autoReply) => {
|
||||
const name = platformMap.get(autoReply.platform_id) || '';
|
||||
worksheet.addRow([autoReply.keyword, autoReply.reply, name]);
|
||||
worksheet.addRow([
|
||||
autoReply.keyword,
|
||||
autoReply.reply,
|
||||
name,
|
||||
autoReply.fuzzy ? '是' : '否',
|
||||
autoReply.has_regular ? '是' : '否',
|
||||
]);
|
||||
});
|
||||
|
||||
// 检查是否存在 excels 文件夹,不存在则创建
|
||||
@@ -186,6 +203,42 @@ export class KeywordReplyController {
|
||||
return [...globalKeywords, ...autoReplies];
|
||||
}
|
||||
|
||||
async getReplaceKeywords(platformId: string) {
|
||||
const replaceKeywords = await ReplaceKeyword.findAll({
|
||||
where: {
|
||||
app_id: platformId,
|
||||
},
|
||||
});
|
||||
|
||||
const globalKeywords = await ReplaceKeyword.findAll({
|
||||
where: {
|
||||
app_id: {
|
||||
[Op.or]: [null, ''],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return [...globalKeywords, ...replaceKeywords];
|
||||
}
|
||||
|
||||
async getTransferKeywords(platformId: string) {
|
||||
const transferKeywords = await TransferKeyword.findAll({
|
||||
where: {
|
||||
app_id: platformId,
|
||||
},
|
||||
});
|
||||
|
||||
const globalKeywords = await TransferKeyword.findAll({
|
||||
where: {
|
||||
app_id: {
|
||||
[Op.or]: [null, ''],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return [...globalKeywords, ...transferKeywords];
|
||||
}
|
||||
|
||||
async list({
|
||||
page,
|
||||
pageSize,
|
||||
@@ -409,8 +462,8 @@ export class KeywordReplyController {
|
||||
const name = platformMap.get(autoReply.app_id) || '';
|
||||
worksheet.addRow([
|
||||
autoReply.keyword,
|
||||
autoReply.fuzzy,
|
||||
autoReply.has_regular,
|
||||
autoReply.fuzzy ? '是' : '否',
|
||||
autoReply.has_regular ? '是' : '否',
|
||||
name,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,32 @@ export class Keyword extends Model {
|
||||
declare mode: string;
|
||||
|
||||
declare platform_id: string;
|
||||
|
||||
declare fuzzy: boolean;
|
||||
|
||||
declare has_regular: boolean;
|
||||
}
|
||||
|
||||
export async function checkAndAddFields(sequelize: Sequelize) {
|
||||
const tableDescription = await Keyword.describe();
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.fuzzy) {
|
||||
await sequelize.getQueryInterface().addColumn('keyword', 'fuzzy', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.has_regular) {
|
||||
await sequelize.getQueryInterface().addColumn('keyword', 'has_regular', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initKeyword(sequelize: Sequelize) {
|
||||
@@ -37,6 +63,16 @@ export function initKeyword(sequelize: Sequelize) {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
fuzzy: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: true,
|
||||
},
|
||||
has_regular: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
@@ -45,4 +81,6 @@ export function initKeyword(sequelize: Sequelize) {
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
|
||||
checkAndAddFields(sequelize);
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export function initPlugin(sequelize: Sequelize) {
|
||||
},
|
||||
version: {
|
||||
type: DataTypes.STRING(255),
|
||||
defaultValue: '1.1.0',
|
||||
defaultValue: '1.2.0-beta.1',
|
||||
allowNull: true,
|
||||
},
|
||||
source: {
|
||||
|
||||
@@ -33,7 +33,8 @@ export function initReplace(sequelize: Sequelize) {
|
||||
},
|
||||
has_regular: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
app_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
|
||||
@@ -27,7 +27,8 @@ export function initTransfer(sequelize: Sequelize) {
|
||||
},
|
||||
has_regular: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
app_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
CTX_FAN_TAG,
|
||||
CTX_NEW_CUSTOMER_TAG,
|
||||
} from '../constants';
|
||||
import { rangeMatch, specialTokenReplace } from '../../utils/strings';
|
||||
import {
|
||||
rangeMatch,
|
||||
specialTokenReplace,
|
||||
replaceKeyword,
|
||||
} from '../../utils/strings';
|
||||
import {
|
||||
ErnieAI,
|
||||
GeminiAI,
|
||||
@@ -55,66 +59,91 @@ export class MessageService {
|
||||
this.llmClientMap = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认回复
|
||||
* @param cfg
|
||||
* @param ctx
|
||||
* @param messages
|
||||
* @returns
|
||||
*/
|
||||
public async getDefaultReply(
|
||||
cfg: Config,
|
||||
ctx: Context,
|
||||
messages: MessageDTO[],
|
||||
) {
|
||||
): Promise<{ type: string; content: string }> {
|
||||
// 先检查是否存在用户的消息
|
||||
const lastUserMsg = messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((msg) => msg.role === 'OTHER');
|
||||
|
||||
const reply = {
|
||||
let reply = {
|
||||
type: 'TEXT',
|
||||
content: cfg.default_reply || '当前消息有点多,我稍后再回复你',
|
||||
};
|
||||
|
||||
if (!lastUserMsg) {
|
||||
this.log.warn(`未匹配到用户消息,所以使用默认回复: ${reply.content}`);
|
||||
return reply;
|
||||
}
|
||||
|
||||
// 再根据 context_count 去保留最后几条消息
|
||||
if (cfg.context_count > 0) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
messages = messages.slice(-cfg.context_count);
|
||||
}
|
||||
|
||||
// 等待随机时间
|
||||
await new Promise((resolve) => {
|
||||
const min = cfg.reply_speed;
|
||||
const max = cfg.reply_random_speed + cfg.reply_speed;
|
||||
const randomTime = min + Math.random() * (max - min);
|
||||
setTimeout(resolve, randomTime * 1000);
|
||||
});
|
||||
|
||||
// 再检查是否使用关键词匹配
|
||||
if (cfg.has_keyword_match) {
|
||||
const data = await this.matchKeyword(ctx, lastUserMsg);
|
||||
if (data && data.content) {
|
||||
this.log.success(`匹配关键词: ${data.content}`);
|
||||
return data;
|
||||
}
|
||||
this.log.warn(`未匹配到关键词`);
|
||||
}
|
||||
|
||||
// 最后检查是否使用 GPT 生成回复
|
||||
if (cfg.has_use_gpt) {
|
||||
this.log.info(`开始使用 GPT 生成回复`);
|
||||
|
||||
const data = await this.getLLMResponse(cfg, ctx, messages);
|
||||
|
||||
if (data && data.content) {
|
||||
this.log.success(`GPT 生成回复: ${data.content}`);
|
||||
return data;
|
||||
} else {
|
||||
// 检查是否需要转接
|
||||
const isTransfer = await this.matchTransferKeyword(ctx, lastUserMsg);
|
||||
if (isTransfer) {
|
||||
this.log.info('需要转接');
|
||||
return {
|
||||
type: 'TRANSFER',
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
|
||||
this.log.warn(`AI 回复生成失败`);
|
||||
// 再根据 context_count 去保留最后几条消息
|
||||
if (cfg.context_count > 0) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
messages = messages.slice(-cfg.context_count);
|
||||
}
|
||||
|
||||
// 等待随机时间
|
||||
await new Promise((resolve) => {
|
||||
const min = cfg.reply_speed;
|
||||
const max = cfg.reply_random_speed + cfg.reply_speed;
|
||||
const randomTime = min + Math.random() * (max - min);
|
||||
setTimeout(resolve, randomTime * 1000);
|
||||
});
|
||||
|
||||
// 再检查是否使用关键词匹配
|
||||
if (cfg.has_keyword_match) {
|
||||
const data = await this.matchKeyword(ctx, lastUserMsg);
|
||||
if (data && data.content) {
|
||||
this.log.success(`匹配关键词: ${data.content}`);
|
||||
reply = data;
|
||||
} else {
|
||||
this.log.warn(`未匹配到关键词`);
|
||||
}
|
||||
}
|
||||
|
||||
// 最后检查是否使用 GPT 生成回复
|
||||
if (
|
||||
cfg.has_use_gpt &&
|
||||
reply.content ===
|
||||
(cfg.default_reply || '当前消息有点多,我稍后再回复你')
|
||||
) {
|
||||
this.log.info(`开始使用 GPT 生成回复`);
|
||||
|
||||
const data = await this.getLLMResponse(cfg, ctx, messages);
|
||||
|
||||
if (data && data.content) {
|
||||
this.log.success(`GPT 生成回复: ${data.content}`);
|
||||
reply = data;
|
||||
} else {
|
||||
this.log.warn(`AI 回复生成失败`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.log.info('使用默认回复');
|
||||
if (reply.type === 'TEXT') {
|
||||
reply.content = await this.matchReplaceKeyword(ctx, reply.content);
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
@@ -125,6 +154,85 @@ export class MessageService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配需要替换的关键词
|
||||
* @param ctx
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
public async matchReplaceKeyword(
|
||||
ctx: Context,
|
||||
reply: string,
|
||||
): Promise<string> {
|
||||
const appId = ctx.get(CTX_APP_ID);
|
||||
if (!appId) return reply;
|
||||
|
||||
const replaceKeywords =
|
||||
await this.autoReplyController.getReplaceKeywords(appId);
|
||||
|
||||
// 先找到匹配的关键词
|
||||
const foundKeywordObj = replaceKeywords.find((keywordObj) => {
|
||||
return keywordObj.keyword.split('|').some((pattern) => {
|
||||
return rangeMatch(
|
||||
pattern,
|
||||
reply,
|
||||
keywordObj.fuzzy,
|
||||
keywordObj.has_regular,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// 如果找到匹配的关键词对象,进行替换
|
||||
if (foundKeywordObj) {
|
||||
foundKeywordObj.keyword.split('|').forEach((pattern) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
reply = replaceKeyword(
|
||||
pattern,
|
||||
reply,
|
||||
foundKeywordObj.replace,
|
||||
foundKeywordObj.fuzzy,
|
||||
foundKeywordObj.has_regular,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配关键词
|
||||
* @param ctx
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
public async matchTransferKeyword(
|
||||
ctx: Context,
|
||||
message: MessageDTO,
|
||||
): Promise<boolean> {
|
||||
const appId = ctx.get(CTX_APP_ID);
|
||||
if (!appId) return false;
|
||||
|
||||
const keywords = await this.autoReplyController.getTransferKeywords(appId);
|
||||
|
||||
// 先找到匹配的关键词
|
||||
const foundKeywordObj = keywords.find((keywordObj) => {
|
||||
return keywordObj.keyword.split('|').some((pattern) => {
|
||||
return rangeMatch(
|
||||
pattern,
|
||||
message.content,
|
||||
keywordObj.fuzzy,
|
||||
keywordObj.has_regular,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (foundKeywordObj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配关键词
|
||||
* @param ctx
|
||||
@@ -143,7 +251,12 @@ export class MessageService {
|
||||
// 先找到匹配的关键词
|
||||
const foundKeywordObj = keywords.find((keywordObj) => {
|
||||
return keywordObj.keyword.split('|').some((pattern) => {
|
||||
return rangeMatch(pattern, message.content);
|
||||
return rangeMatch(
|
||||
pattern,
|
||||
message.content,
|
||||
keywordObj.fuzzy,
|
||||
keywordObj.has_regular,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+165
-26
@@ -2,56 +2,195 @@
|
||||
* 范围匹配
|
||||
* @param ptt 范围查询关键词 e.g. 'hello [and] world'
|
||||
* @param msg 消息
|
||||
* @param fuzzy 是否模糊匹配
|
||||
* @param has_regular 是否使用正则表达式
|
||||
* @returns
|
||||
*/
|
||||
export function rangeMatch(ptt: string, msg: string): boolean {
|
||||
export function rangeMatch(
|
||||
ptt: string,
|
||||
msg: string,
|
||||
fuzzy: boolean,
|
||||
has_regular: boolean,
|
||||
): boolean {
|
||||
if (ptt.includes('[and]')) {
|
||||
const keywords = ptt.split('[and]');
|
||||
return keywords.every((keyword) => matchKeyword(keyword.trim(), msg));
|
||||
return keywords.every((keyword, index) => {
|
||||
if (index === 0) {
|
||||
return matchKeyword(keyword.trim(), msg, fuzzy, has_regular);
|
||||
}
|
||||
const prevKeyword = keywords[index - 1].trim();
|
||||
const prevIndex = msg.indexOf(prevKeyword);
|
||||
if (prevIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
return matchKeyword(
|
||||
keyword.trim(),
|
||||
msg.slice(prevIndex + prevKeyword.length),
|
||||
fuzzy,
|
||||
has_regular,
|
||||
);
|
||||
});
|
||||
}
|
||||
return matchKeyword(ptt, msg);
|
||||
return matchKeyword(ptt, msg, fuzzy, has_regular);
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配关键词
|
||||
* @param ptt 匹配模式
|
||||
* @param msg 消息
|
||||
* @param fuzzy 是否模糊匹配
|
||||
* @param has_regular 是否使用正则表达式
|
||||
* @returns
|
||||
*/
|
||||
export function matchKeyword(ptt: string, msg: string): boolean {
|
||||
let pattern = ptt.trim();
|
||||
export function matchKeyword(
|
||||
ptt: string,
|
||||
msg: string,
|
||||
fuzzy: boolean,
|
||||
has_regular: boolean,
|
||||
): boolean {
|
||||
try {
|
||||
const pattern = ptt.trim();
|
||||
if (has_regular) {
|
||||
const regex = new RegExp(pattern);
|
||||
return regex.test(msg);
|
||||
}
|
||||
|
||||
// 如果模式只是一个星号,它应该匹配任何消息。
|
||||
if (pattern === '*') {
|
||||
return true;
|
||||
if (fuzzy) {
|
||||
return msg.includes(pattern);
|
||||
}
|
||||
|
||||
return msg.trim() === pattern;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 范围匹配
|
||||
* @param ptt 范围查询关键词 e.g. 'hello [and] world'
|
||||
* @param msg 消息
|
||||
* @param fuzzy 是否模糊匹配
|
||||
* @param has_regular 是否使用正则表达式
|
||||
* @returns 匹配到的开始和结束位置数组
|
||||
*/
|
||||
export function rangeMatchPosition(
|
||||
ptt: string,
|
||||
msg: string,
|
||||
fuzzy: boolean,
|
||||
has_regular: boolean,
|
||||
): Array<[number, number]> {
|
||||
const matchPositions: Array<[number, number]> = [];
|
||||
|
||||
if (ptt.includes('[and]')) {
|
||||
const keywords = ptt.split('[and]');
|
||||
let searchStart = 0;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const keyword of keywords) {
|
||||
const trimmedKeyword = keyword.trim();
|
||||
const matchPosition = matchKeywordPosition(
|
||||
trimmedKeyword,
|
||||
msg,
|
||||
fuzzy,
|
||||
has_regular,
|
||||
searchStart,
|
||||
);
|
||||
if (matchPosition) {
|
||||
matchPositions.push(matchPosition);
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
searchStart = matchPosition[1];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const matchPosition = matchKeywordPosition(ptt, msg, fuzzy, has_regular, 0);
|
||||
if (matchPosition) {
|
||||
matchPositions.push(matchPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// 合并连续的 '*' 字符为一个 '*'
|
||||
pattern = pattern.replace(/\*+/g, '*');
|
||||
return matchPositions;
|
||||
}
|
||||
|
||||
// 如果模式不包含 '*',则检查是否包含关键词
|
||||
if (!pattern.includes('*')) {
|
||||
return msg.includes(pattern);
|
||||
/**
|
||||
* 匹配关键词并返回位置
|
||||
* @param ptt 匹配模式
|
||||
* @param msg 消息
|
||||
* @param fuzzy 是否模糊匹配
|
||||
* @param has_regular 是否使用正则表达式
|
||||
* @param searchStart 搜索的起始位置
|
||||
* @returns 匹配到的开始和结束位置
|
||||
*/
|
||||
function matchKeywordPosition(
|
||||
ptt: string,
|
||||
msg: string,
|
||||
fuzzy: boolean,
|
||||
has_regular: boolean,
|
||||
searchStart: number,
|
||||
): [number, number] | null {
|
||||
const pattern = ptt.trim();
|
||||
|
||||
if (has_regular) {
|
||||
const regex = new RegExp(pattern);
|
||||
const match = regex.exec(msg.slice(searchStart));
|
||||
if (match) {
|
||||
return [
|
||||
searchStart + match.index,
|
||||
searchStart + match.index + match[0].length,
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = pattern.split('*');
|
||||
if (fuzzy) {
|
||||
const index = msg.indexOf(pattern, searchStart);
|
||||
if (index !== -1) {
|
||||
return [index, index + pattern.length];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = msg.indexOf(pattern, searchStart);
|
||||
if (index !== -1 && index + pattern.length === msg.length) {
|
||||
return [index, index + pattern.length];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换匹配的关键词
|
||||
* @param ptt 匹配模式
|
||||
* @param msg 消息
|
||||
* @param replace 替换字符串
|
||||
* @param fuzzy 是否模糊匹配
|
||||
* @param has_regular 是否使用正则表达式
|
||||
* @returns
|
||||
*/
|
||||
export function replaceKeyword(
|
||||
ptt: string,
|
||||
msg: string,
|
||||
replace: string,
|
||||
fuzzy: boolean,
|
||||
has_regular: boolean,
|
||||
): string {
|
||||
const matchPositions = rangeMatchPosition(ptt, msg, fuzzy, has_regular);
|
||||
if (matchPositions.length === 0) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const part of parts) {
|
||||
// 跳过空字符串(它们来自模式开始、结束或连续 '*')
|
||||
if (part === '') continue;
|
||||
|
||||
const index = msg.indexOf(part, lastIndex);
|
||||
// 如果找不到部分或部分不是按顺序出现,则匹配失败
|
||||
if (index === -1 || index < lastIndex) {
|
||||
return false;
|
||||
}
|
||||
lastIndex = index + part.length;
|
||||
for (const [start, end] of matchPositions) {
|
||||
result += msg.slice(lastIndex, start) + replace;
|
||||
lastIndex = end;
|
||||
}
|
||||
|
||||
// 确保消息的剩余部分可以被模式尾部的 '*' 匹配
|
||||
return parts[parts.length - 1] === '' || lastIndex <= msg.length;
|
||||
result += msg.slice(lastIndex);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -130,6 +130,8 @@ export interface Keyword {
|
||||
platform_id?: string;
|
||||
keyword: string;
|
||||
reply: string;
|
||||
fuzzy?: boolean;
|
||||
has_regular?: boolean;
|
||||
}
|
||||
|
||||
export interface TransferKeyword {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { WECHAT_NEWS_PLUGIN } from './wechat';
|
||||
import { QIANNIU_GOODS_PLUGIN } from './qianniu';
|
||||
import { NORMAL_PLUGIN } from './normal';
|
||||
|
||||
// 转换为 JSON 格式
|
||||
// https://www.lambdatest.com/free-online-tools/json-escape
|
||||
@@ -13,21 +12,12 @@ export const SystemPluginList = [
|
||||
},
|
||||
{
|
||||
type: 'plugin',
|
||||
title: '微信热榜播报插件',
|
||||
author: '系统插件',
|
||||
description: '当用户使用 @BOT 并且携带 [热榜] 关键字时,将会触发此插件。',
|
||||
tags: ['微信', '热榜', '机器人'],
|
||||
code: WECHAT_NEWS_PLUGIN,
|
||||
icon: '📰',
|
||||
},
|
||||
{
|
||||
type: 'plugin',
|
||||
title: '千牛商品查询插件',
|
||||
title: '基础对话插件',
|
||||
author: '系统插件',
|
||||
description:
|
||||
'会携带商品名称,商品 ID 等信息去询问 GPT 知识库(需要自己配置知识库)',
|
||||
tags: ['千牛', '商品', '机器人'],
|
||||
code: QIANNIU_GOODS_PLUGIN,
|
||||
icon: '🎁',
|
||||
'默认的回复流程,会根据设置去选择使用关键词回复或者使用 GPT 回复。',
|
||||
tags: ['系统'],
|
||||
code: NORMAL_PLUGIN,
|
||||
icon: '⚙️',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const NORMAL_PLUGIN = `const cc = require('config_srv');
|
||||
const rp = require('reply_srv');
|
||||
|
||||
/**
|
||||
* 插件主函数
|
||||
* @param {AppContext} ctx - 上下文信息
|
||||
* @param {Message[]} messages - 消息数组
|
||||
* @returns {Reply} 插件执行结果
|
||||
*/
|
||||
async function main(ctx, messages) {
|
||||
const cfg = await cc.get(ctx);
|
||||
return await rp.getDefaultReply(cfg, ctx, messages);
|
||||
}`;
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
useToast,
|
||||
Flex,
|
||||
Box,
|
||||
Switch,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
} from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
@@ -54,6 +59,8 @@ const EditKeyword = ({
|
||||
const [currentPlatform, setCurrentPlatform] = useState<App | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [fuzzy, setFuzzy] = useState<boolean>(true);
|
||||
const [regular, setRegular] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editKeyword?.keyword) {
|
||||
@@ -204,6 +211,34 @@ const EditKeyword = ({
|
||||
isLoading={isPlatformsLoading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex direction="column">
|
||||
<Box m={2}>
|
||||
<FormControl display="flex" alignItems="center">
|
||||
<FormLabel htmlFor="fuzzy" mb="0">
|
||||
模糊匹配
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="fuzzy"
|
||||
isChecked={fuzzy}
|
||||
onChange={() => setFuzzy(!fuzzy)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box m={2}>
|
||||
<FormControl display="flex" alignItems="center">
|
||||
<FormLabel htmlFor="regular" mb="0">
|
||||
正则匹配
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="regular"
|
||||
isChecked={regular}
|
||||
onChange={() => setRegular(!regular)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<KeywordInput
|
||||
newKeyword={newKeyword}
|
||||
setNewKeyword={setNewKeyword}
|
||||
|
||||
@@ -177,8 +177,8 @@ const ReplaceKeyword = () => {
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
||||
<Alert status="info" mr={'20px'}>
|
||||
当 ChatGPT
|
||||
生成的回复中包含下面的关键词规则时,将该关键词自动替换为指定的关键词。
|
||||
例如:关键词是“你好”,替换内容是 “您好”,当回复为 “你好,我是 ChatGPT”
|
||||
时,将替换为“您好,我是 ChatGPT”。
|
||||
</Alert>
|
||||
<Flex alignItems="center">
|
||||
<HStack>
|
||||
|
||||
@@ -229,6 +229,8 @@ const ReplyKeyword = () => {
|
||||
<Th>平台</Th>
|
||||
<Th>关键词</Th>
|
||||
<Th>回复内容</Th>
|
||||
<Th>模糊匹配</Th>
|
||||
<Th>正则</Th>
|
||||
<Th>操作</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -256,6 +258,8 @@ const ReplyKeyword = () => {
|
||||
>
|
||||
{keyword.reply}
|
||||
</Td>
|
||||
<Td>{keyword.fuzzy ? '是' : '否'}</Td>
|
||||
<Td>{keyword.has_regular ? '是' : '否'}</Td>
|
||||
<Td>
|
||||
<Grid templateColumns="repeat(2, 1fr)" gap={2}>
|
||||
<Tooltip label="删除">
|
||||
|
||||
@@ -232,8 +232,8 @@ const TransferKeyword = () => {
|
||||
<Tr>
|
||||
<Th>平台</Th>
|
||||
<Th>关键词</Th>
|
||||
<Th>是否模糊匹配</Th>
|
||||
<Th>是否含正则表达式</Th>
|
||||
<Th>模糊匹配</Th>
|
||||
<Th>正则</Th>
|
||||
<Th>操作</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
|
||||
@@ -31,7 +31,6 @@ const InstanceListComponent = () => {
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('selectedAppId', selectedAppId, filteredInstances);
|
||||
setCurrentAppId(selectedAppId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedAppId]);
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { loader } from '@monaco-editor/react';
|
||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
import { HashRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import GeneralSettings from './components/Settings/GeneralSettings';
|
||||
import LLMSettings from './components/Settings/LLMSettings';
|
||||
@@ -64,6 +64,9 @@ const App = () => {
|
||||
trackPageView('Settings');
|
||||
}, []);
|
||||
|
||||
// 打印当前的 url
|
||||
// console.log('current url:', window.location.href);
|
||||
|
||||
const fetchConfigActive = useCallback(
|
||||
async (appId: string, instanceId?: string) => {
|
||||
try {
|
||||
@@ -236,7 +239,7 @@ const App = () => {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/settings.html"
|
||||
path="/"
|
||||
element={
|
||||
<PluginPage
|
||||
appId={settings.appId}
|
||||
@@ -244,10 +247,7 @@ const App = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings.html/editor"
|
||||
element={<PluginEditPage />}
|
||||
/>
|
||||
<Route path="/editor" element={<PluginEditPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</TabPanel>
|
||||
|
||||
@@ -198,12 +198,12 @@ const PluginPage = ({ appId, instanceId }: PluginPageProps) => {
|
||||
const handleEdit = (plugin: Plugin) => {
|
||||
if (plugin.type === 'plugin') {
|
||||
setCurrentPlugin(plugin);
|
||||
navigate('/settings.html/editor');
|
||||
navigate('/editor');
|
||||
}
|
||||
|
||||
if (plugin.type === 'custom') {
|
||||
setCurrentPlugin(null);
|
||||
navigate('/settings.html/editor');
|
||||
navigate('/editor');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ const PluginEdit = () => {
|
||||
<Button
|
||||
leftIcon={<FiChevronLeft />}
|
||||
colorScheme="teal"
|
||||
onClick={() => navigate('/settings.html')}
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
@@ -258,7 +258,7 @@ const PluginEdit = () => {
|
||||
onClick={async () => {
|
||||
await handleDeletePlugin();
|
||||
onClose();
|
||||
navigate('/settings.html');
|
||||
navigate('/');
|
||||
}}
|
||||
ml={3}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user