mirror of
https://github.com/cs-lazy-tools/ChatGPT-On-CS.git
synced 2026-08-28 10:00:27 +08:00
fix: 配置读取错误
This commit is contained in:
Vendored
+2
-1
@@ -28,6 +28,7 @@
|
||||
"*.{css,sass,scss}.d.ts": true
|
||||
},
|
||||
"cSpell.words": [
|
||||
"dify"
|
||||
"dify",
|
||||
"jinritemai"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,55 +22,70 @@ export class ConfigController {
|
||||
|
||||
let config;
|
||||
|
||||
// 先查找实例配置
|
||||
if (instanceId) {
|
||||
config = await Config.findOne({
|
||||
where: { platform_id: appId, instance_id: instanceId },
|
||||
});
|
||||
|
||||
// 如果实例配置存在且激活,直接返回
|
||||
if (config && config.active) {
|
||||
return this.mergeWithGlobalConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
// 查找应用级别配置
|
||||
if (appId) {
|
||||
config = await Config.findOne({
|
||||
where: { platform_id: appId, instance_id: '' },
|
||||
});
|
||||
|
||||
// 如果应用级别配置存在且激活,直接返回
|
||||
if (config && config.active) {
|
||||
return this.mergeWithGlobalConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
// 查找全局配置
|
||||
config = await Config.findOne({
|
||||
where: { instance_id: instanceId, active: true },
|
||||
where: { global: true },
|
||||
});
|
||||
|
||||
if (!config && appId) {
|
||||
config = await Config.findOne({
|
||||
where: { platform_id: appId, active: true },
|
||||
});
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
config = await Config.findOne({
|
||||
where: { global: true },
|
||||
});
|
||||
}
|
||||
|
||||
// 如果全局配置不存在,创建一个默认的全局配置
|
||||
if (!config) {
|
||||
config = await Config.create({
|
||||
global: true,
|
||||
});
|
||||
} else {
|
||||
const globalConfig = await Config.findOne({
|
||||
where: { global: true },
|
||||
});
|
||||
|
||||
// 这三个配置项是全局配置,需要合并到实例配置中
|
||||
if (globalConfig) {
|
||||
config.has_keyword_match = globalConfig.has_keyword_match;
|
||||
config.has_paused = globalConfig.has_paused;
|
||||
config.has_use_gpt = globalConfig.has_use_gpt;
|
||||
config.has_mouse_close = globalConfig.has_mouse_close;
|
||||
config.has_esc_close = globalConfig.has_esc_close;
|
||||
}
|
||||
|
||||
// 再检查 key 和 base_url 是否存在,不存在则使用全局配置
|
||||
if (!config.key || !config.base_url) {
|
||||
config.llm_type = globalConfig?.llm_type || 'chatgpt';
|
||||
config.model = globalConfig?.model || 'gpt-3.5-turbo';
|
||||
}
|
||||
|
||||
if (!config.key) {
|
||||
config.key = globalConfig?.key || '';
|
||||
}
|
||||
|
||||
if (!config.base_url) {
|
||||
config.base_url = globalConfig?.base_url || '';
|
||||
}
|
||||
}
|
||||
|
||||
return this.mergeWithGlobalConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并全局配置到指定配置
|
||||
* @param config 指定的配置
|
||||
* @returns 合并后的配置
|
||||
*/
|
||||
private async mergeWithGlobalConfig(config: Config): Promise<Config> {
|
||||
const globalConfig = await Config.findOne({
|
||||
where: { global: true },
|
||||
});
|
||||
|
||||
if (globalConfig) {
|
||||
// 合并特定的全局配置项到实例配置
|
||||
config.has_keyword_match = globalConfig.has_keyword_match;
|
||||
config.has_paused = globalConfig.has_paused;
|
||||
config.has_use_gpt = globalConfig.has_use_gpt;
|
||||
config.has_mouse_close = globalConfig.has_mouse_close;
|
||||
config.has_esc_close = globalConfig.has_esc_close;
|
||||
}
|
||||
|
||||
// 检查 key 和 base_url,如果不存在则使用全局配置
|
||||
config.llm_type = config.llm_type || globalConfig?.llm_type || 'chatgpt';
|
||||
config.model = config.model || globalConfig?.model || 'gpt-3.5-turbo';
|
||||
config.key = config.key || globalConfig?.key || '';
|
||||
config.base_url = config.base_url || globalConfig?.base_url || '';
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -93,6 +108,7 @@ export class ConfigController {
|
||||
config = await Config.findOne({
|
||||
where: { instance_id: instanceId },
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
config = await Config.create({
|
||||
platform_id: appId,
|
||||
@@ -105,6 +121,7 @@ export class ConfigController {
|
||||
config = await Config.findOne({
|
||||
where: { platform_id: appId },
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
config = await Config.create({
|
||||
platform_id: appId,
|
||||
@@ -236,27 +253,7 @@ export class ConfigController {
|
||||
appId: string | undefined;
|
||||
instanceId: string | undefined;
|
||||
}): Promise<boolean> {
|
||||
let config = null;
|
||||
if (instanceId) {
|
||||
config = await Config.findOne({
|
||||
where: { instance_id: instanceId },
|
||||
});
|
||||
|
||||
return config?.active || false;
|
||||
}
|
||||
|
||||
if (appId) {
|
||||
config = await Config.findOne({
|
||||
where: { platform_id: appId },
|
||||
});
|
||||
|
||||
return config?.active || false;
|
||||
}
|
||||
|
||||
config = await Config.findOne({
|
||||
where: { global: true },
|
||||
});
|
||||
|
||||
const config = await this.findConfig(appId, instanceId);
|
||||
return config?.active || false;
|
||||
}
|
||||
|
||||
@@ -283,37 +280,7 @@ export class ConfigController {
|
||||
| DriverConfig
|
||||
| undefined
|
||||
> {
|
||||
let config = null;
|
||||
if (instanceId) {
|
||||
config = await Config.findOne({
|
||||
where: { instance_id: instanceId },
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
config = await Config.create({
|
||||
platform_id: appId,
|
||||
instance_id: instanceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!config && appId) {
|
||||
config = await Config.findOne({
|
||||
where: { platform_id: appId },
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
config = await Config.create({
|
||||
platform_id: appId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
config = await Config.findOne({
|
||||
where: { global: true },
|
||||
});
|
||||
}
|
||||
const config = await this.findConfig(appId, instanceId);
|
||||
|
||||
if (type === 'generic') {
|
||||
return {
|
||||
@@ -329,6 +296,8 @@ export class ConfigController {
|
||||
defaultReply: config?.default_reply || '',
|
||||
truncateWordCount: config?.truncate_word_count || 0,
|
||||
truncateWordKey: config?.truncate_word_key || '',
|
||||
jinritemaiDefaultReplyMatch:
|
||||
config?.jinritemai_default_reply_match || '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -359,6 +328,8 @@ export class ConfigController {
|
||||
hasUseGpt: config?.has_use_gpt || false,
|
||||
hasMouseClose: config?.has_mouse_close || false,
|
||||
hasEscClose: config?.has_esc_close || false,
|
||||
hasTransfer: config?.has_transfer || false,
|
||||
hasReplace: config?.has_replace || false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -387,38 +358,9 @@ export class ConfigController {
|
||||
| PluginConfig
|
||||
| DriverConfig;
|
||||
}) {
|
||||
let dbConfig = null;
|
||||
if (instanceId) {
|
||||
dbConfig = await Config.findOne({
|
||||
where: { instance_id: instanceId },
|
||||
});
|
||||
|
||||
if (!dbConfig) {
|
||||
dbConfig = await Config.create({
|
||||
platform_id: appId,
|
||||
instance_id: instanceId,
|
||||
});
|
||||
}
|
||||
} else if (appId) {
|
||||
dbConfig = await Config.findOne({
|
||||
where: { platform_id: appId },
|
||||
});
|
||||
|
||||
if (!dbConfig) {
|
||||
dbConfig = await Config.create({
|
||||
platform_id: appId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dbConfig = await Config.findOne({
|
||||
where: { global: true },
|
||||
});
|
||||
|
||||
if (!dbConfig) {
|
||||
dbConfig = await Config.create({
|
||||
global: true,
|
||||
});
|
||||
}
|
||||
let dbConfig = await this.findConfig(appId, instanceId);
|
||||
if (!dbConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'generic') {
|
||||
@@ -434,6 +376,7 @@ export class ConfigController {
|
||||
default_reply: config.defaultReply,
|
||||
truncate_word_count: config.truncateWordCount,
|
||||
truncate_word_key: config.truncateWordKey,
|
||||
jinritemai_default_reply_match: config.jinritemaiDefaultReplyMatch,
|
||||
});
|
||||
} else if (type === 'llm') {
|
||||
const config = cfg as LLMConfig;
|
||||
@@ -469,6 +412,8 @@ export class ConfigController {
|
||||
has_use_gpt: config.hasUseGpt,
|
||||
has_mouse_close: config.hasMouseClose,
|
||||
has_esc_close: config.hasEscClose,
|
||||
has_transfer: config.hasTransfer,
|
||||
has_replace: config.hasReplace,
|
||||
});
|
||||
} else {
|
||||
const config = cfg as AccountConfig;
|
||||
@@ -527,4 +472,44 @@ export class ConfigController {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找配置
|
||||
* @param appId
|
||||
* @param instanceId
|
||||
* @returns
|
||||
*/
|
||||
private async findConfig(
|
||||
appId: string | undefined,
|
||||
instanceId: string | undefined,
|
||||
): Promise<Config | null> {
|
||||
let config = null;
|
||||
if (instanceId) {
|
||||
config = await Config.findOne({
|
||||
where: { instance_id: instanceId },
|
||||
});
|
||||
|
||||
if (config && config.active) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config && appId) {
|
||||
config = await Config.findOne({
|
||||
where: { platform_id: appId },
|
||||
});
|
||||
|
||||
if (config && config.active) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
config = await Config.findOne({
|
||||
where: { global: true },
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export class KeywordReplyController {
|
||||
autoReplies.push({
|
||||
keyword: String(keyword),
|
||||
reply: String(reply),
|
||||
mode: 'fuzzy', // 废弃字段,这里只是兼容旧数据
|
||||
platform_id: String(platformId),
|
||||
fuzzy,
|
||||
has_regular,
|
||||
|
||||
@@ -59,6 +59,12 @@ export class Config extends Model {
|
||||
declare truncate_word_count: number; // 截断词数
|
||||
|
||||
declare truncate_word_key: string; // 截断词
|
||||
|
||||
declare has_transfer: boolean; // 是否开启关键词转接给其它客服
|
||||
|
||||
declare has_replace: boolean; // 是否开启关键词替换
|
||||
|
||||
declare jinritemai_default_reply_match: string; // 抖店默认回复
|
||||
}
|
||||
|
||||
export async function checkAndAddFields(sequelize: Sequelize) {
|
||||
@@ -68,7 +74,7 @@ export async function checkAndAddFields(sequelize: Sequelize) {
|
||||
if (!tableDescription.has_esc_close) {
|
||||
await sequelize.getQueryInterface().addColumn('n_config', 'has_esc_close', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
defaultValue: true,
|
||||
});
|
||||
}
|
||||
@@ -79,8 +85,8 @@ export async function checkAndAddFields(sequelize: Sequelize) {
|
||||
.getQueryInterface()
|
||||
.addColumn('n_config', 'truncate_word_count', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 4000,
|
||||
allowNull: true,
|
||||
defaultValue: 210,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,10 +96,39 @@ export async function checkAndAddFields(sequelize: Sequelize) {
|
||||
.getQueryInterface()
|
||||
.addColumn('n_config', 'truncate_word_key', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
defaultValue: '',
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.has_transfer) {
|
||||
await sequelize.getQueryInterface().addColumn('n_config', 'has_transfer', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.has_replace) {
|
||||
await sequelize.getQueryInterface().addColumn('n_config', 'has_replace', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.jinritemai_default_reply_match) {
|
||||
await sequelize
|
||||
.getQueryInterface()
|
||||
.addColumn('n_config', 'jinritemai_default_reply_match', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '很高兴为您服务,请问有什么可以帮您?',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initConfig(sequelize: Sequelize) {
|
||||
@@ -225,13 +260,28 @@ export function initConfig(sequelize: Sequelize) {
|
||||
truncate_word_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 4000,
|
||||
defaultValue: 210,
|
||||
},
|
||||
truncate_word_key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '',
|
||||
},
|
||||
has_transfer: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
allowNull: true,
|
||||
},
|
||||
has_replace: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
allowNull: true,
|
||||
},
|
||||
jinritemai_default_reply_match: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '很高兴为您服务,请问有什么可以帮您?',
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
|
||||
@@ -125,7 +125,7 @@ export class DispatchService {
|
||||
|
||||
public async syncConfig(): Promise<boolean> {
|
||||
try {
|
||||
const cfg = await this.configController.getConfigByType({
|
||||
let cfg = await this.configController.getConfigByType({
|
||||
appId: undefined,
|
||||
instanceId: undefined,
|
||||
type: 'driver',
|
||||
@@ -140,10 +140,38 @@ export class DispatchService {
|
||||
hasPaused = cfg.hasPaused || false;
|
||||
}
|
||||
|
||||
cfg = await this.configController.getConfigByType({
|
||||
appId: undefined,
|
||||
instanceId: undefined,
|
||||
type: 'generic',
|
||||
});
|
||||
|
||||
if (!cfg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let jdr = '很高兴为您服务,请问有什么可以帮您?';
|
||||
if ('jinritemaiDefaultReplyMatch' in cfg) {
|
||||
jdr = cfg.jinritemaiDefaultReplyMatch || '';
|
||||
}
|
||||
|
||||
let twkey = '';
|
||||
if ('truncateWordKey' in cfg) {
|
||||
twkey = cfg.truncateWordKey || '';
|
||||
}
|
||||
|
||||
let twcount = 210;
|
||||
if ('truncateWordCount' in cfg) {
|
||||
twcount = cfg.truncateWordCount || 210;
|
||||
}
|
||||
|
||||
await emitAndWait(this.io, 'strategyService-updateStatus', {
|
||||
status: hasPaused
|
||||
? StrategyServiceStatusEnum.STOPPED
|
||||
: StrategyServiceStatusEnum.RUNNING,
|
||||
jdr,
|
||||
twkey,
|
||||
twcount,
|
||||
});
|
||||
|
||||
const instances = await Instance.findAll();
|
||||
@@ -182,17 +210,6 @@ export class DispatchService {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateStatus(status: StrategyServiceStatusEnum): Promise<any> {
|
||||
try {
|
||||
return await emitAndWait(this.io, 'strategyService-updateStatus', {
|
||||
status,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update status', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllPlatforms(): Promise<Platform[]> {
|
||||
const maxRetries = 10;
|
||||
let attempt = 0;
|
||||
|
||||
@@ -77,6 +77,8 @@ export class MessageService {
|
||||
.reverse()
|
||||
.find((msg) => msg.role === 'OTHER');
|
||||
|
||||
let hasDefaultReply = true;
|
||||
|
||||
let reply = {
|
||||
type: 'TEXT',
|
||||
content: cfg.default_reply || '当前消息有点多,我稍后再回复你',
|
||||
@@ -85,14 +87,17 @@ export class MessageService {
|
||||
if (!lastUserMsg) {
|
||||
this.log.warn(`未匹配到用户消息,所以使用默认回复: ${reply.content}`);
|
||||
} else {
|
||||
// 检查是否需要转接
|
||||
const isTransfer = await this.matchTransferKeyword(ctx, lastUserMsg);
|
||||
if (isTransfer) {
|
||||
this.log.info('需要转接');
|
||||
return {
|
||||
type: 'TRANSFER',
|
||||
content: '',
|
||||
};
|
||||
if (cfg.has_transfer) {
|
||||
// 检查是否需要转接
|
||||
const isTransfer = await this.matchTransferKeyword(ctx, lastUserMsg);
|
||||
if (isTransfer) {
|
||||
this.log.info('需要转接');
|
||||
hasDefaultReply = false;
|
||||
return {
|
||||
type: 'TRANSFER',
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 再根据 context_count 去保留最后几条消息
|
||||
@@ -115,17 +120,14 @@ export class MessageService {
|
||||
if (data && data.content) {
|
||||
this.log.success(`匹配关键词: ${data.content}`);
|
||||
reply = data;
|
||||
hasDefaultReply = false;
|
||||
} else {
|
||||
this.log.warn(`未匹配到关键词`);
|
||||
}
|
||||
}
|
||||
|
||||
// 最后检查是否使用 GPT 生成回复
|
||||
if (
|
||||
cfg.has_use_gpt &&
|
||||
reply.content ===
|
||||
(cfg.default_reply || '当前消息有点多,我稍后再回复你')
|
||||
) {
|
||||
if (cfg.has_use_gpt && hasDefaultReply) {
|
||||
this.log.info(`开始使用 GPT 生成回复`);
|
||||
|
||||
const data = await this.getLLMResponse(cfg, ctx, messages);
|
||||
@@ -133,15 +135,25 @@ export class MessageService {
|
||||
if (data && data.content) {
|
||||
this.log.success(`GPT 生成回复: ${data.content}`);
|
||||
reply = data;
|
||||
hasDefaultReply = false;
|
||||
} else {
|
||||
this.log.warn(`AI 回复生成失败`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.log.info('使用默认回复');
|
||||
if (reply.type === 'TEXT') {
|
||||
reply.content = await this.matchReplaceKeyword(ctx, reply.content);
|
||||
if (hasDefaultReply) {
|
||||
const replyContent = await this.choseRandomReply(reply.content);
|
||||
reply = {
|
||||
type: reply.type as MessageType,
|
||||
content: replyContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (cfg.has_replace) {
|
||||
if (reply.type === 'TEXT') {
|
||||
reply.content = await this.matchReplaceKeyword(ctx, reply.content);
|
||||
}
|
||||
}
|
||||
|
||||
return reply;
|
||||
@@ -261,10 +273,7 @@ export class MessageService {
|
||||
});
|
||||
|
||||
if (foundKeywordObj) {
|
||||
const replies = foundKeywordObj.reply.split('[or]');
|
||||
const chosenReply = specialTokenReplace(
|
||||
replies[Math.floor(Math.random() * replies.length)],
|
||||
);
|
||||
const chosenReply = await this.choseRandomReply(foundKeywordObj.reply);
|
||||
|
||||
let msgType = 'TEXT';
|
||||
if (chosenReply.includes('[@]') && chosenReply.includes('[/@]')) {
|
||||
@@ -287,6 +296,15 @@ export class MessageService {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async choseRandomReply(reply: string) {
|
||||
const replies = reply.split('[or]');
|
||||
const chosenReply = specialTokenReplace(
|
||||
replies[Math.floor(Math.random() * replies.length)],
|
||||
);
|
||||
|
||||
return chosenReply;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 LLM 是否可用
|
||||
*/
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface GenericConfig {
|
||||
defaultReply: string;
|
||||
truncateWordCount: number;
|
||||
truncateWordKey: string;
|
||||
jinritemaiDefaultReplyMatch: string;
|
||||
}
|
||||
|
||||
export interface LLMConfig {
|
||||
@@ -79,4 +80,6 @@ export interface DriverConfig {
|
||||
hasUseGpt: boolean;
|
||||
hasMouseClose: boolean;
|
||||
hasEscClose: boolean;
|
||||
hasTransfer: boolean;
|
||||
hasReplace: boolean;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ export interface GenericConfig {
|
||||
defaultReply: string;
|
||||
truncateWordCount: number;
|
||||
truncateWordKey: string;
|
||||
jinritemaiDefaultReplyMatch: string;
|
||||
}
|
||||
|
||||
export interface LLMConfig {
|
||||
@@ -102,6 +103,8 @@ export interface DriverConfig {
|
||||
hasUseGpt: boolean;
|
||||
hasMouseClose: boolean;
|
||||
hasEscClose: boolean;
|
||||
hasTransfer: boolean;
|
||||
hasReplace: boolean;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
|
||||
@@ -18,6 +18,8 @@ const Panels = () => {
|
||||
hasUseGpt: false,
|
||||
hasMouseClose: true,
|
||||
hasEscClose: true,
|
||||
hasTransfer: true,
|
||||
hasReplace: true,
|
||||
});
|
||||
|
||||
const { data } = useQuery(['config', 'driver'], async () => {
|
||||
@@ -38,22 +40,25 @@ const Panels = () => {
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const pausedHandler = useCallback((message: any) => {
|
||||
if (message.event === 'has_paused') {
|
||||
setDriverSettings((prevSettings) => ({
|
||||
...prevSettings,
|
||||
hasPaused: true,
|
||||
}));
|
||||
const pausedHandler = useCallback(
|
||||
(message: any) => {
|
||||
if (message.event === 'has_paused') {
|
||||
setDriverSettings((prevSettings) => ({
|
||||
...prevSettings,
|
||||
hasPaused: true,
|
||||
}));
|
||||
|
||||
toast({
|
||||
title: '自动回复已暂停',
|
||||
status: 'info',
|
||||
position: 'top',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
toast({
|
||||
title: '自动回复已暂停',
|
||||
status: 'info',
|
||||
position: 'top',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[toast],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = registerEventHandler(pausedHandler);
|
||||
@@ -149,7 +154,27 @@ const Panels = () => {
|
||||
</Tooltip>
|
||||
</Checkbox> */}
|
||||
<Checkbox
|
||||
isChecked={driverSettings.hasEscClose}
|
||||
isChecked={driverSettings.hasTransfer}
|
||||
onChange={(e) =>
|
||||
handleUpdateConfig({ hasTransfer: e.target.checked })
|
||||
}
|
||||
>
|
||||
<Tooltip label="如果匹配到设定的关键词,将自动转人工">
|
||||
关键词转人工
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
isChecked={driverSettings.hasReplace}
|
||||
onChange={(e) =>
|
||||
handleUpdateConfig({ hasReplace: e.target.checked })
|
||||
}
|
||||
>
|
||||
<Tooltip label="如果匹配到设定的关键词,将自动替换成自定义的关键词">
|
||||
关键词替换
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
isChecked={driverSettings.hasReplace}
|
||||
onChange={(e) =>
|
||||
handleUpdateConfig({ hasEscClose: e.target.checked })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Stack,
|
||||
Text,
|
||||
Icon,
|
||||
Box,
|
||||
Tooltip,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiHelpCircle } from 'react-icons/fi';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import MyTextarea from '../../../common/components/MyTextarea';
|
||||
|
||||
type ReplyInputProps = {
|
||||
newReply: string;
|
||||
setNewReply: (value: string) => void;
|
||||
handleAddReply: () => void;
|
||||
handleInsertRandomChar: () => void;
|
||||
};
|
||||
|
||||
const ReplyInput = ({
|
||||
newReply,
|
||||
setNewReply,
|
||||
handleAddReply,
|
||||
handleInsertRandomChar,
|
||||
}: ReplyInputProps) => (
|
||||
<>
|
||||
<Flex mb="8px" mt="22px">
|
||||
<Text mr={2} fontSize={'large'} fontWeight={'bold'}>
|
||||
回复内容
|
||||
</Text>
|
||||
<Tooltip label="添加的多个关键词只要一个匹配上了,将会触发回复,如果有多个回复,将会随机选择一个回复。">
|
||||
<Box color={'gray.500'}>
|
||||
<Icon as={FiHelpCircle} w={6} h={6} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Tooltip label="在拼多多平台等平台,是不允许每次重复一个回答的,所以可以插入一个随机符,以规避这个问题">
|
||||
<Button onClick={handleInsertRandomChar} mt="4" mr={4} colorScheme="teal">
|
||||
插入随机符
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Stack direction="row" mt="4">
|
||||
<MyTextarea
|
||||
mb="4"
|
||||
maxLength={200}
|
||||
placeholder="回复内容"
|
||||
value={newReply}
|
||||
onChange={(e) => setNewReply(e.target.value)}
|
||||
/>
|
||||
<Button onClick={handleAddReply} colorScheme="green">
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ReplyInput;
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Stack,
|
||||
Text,
|
||||
Icon,
|
||||
HStack,
|
||||
Box,
|
||||
Switch,
|
||||
Select,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Flex,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
|
||||
type ReplyListProps = {
|
||||
replyList: string[];
|
||||
handleReplyClick: (reply: string, index: number) => void;
|
||||
handleDeleteReply: (index: number) => void;
|
||||
};
|
||||
|
||||
const ReplyList = ({
|
||||
replyList,
|
||||
handleReplyClick,
|
||||
handleDeleteReply,
|
||||
}: ReplyListProps) => (
|
||||
<Stack direction="row" spacing={4} wrap="wrap">
|
||||
{replyList.map((item, index) => (
|
||||
<Text
|
||||
key={index}
|
||||
p="1"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
cursor="pointer"
|
||||
maxWidth="220px"
|
||||
onClick={() => handleReplyClick(item, index)}
|
||||
style={{ whiteSpace: 'normal', wordWrap: 'break-word' }}
|
||||
>
|
||||
{item}
|
||||
<IconButton
|
||||
ml={3}
|
||||
aria-label="Delete reply"
|
||||
icon={<DeleteIcon />}
|
||||
colorScheme="red"
|
||||
size="xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteReply(index);
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
export default ReplyList;
|
||||
@@ -0,0 +1,123 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import ReplyInput from './ReplyInput';
|
||||
import ReplyList from './ReplyList';
|
||||
|
||||
type EditKeywordProps = {
|
||||
reply: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleEdit: (val: string) => void;
|
||||
};
|
||||
|
||||
const EditKeyword = ({
|
||||
reply,
|
||||
isOpen,
|
||||
onClose,
|
||||
handleEdit,
|
||||
}: EditKeywordProps) => {
|
||||
const toast = useToast();
|
||||
const [replyList, setReplyList] = useState<string[]>(
|
||||
reply.split('[or]') || [],
|
||||
);
|
||||
|
||||
const [newReply, setNewReply] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!reply) {
|
||||
setReplyList([]);
|
||||
} else {
|
||||
setReplyList(reply.split('[or]') || []);
|
||||
}
|
||||
}, [reply]);
|
||||
|
||||
const handleAddReply = () => {
|
||||
if (newReply) {
|
||||
setReplyList([...replyList, newReply]);
|
||||
setNewReply('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteReply = (index: number) => {
|
||||
setReplyList(replyList.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleInsertRandomChar = () => {
|
||||
setNewReply(`${newReply}[~]`);
|
||||
};
|
||||
|
||||
const handleReplyClick = (item: string, index: number) => {
|
||||
setNewReply(item);
|
||||
handleDeleteReply(index);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (reply === '') {
|
||||
throw new Error('回复内容不能为空');
|
||||
}
|
||||
|
||||
await handleEdit(replyList.join('[or]'));
|
||||
toast({
|
||||
position: 'top',
|
||||
title: '保存成功',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size={'4xl'}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>编辑默认回复</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<ReplyInput
|
||||
newReply={newReply}
|
||||
setNewReply={setNewReply}
|
||||
handleAddReply={handleAddReply}
|
||||
handleInsertRandomChar={handleInsertRandomChar}
|
||||
/>
|
||||
<ReplyList
|
||||
replyList={replyList}
|
||||
handleReplyClick={handleReplyClick}
|
||||
handleDeleteReply={handleDeleteReply}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme="blue" mr={3} onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditKeyword;
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
useToast,
|
||||
Stack,
|
||||
Skeleton,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
updateConfig,
|
||||
} from '../../../common/services/platform/controller';
|
||||
import { GenericConfig } from '../../../common/services/platform/platform.d';
|
||||
import EditKeyword from '../EditKeyword';
|
||||
|
||||
const GeneralSettings = ({
|
||||
appId,
|
||||
@@ -42,6 +44,12 @@ const GeneralSettings = ({
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const toast = useToast();
|
||||
const {
|
||||
isOpen: isOpenEditKeyword,
|
||||
onOpen: onOpenEditKeyword,
|
||||
onClose: onCloseEditKeyword,
|
||||
} = useDisclosure();
|
||||
|
||||
const { data, isLoading } = useQuery(
|
||||
['config', 'generic', appId, instanceId],
|
||||
async () => {
|
||||
@@ -204,10 +212,14 @@ const GeneralSettings = ({
|
||||
<Input
|
||||
placeholder="输入默认回复内容"
|
||||
value={config.defaultReply}
|
||||
disabled
|
||||
onChange={(e) =>
|
||||
handleUpdateConfig({ defaultReply: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Button ml={2} onClick={onOpenEditKeyword}>
|
||||
编辑
|
||||
</Button>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
|
||||
@@ -296,7 +308,7 @@ const GeneralSettings = ({
|
||||
</Flex>
|
||||
<Slider
|
||||
min={50}
|
||||
max={4000}
|
||||
max={210}
|
||||
step={5}
|
||||
value={config.truncateWordCount}
|
||||
onChange={(truncateWordCount) =>
|
||||
@@ -327,6 +339,34 @@ const GeneralSettings = ({
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
|
||||
<FormControl mt={3}>
|
||||
<FormLabel>
|
||||
{' '}
|
||||
<Tooltip label="因为抖店默认有个不可关闭的首条自动回复,所以如果要自动回复后还能继续回复,这里需要设置的和抖店那个回复内容一致">
|
||||
<Text mb="8px">抖店默认首条回复</Text>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
<Flex>
|
||||
<Input
|
||||
placeholder="抖店默认首条回复"
|
||||
max={5}
|
||||
value={config.jinritemaiDefaultReplyMatch}
|
||||
onChange={(e) =>
|
||||
handleUpdateConfig({
|
||||
jinritemaiDefaultReplyMatch: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
|
||||
<EditKeyword
|
||||
isOpen={isOpenEditKeyword}
|
||||
onClose={onCloseEditKeyword}
|
||||
reply={config.defaultReply}
|
||||
handleEdit={(reply) => handleUpdateConfig({ defaultReply: reply })}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user