mirror of
https://github.com/cs-lazy-tools/ChatGPT-On-CS.git
synced 2026-08-28 10:00:27 +08:00
update: 优化应用性能
This commit is contained in:
@@ -35,7 +35,12 @@ if (
|
||||
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"',
|
||||
),
|
||||
);
|
||||
execSync('npm run postinstall');
|
||||
|
||||
try {
|
||||
execSync('npm run postinstall');
|
||||
} catch (err) {
|
||||
console.log(chalk.black.bgRed('Failed to build postinstall files', err));
|
||||
}
|
||||
}
|
||||
|
||||
const configuration: webpack.Configuration = {
|
||||
@@ -115,7 +120,7 @@ const configuration: webpack.Configuration = {
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
...(skipDLLs
|
||||
...(skipDLLs || !fs.existsSync(manifest)
|
||||
? []
|
||||
: [
|
||||
new webpack.DllReferencePlugin({
|
||||
|
||||
@@ -49,6 +49,6 @@ ${chalk.bold(
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Native dependencies could not be checked');
|
||||
console.log('Native dependencies could not be checked', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { execSync } from 'child_process';
|
||||
import chalk from 'chalk';
|
||||
|
||||
try {
|
||||
execSync('electron-builder install-app-deps', { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
// 错误处理: 输出错误信息和退出码
|
||||
console.error(
|
||||
chalk.red('Failed to install dependencies with electron-builder:'),
|
||||
);
|
||||
console.error(chalk.red(`Error message: ${error.message}`)); // 错误消息
|
||||
if (error instanceof Error && error.stack) {
|
||||
console.error(chalk.red(`Stack trace: ${error.stack}`)); // 堆栈跟踪
|
||||
}
|
||||
console.error(chalk.red(`Exit code: ${error.status}`)); // 错误码
|
||||
process.exit(error.status || 1); // 使用捕获到的退出码作为进程的退出码,或者默认为 1
|
||||
}
|
||||
@@ -26,6 +26,8 @@ module.exports = {
|
||||
'react/no-array-index-key': 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'no-console': 'off',
|
||||
'max-classes-per-file': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
|
||||
Vendored
+4
-1
@@ -26,5 +26,8 @@
|
||||
"test/**/__snapshots__": true,
|
||||
"package-lock.json": true,
|
||||
"*.{css,sass,scss}.d.ts": true
|
||||
}
|
||||
},
|
||||
"cSpell.words": [
|
||||
"dify"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -46,4 +46,46 @@ pnpm i
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## 构建 Electron 的 Sqlite3
|
||||
注意 Electron 有个 Sqlite3 的坑,需要重新编译,可以参考以下命令
|
||||
|
||||
```bash
|
||||
pnpm --prefix=node_modules/sqlite3 run rebuild
|
||||
|
||||
https://registry.npmmirror.com
|
||||
|
||||
# 使用 Visual Studio 社区中的 Desktop development with C++ 组件安装 C++ 工具集
|
||||
|
||||
nvm use 18
|
||||
|
||||
npm install --global windows-build-tools
|
||||
|
||||
pnpm config set msvs_version=2022
|
||||
npm config set msvs_version 2022
|
||||
|
||||
# 删除node_modules目录和package-lock.json文件:
|
||||
|
||||
rm -rf node_modules
|
||||
rm package-lock.json
|
||||
|
||||
# 重新编译sqlite3模块:
|
||||
|
||||
npm i -g node-gyp
|
||||
|
||||
# 项目根目录下执行
|
||||
pnpm i -D node-addon-api@7.0.0
|
||||
|
||||
cd node_modules/sqlite3
|
||||
|
||||
|
||||
node-gyp rebuild --verbose
|
||||
# 或者执行
|
||||
node-gyp rebuild --target=26.2.1 --arch=x64 --target_platform=win32 --dist-url=https://electronjs.org/headers --module_name=node_sqlite3 --module_path=../lib/binding/electron-v26.2.1-win32-x64
|
||||
|
||||
# 注意检查一下报错,可能需要在下面路径创建 node-addon-api(复制过去)
|
||||
|
||||
pnpm i -D electron-builder
|
||||
pnpm run postinstall
|
||||
```
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -9,6 +9,7 @@ files:
|
||||
- package.json
|
||||
afterSign: ".erb/scripts/notarize.js"
|
||||
win:
|
||||
requestedExecutionLevel: requireAdministrator
|
||||
target:
|
||||
- nsis
|
||||
nsis:
|
||||
@@ -28,6 +29,4 @@ directories:
|
||||
output: release/build
|
||||
extraResources:
|
||||
- "./assets/**"
|
||||
# build:
|
||||
# afterPack: "./removeLocales.js"
|
||||
afterPack: "./removeLocales.js"
|
||||
+40
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chatgpt-on-cs",
|
||||
"description": "多平台智能客服,允许使用 ChatGPT 作为客服机器人",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"keywords": [
|
||||
"electron",
|
||||
"boilerplate",
|
||||
@@ -101,15 +101,32 @@
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@hw-hmscore/analytics-web": "6.9.9-301",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"axios": "^1.6.7",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.4",
|
||||
"dottie": "^2.0.6",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
"express": "^4.19.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"framer-motion": "^11.0.3",
|
||||
"immer": "^10.0.3",
|
||||
"inflection": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.18.3",
|
||||
"luxon": "^3.4.4",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"ms": "^2.1.3",
|
||||
"net": "^1.0.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^4.38.3",
|
||||
"pg-connection-string": "^2.6.4",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.50.1",
|
||||
@@ -118,8 +135,19 @@
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-spinners": "^0.13.8",
|
||||
"react-use-websocket": "^4.8.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"retry-as-promised": "^7.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"sequelize": "^6.37.3",
|
||||
"sequelize-pool": "^8.0.0",
|
||||
"socket.io": "^4.7.5",
|
||||
"source-map-support": "^0.5.21",
|
||||
"toposort-class": "^1.0.1",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.11.0",
|
||||
"wkx": "^0.5.0",
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -130,15 +158,21 @@
|
||||
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
|
||||
"@testing-library/jest-dom": "^6.1.3",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "20.6.2",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-test-renderer": "^18.0.1",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/terser-webpack-plugin": "^5.0.4",
|
||||
"@types/webpack-bundle-analyzer": "^4.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||
"@typescript-eslint/parser": "^6.7.0",
|
||||
"app-builder-lib": "25.0.0-alpha.6",
|
||||
"browserslist-config-erb": "^0.0.3",
|
||||
"chalk": "^4.1.2",
|
||||
"concurrently": "^8.2.1",
|
||||
@@ -149,8 +183,9 @@
|
||||
"detect-port": "^1.5.1",
|
||||
"dotenv": "^16.4.4",
|
||||
"electron": "^26.2.1",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"electronmon": "^2.0.2",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
@@ -170,6 +205,7 @@
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"node-addon-api": "file:C:/Users/33204/Documents/project/alsritter.icu/lazy-rap-web/tmp/node-addon-api",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-eslint": "^16.3.0",
|
||||
"react-refresh": "^0.14.0",
|
||||
@@ -177,12 +213,14 @@
|
||||
"rimraf": "^5.0.1",
|
||||
"sass": "^1.67.0",
|
||||
"sass-loader": "^13.3.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"style-loader": "^3.3.3",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.2.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.88.2",
|
||||
|
||||
Generated
+3672
-2429
File diff suppressed because it is too large
Load Diff
Generated
+1237
-4
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chatgpt-on-cs",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"description": "多平台智能客服,允许使用 ChatGPT 作为客服机器人",
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
@@ -13,6 +13,8 @@
|
||||
"rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
|
||||
"postinstall": "pnpm run rebuild && pnpm run link-modules",
|
||||
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts"
|
||||
},
|
||||
"dependencies": {}
|
||||
},
|
||||
"dependencies": {
|
||||
"sqlite3": "^5.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-5
@@ -1,5 +0,0 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
@@ -0,0 +1,403 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import bodyParser from 'body-parser';
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import { BrowserWindow } from 'electron';
|
||||
import './ormconfig';
|
||||
import { StrategyServiceStatusEnum } from './constants/constants';
|
||||
import { SessionController } from './controllers/sessionController';
|
||||
import { ConfigController } from './controllers/configController';
|
||||
import { MessageController } from './controllers/messageController';
|
||||
import { StrategyService } from './services/strategyService';
|
||||
import { MessageService } from './services/messageService';
|
||||
import { SessionService } from './services/sessionService';
|
||||
import { BroadcastService } from './services/broadcastService';
|
||||
import { SystemService } from './services/systemService';
|
||||
import { ConfigService } from './services/configService';
|
||||
import { PlatformConfigController } from './controllers/platformConfigController';
|
||||
import { AutoReplyController } from './controllers/autoReplyController';
|
||||
|
||||
const sessionController = new SessionController();
|
||||
const configController = new ConfigController();
|
||||
const platformConfigController = new PlatformConfigController();
|
||||
const messageController = new MessageController();
|
||||
const autoReplyController = new AutoReplyController();
|
||||
|
||||
class BKServer {
|
||||
private app: express.Application;
|
||||
|
||||
private port: number;
|
||||
|
||||
private server: http.Server;
|
||||
|
||||
private io: Server;
|
||||
|
||||
private strategyService: StrategyService;
|
||||
|
||||
private messageService: MessageService;
|
||||
|
||||
private sessionService: SessionService;
|
||||
|
||||
private configService: ConfigService;
|
||||
|
||||
private broadcastService: BroadcastService;
|
||||
|
||||
private systemService: SystemService;
|
||||
|
||||
private ptfs: any[] = [];
|
||||
|
||||
constructor(port: number, mainWindow: BrowserWindow) {
|
||||
this.app = express();
|
||||
this.app.use(bodyParser.json());
|
||||
this.app.use(cors());
|
||||
this.port = port;
|
||||
|
||||
this.server = http.createServer(this.app);
|
||||
this.io = new Server(this.server, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST'],
|
||||
},
|
||||
connectionStateRecovery: {
|
||||
maxDisconnectionDuration: 2 * 60 * 1000,
|
||||
skipMiddlewares: true,
|
||||
// 配置使用 websocket
|
||||
},
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
this.messageService = new MessageService(
|
||||
configController,
|
||||
messageController,
|
||||
autoReplyController,
|
||||
);
|
||||
this.configService = new ConfigService(
|
||||
configController,
|
||||
platformConfigController,
|
||||
);
|
||||
this.broadcastService = new BroadcastService(mainWindow);
|
||||
this.strategyService = new StrategyService(this.io);
|
||||
this.sessionService = new SessionService(
|
||||
sessionController,
|
||||
this.strategyService,
|
||||
);
|
||||
this.systemService = new SystemService(this.io);
|
||||
this.configureSocketIO();
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
private configureSocketIO(): void {
|
||||
this.io.on('connection', (socket) => {
|
||||
console.log('Client connected registerHandlers');
|
||||
this.messageService.registerHandlers(socket);
|
||||
this.sessionService.registerHandlers(socket);
|
||||
this.configService.registerHandlers(socket);
|
||||
this.broadcastService.registerHandlers(socket);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('user disconnected');
|
||||
// 再关闭这个连接绑定的事件处理器
|
||||
socket.removeAllListeners();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// 查看消息列表
|
||||
this.app.get(
|
||||
'/api/v1/msg/list',
|
||||
asyncHandler(async (req, res) => {
|
||||
const {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
platform_id: platformId,
|
||||
keyword,
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
} = req.query;
|
||||
|
||||
const query = {
|
||||
page,
|
||||
pageSize,
|
||||
platformId,
|
||||
keyword,
|
||||
startTime,
|
||||
endTime,
|
||||
orderBy: 'messages.created_at desc',
|
||||
};
|
||||
|
||||
const { total, msgs } =
|
||||
// @ts-ignore
|
||||
await sessionController.listMessagesWithSessions(query);
|
||||
|
||||
// group messages
|
||||
const groupedMsgs = msgs.reduce((acc: any, msg) => {
|
||||
acc[msg.username] = acc[msg.username] || [];
|
||||
acc[msg.username].push(msg);
|
||||
return acc;
|
||||
}, {});
|
||||
res.json({
|
||||
success: true,
|
||||
data: groupedMsgs,
|
||||
total,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// 使用平台
|
||||
this.app.post(
|
||||
'/api/v1/base/platform',
|
||||
asyncHandler(async (req, res) => {
|
||||
const ids = req.body;
|
||||
console.log(`ids: ${ids}`);
|
||||
const data = await this.strategyService.updateStrategies(ids);
|
||||
res.json({ success: true, data });
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取所有平台
|
||||
this.app.get(
|
||||
'/api/v1/base/platform/all',
|
||||
asyncHandler(async (req, res) => {
|
||||
if (this.ptfs.length === 0) {
|
||||
this.ptfs = this.strategyService.getAllPlatforms();
|
||||
}
|
||||
|
||||
res.json({ success: true, data: this.ptfs });
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取平台设置
|
||||
this.app.get(
|
||||
'/api/v1/base/platform/settings',
|
||||
asyncHandler(async (req, res) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { total, data } = await platformConfigController.list({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
});
|
||||
res.json({ success: true, data });
|
||||
}),
|
||||
);
|
||||
|
||||
this.app.post(
|
||||
'/api/v1/base/platform/settings',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { platform_id: platformId, config } = req.body;
|
||||
await platformConfigController.updateByPlatformId(platformId, config);
|
||||
res.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
// Endpoint to update runner status based on incoming configuration
|
||||
this.app.post('/api/v1/base/runner', async (req, res) => {
|
||||
const {
|
||||
is_paused: isPaused,
|
||||
is_keyword_match: isKeywordMatch,
|
||||
ids,
|
||||
} = req.body;
|
||||
try {
|
||||
if (isPaused) {
|
||||
await this.strategyService.updateStatus(
|
||||
StrategyServiceStatusEnum.STOPPED,
|
||||
);
|
||||
} else {
|
||||
await this.strategyService.updateStatus(
|
||||
StrategyServiceStatusEnum.RUNNING,
|
||||
);
|
||||
}
|
||||
|
||||
if (isKeywordMatch) {
|
||||
this.messageService.updateKeywordMatch(isKeywordMatch);
|
||||
}
|
||||
|
||||
await this.strategyService.updateStrategies(ids);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to retrieve configuration settings
|
||||
this.app.get('/api/v1/base/settings', async (req, res) => {
|
||||
try {
|
||||
const config = await configController.getConfig();
|
||||
res.json({ success: true, data: config });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to update configuration settings
|
||||
this.app.post('/api/v1/base/settings', async (req, res) => {
|
||||
try {
|
||||
const cfg = req.body;
|
||||
await configController.updateConfig(1, cfg); // Assuming the ID is known and static
|
||||
// globalVariable.merged_message_num = cfg.merged_message_num;
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.app.get('/api/v1/reply/list', async (req, res) => {
|
||||
const { page = 1, page_size: pageSize, ptf_id: platformId } = req.query;
|
||||
|
||||
const query = {
|
||||
page,
|
||||
pageSize,
|
||||
platformId,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const { total, autoReplies } = await autoReplyController.list(query);
|
||||
|
||||
const data = autoReplies;
|
||||
if (this.ptfs.length === 0) {
|
||||
this.ptfs = this.strategyService.getAllPlatforms();
|
||||
}
|
||||
|
||||
const ptfMap = new Map(this.ptfs.map((ptf) => [ptf.id, ptf]));
|
||||
const results: any[] = [];
|
||||
data.forEach((item) => {
|
||||
const ptfId = item.platform_id;
|
||||
const ptf = ptfMap.get(ptfId);
|
||||
|
||||
const result = {
|
||||
id: item.id,
|
||||
platform_id: item.platform_id,
|
||||
keyword: item.keyword,
|
||||
reply: item.reply,
|
||||
mode: item.mode,
|
||||
ptf_name: ptf ? ptf.name : '全局',
|
||||
};
|
||||
|
||||
results.push(result);
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results,
|
||||
total,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
});
|
||||
|
||||
this.app.post('/api/v1/reply/create', async (req, res) => {
|
||||
const { platform_id: platformId, keyword, reply, mode } = req.body;
|
||||
await autoReplyController.create({
|
||||
mode,
|
||||
platform_id: platformId,
|
||||
keyword,
|
||||
reply,
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
this.app.post('/api/v1/reply/update', async (req, res) => {
|
||||
const { id, platform_id: platformId, keyword, reply, mode } = req.body;
|
||||
await autoReplyController.update(id, {
|
||||
mode,
|
||||
platform_id: platformId,
|
||||
keyword,
|
||||
reply,
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
this.app.post('/api/v1/reply/delete', async (req, res) => {
|
||||
const { id } = req.body;
|
||||
await autoReplyController.delete(id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
// TODO: 后续需要根据通过 WS 去检查后端服务是否健康
|
||||
this.app.get('/api/v1/base/health', async (req, res) => {
|
||||
try {
|
||||
const resp = await this.systemService.checkHealth();
|
||||
if (resp) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.json({
|
||||
success: false,
|
||||
data: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 检查 GPT 链接是否正常
|
||||
this.app.get('/api/v1/base/gpt/health', async (req, res) => {
|
||||
const { base_url: gptBaseUrl, key, use_dify: useDify, model } = req.query;
|
||||
// useDify 是 string 类型,需要转换为 boolean 类型
|
||||
const dify = useDify === 'true';
|
||||
|
||||
try {
|
||||
const { status, message } = await this.messageService.checkApiHealth({
|
||||
baseUrl: String(gptBaseUrl),
|
||||
apiKey: String(key),
|
||||
useDify: dify,
|
||||
model: String(model),
|
||||
});
|
||||
res.json({
|
||||
status,
|
||||
message,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.json({
|
||||
status: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 启动服务器的方法
|
||||
start() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = this.server
|
||||
.listen(this.port, () => {
|
||||
console.log(`Server is running on http://localhost:${this.port}`);
|
||||
resolve(true);
|
||||
})
|
||||
.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// 停止服务器的方法
|
||||
stop() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.server) {
|
||||
this.server.close((err: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
} else {
|
||||
reject(new Error('Server not initialized'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default BKServer;
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum StrategyServiceStatusEnum {
|
||||
RUNNING = 'RUNNING',
|
||||
STOPPED = 'STOPPED',
|
||||
}
|
||||
|
||||
export enum PlatformTypeEnum {
|
||||
HOT = 'HOT',
|
||||
E_COMMERCE = 'E_COMMERCE',
|
||||
RECRUIT = 'RECRUIT',
|
||||
LAW = 'LAW',
|
||||
OTHER = 'OTHER',
|
||||
ME_MEDIA = 'ME_MEDIA',
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { AutoReply } from '../entities/autoReply';
|
||||
|
||||
export class AutoReplyController {
|
||||
async create(autoReplyData: any) {
|
||||
return AutoReply.create(autoReplyData);
|
||||
}
|
||||
|
||||
async update(id: number, autoReplyData: any) {
|
||||
const autoReply = await AutoReply.findByPk(id);
|
||||
if (!autoReply) {
|
||||
throw new Error('AutoReply not found');
|
||||
}
|
||||
await autoReply.update(autoReplyData);
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
const autoReply = await AutoReply.findByPk(id);
|
||||
if (!autoReply) {
|
||||
throw new Error('AutoReply not found');
|
||||
}
|
||||
await autoReply.destroy();
|
||||
}
|
||||
|
||||
async getKeywords(platformId: string) {
|
||||
const autoReplies = await AutoReply.findAll({
|
||||
where: {
|
||||
platform_id: platformId,
|
||||
},
|
||||
});
|
||||
|
||||
const globalKeywords = await AutoReply.findAll({
|
||||
where: {
|
||||
platform_id: '',
|
||||
},
|
||||
});
|
||||
|
||||
return [...globalKeywords, ...autoReplies];
|
||||
}
|
||||
|
||||
async list({
|
||||
page,
|
||||
pageSize,
|
||||
platformId,
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
platformId: string;
|
||||
}) {
|
||||
try {
|
||||
const { rows: autoReplies, count: total } =
|
||||
await AutoReply.findAndCountAll({
|
||||
where: platformId
|
||||
? {
|
||||
platform_id: platformId,
|
||||
}
|
||||
: {},
|
||||
offset: (page - 1) * pageSize,
|
||||
limit: pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
autoReplies,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error in AutoReplyController.list:', error.message);
|
||||
return {
|
||||
total: 0,
|
||||
autoReplies: [],
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Config } from '../entities/config';
|
||||
|
||||
export class ConfigController {
|
||||
async getConfig(): Promise<Config> {
|
||||
try {
|
||||
const config = await Config.findByPk(1);
|
||||
if (!config) {
|
||||
throw new Error('Config not found');
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
const newConfig = await Config.create({
|
||||
extract_phone: true,
|
||||
extract_product: true,
|
||||
save_path: '',
|
||||
reply_speed: 0,
|
||||
merged_message_num: 7,
|
||||
wait_humans_time: 60,
|
||||
gpt_base_url: 'https://api.openai.com/v1',
|
||||
gpt_key: 'your-key',
|
||||
gpt_model: 'gpt-3.5-turbo',
|
||||
gpt_temperature: 0.7,
|
||||
gpt_top_p: 0.9,
|
||||
stream: true,
|
||||
use_lazy: false,
|
||||
lazy_key: 'your-key',
|
||||
});
|
||||
return newConfig;
|
||||
}
|
||||
}
|
||||
|
||||
async updateConfig(id: number, configData: Partial<Config>): Promise<Config> {
|
||||
const config = await Config.findByPk(id);
|
||||
if (!config) {
|
||||
throw new Error('Config not found');
|
||||
}
|
||||
|
||||
const data = await config.update(configData);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { GlobalParam } from '../entities/globalParam';
|
||||
|
||||
export class GlobalParamController {
|
||||
async create(globalParamData: any) {
|
||||
return GlobalParam.create(globalParamData);
|
||||
}
|
||||
|
||||
async update(id: number, globalParamData: any) {
|
||||
const globalParam = await GlobalParam.findByPk(id);
|
||||
if (!globalParam) {
|
||||
throw new Error('GlobalParam not found');
|
||||
}
|
||||
return globalParam.update(globalParamData);
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
const globalParam = await GlobalParam.findByPk(id);
|
||||
if (!globalParam) {
|
||||
throw new Error('GlobalParam not found');
|
||||
}
|
||||
return globalParam.destroy();
|
||||
}
|
||||
|
||||
async list() {
|
||||
return GlobalParam.findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Message } from '../entities/message';
|
||||
import { MessageDTO } from '../types';
|
||||
|
||||
export class MessageController {
|
||||
async create(messageData: Message) {
|
||||
// @ts-ignore
|
||||
const data = await Message.create(messageData);
|
||||
return data;
|
||||
}
|
||||
|
||||
async update(id: number, messageData: Message) {
|
||||
const message = await Message.findByPk(id);
|
||||
if (!message) {
|
||||
throw new Error('Message not found');
|
||||
}
|
||||
await message.update(messageData);
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
const message = await Message.findByPk(id);
|
||||
if (!message) {
|
||||
throw new Error('Message not found');
|
||||
}
|
||||
await message.destroy();
|
||||
}
|
||||
|
||||
async list({
|
||||
page,
|
||||
pageSize,
|
||||
sessionId,
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sessionId: number;
|
||||
}) {
|
||||
const { rows: messages, count: total } = await Message.findAndCountAll({
|
||||
where: {
|
||||
session_id: sessionId,
|
||||
},
|
||||
offset: (page - 1) * pageSize,
|
||||
limit: pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
async checkExists(unique: string, sessionId: number) {
|
||||
const message = await Message.findOne({
|
||||
where: { session_id: sessionId, unique },
|
||||
});
|
||||
return !!message;
|
||||
}
|
||||
|
||||
async batchCreateMsgs(sessionId: number, msgs: Message[] | MessageDTO[]) {
|
||||
const messages = msgs.map((msg) => {
|
||||
return {
|
||||
...msg,
|
||||
session_id: sessionId,
|
||||
};
|
||||
});
|
||||
const data = await Message.bulkCreate(messages);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { PlatformConfig } from '../entities/platformConfig';
|
||||
|
||||
export class PlatformConfigController {
|
||||
async updateByPlatformId(
|
||||
platformId: string,
|
||||
platformConfigData: Partial<PlatformConfig>,
|
||||
): Promise<PlatformConfig> {
|
||||
const platformConfig = await PlatformConfig.findOne({
|
||||
where: { platform_id: platformId },
|
||||
});
|
||||
|
||||
if (platformConfig) {
|
||||
// @ts-ignore
|
||||
return this.update(platformConfig.id, platformConfigData);
|
||||
}
|
||||
return this.create({
|
||||
platform_id: platformId,
|
||||
...platformConfigData,
|
||||
});
|
||||
}
|
||||
|
||||
async getByPlatformId(platformId: string) {
|
||||
const data = await PlatformConfig.findOne({
|
||||
where: { platform_id: platformId },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new Error('PlatformConfig not found');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async create(platformConfigData: any): Promise<PlatformConfig> {
|
||||
const platformConfig = await PlatformConfig.create(platformConfigData);
|
||||
return platformConfig;
|
||||
}
|
||||
|
||||
async update(id: number, platformConfigData: Partial<PlatformConfig>) {
|
||||
const platformConfig = await PlatformConfig.findOne({
|
||||
where: { id },
|
||||
});
|
||||
// @ts-ignore
|
||||
const data = Object.assign(platformConfig, platformConfigData);
|
||||
return data;
|
||||
}
|
||||
|
||||
async delete(id: number): Promise<void> {
|
||||
const platformConfig = await PlatformConfig.findOne({
|
||||
where: { id },
|
||||
});
|
||||
if (!platformConfig) {
|
||||
return;
|
||||
}
|
||||
await platformConfig.destroy();
|
||||
}
|
||||
|
||||
async list(page: { page: number; pageSize: number }): Promise<{
|
||||
total: number;
|
||||
data: PlatformConfig[];
|
||||
}> {
|
||||
const data = await PlatformConfig.findAll({
|
||||
limit: page.pageSize,
|
||||
offset: (page.page - 1) * page.pageSize,
|
||||
});
|
||||
|
||||
const total = await PlatformConfig.count();
|
||||
|
||||
return { total, data };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Session } from '../entities/session';
|
||||
|
||||
export class SessionCreate {
|
||||
username: string;
|
||||
|
||||
platform_id: string;
|
||||
|
||||
platform: string;
|
||||
|
||||
goods_name: string;
|
||||
|
||||
goods_avatar: string;
|
||||
|
||||
last_active: Date;
|
||||
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export class SessionController {
|
||||
async create(sessionData: SessionCreate) {
|
||||
// @ts-ignore
|
||||
return Session.create(sessionData);
|
||||
}
|
||||
|
||||
async update(id: number, sessionData: Session) {
|
||||
const session = await Session.findByPk(id);
|
||||
if (!session) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
await session.update(sessionData);
|
||||
return session;
|
||||
}
|
||||
|
||||
async search(platformId: string, username: string) {
|
||||
return Session.findAll({
|
||||
where: {
|
||||
platform_id: platformId,
|
||||
username,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async listMessagesWithSessions(data: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
keyword?: string;
|
||||
platformId?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
orderBy: string;
|
||||
}): Promise<{
|
||||
total: number;
|
||||
msgs: any[];
|
||||
}> {
|
||||
const { page, pageSize, keyword, platformId, startTime, endTime, orderBy } =
|
||||
data;
|
||||
|
||||
// 构建查询条件和参数
|
||||
const conditions: string[] = [];
|
||||
const parameters: any[] = [];
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(`messages.content LIKE '%' || ? || '%'`);
|
||||
parameters.push(keyword);
|
||||
}
|
||||
if (platformId) {
|
||||
conditions.push(`sessions.platform_id = ?`);
|
||||
parameters.push(platformId);
|
||||
}
|
||||
if (startTime) {
|
||||
conditions.push(`sessions.created_at >= ?`);
|
||||
parameters.push(startTime);
|
||||
}
|
||||
if (endTime) {
|
||||
conditions.push(`sessions.created_at <= ?`);
|
||||
parameters.push(endTime);
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建 WHERE 语句
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
// 查询 session IDs
|
||||
const sessionIdsQuery = `
|
||||
SELECT DISTINCT sessions.id
|
||||
FROM sessions
|
||||
JOIN messages ON sessions.id = messages.session_id
|
||||
${whereClause}
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const sessionIds = await Session.sequelize?.query(sessionIdsQuery, {
|
||||
replacements: [...parameters, pageSize, (page - 1) * pageSize],
|
||||
type: 'SELECT',
|
||||
});
|
||||
|
||||
if (!sessionIds) {
|
||||
return { total: 0, msgs: [] };
|
||||
}
|
||||
|
||||
// 查询消息
|
||||
const messagesQuery = `
|
||||
SELECT messages.*, sessions.*
|
||||
FROM messages
|
||||
JOIN sessions ON messages.session_id = sessions.id
|
||||
WHERE messages.session_id IN (${sessionIds.map((s: any) => s.id).join(',')})
|
||||
ORDER BY messages.created_at DESC
|
||||
`;
|
||||
|
||||
const messages = await Session.sequelize?.query(messagesQuery, {
|
||||
type: 'SELECT',
|
||||
});
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(DISTINCT sessions.id) AS total
|
||||
FROM sessions
|
||||
JOIN messages ON sessions.id = messages.session_id
|
||||
${whereClause}
|
||||
`;
|
||||
|
||||
const totalCount = await Session.sequelize?.query(countQuery, {
|
||||
replacements: parameters,
|
||||
type: 'SELECT',
|
||||
});
|
||||
|
||||
return {
|
||||
// @ts-ignore
|
||||
total: totalCount[0].total,
|
||||
// @ts-ignore
|
||||
msgs: messages,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(
|
||||
'listMessagesWithSessions error:',
|
||||
error.message,
|
||||
error.stack,
|
||||
);
|
||||
}
|
||||
return { total: 0, msgs: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
// Extend the Model class with the attributes interface
|
||||
export class AutoReply extends Model {
|
||||
declare id: number; // Note that the `null assertion` `!` is required in strict mode.
|
||||
|
||||
declare keyword: string;
|
||||
|
||||
declare reply: string;
|
||||
|
||||
declare mode: string;
|
||||
|
||||
declare platform_id: string;
|
||||
}
|
||||
|
||||
export function initAutoReply(sequelize: Sequelize) {
|
||||
AutoReply.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
keyword: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
reply: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
mode: {
|
||||
type: DataTypes.STRING(55),
|
||||
allowNull: false,
|
||||
},
|
||||
platform_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'AutoReply',
|
||||
tableName: 'auto_reply',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
// Extend the Model class with the attributes interface
|
||||
export class Config extends Model {
|
||||
declare id: number; // Note that the `null assertion` `!` is required in strict mode.
|
||||
|
||||
declare extract_phone: boolean;
|
||||
|
||||
declare extract_product: boolean;
|
||||
|
||||
declare save_path: string;
|
||||
|
||||
declare reply_speed: number;
|
||||
|
||||
declare merged_message_num: number;
|
||||
|
||||
declare wait_humans_time: number;
|
||||
|
||||
declare gpt_base_url: string;
|
||||
|
||||
declare gpt_key: string;
|
||||
|
||||
declare use_dify: boolean;
|
||||
|
||||
declare gpt_model: string;
|
||||
|
||||
declare gpt_temperature: number;
|
||||
|
||||
declare gpt_top_p: number;
|
||||
|
||||
declare stream: boolean;
|
||||
|
||||
declare use_lazy: boolean;
|
||||
|
||||
declare lazy_key: string;
|
||||
}
|
||||
|
||||
export function initConfig(sequelize: Sequelize) {
|
||||
Config.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
extract_phone: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
extract_product: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
save_path: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
reply_speed: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false,
|
||||
},
|
||||
merged_message_num: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false,
|
||||
},
|
||||
wait_humans_time: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false,
|
||||
},
|
||||
gpt_base_url: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
gpt_key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
use_dify: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
gpt_model: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
gpt_temperature: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false,
|
||||
},
|
||||
gpt_top_p: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false,
|
||||
},
|
||||
stream: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
},
|
||||
use_lazy: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
},
|
||||
lazy_key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Config',
|
||||
tableName: 'config',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
// Extend the Model class with the attributes interface
|
||||
export class GlobalParam extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare key: string;
|
||||
|
||||
declare value: string;
|
||||
}
|
||||
|
||||
export function initGlobalParam(sequelize: Sequelize) {
|
||||
GlobalParam.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
key: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'GlobalParam',
|
||||
tableName: 'global_params',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
// Extend the Model class with the attributes interface
|
||||
export class Message extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare session_id: number;
|
||||
|
||||
declare role: string;
|
||||
|
||||
declare content: string;
|
||||
|
||||
declare unique: string;
|
||||
|
||||
declare msg_type: string;
|
||||
|
||||
declare created_at: Date;
|
||||
}
|
||||
|
||||
export function initMessage(sequelize: Sequelize) {
|
||||
Message.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
session_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
unique: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
msg_type: {
|
||||
type: DataTypes.STRING(55),
|
||||
allowNull: false,
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Message',
|
||||
tableName: 'messages',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
// Extend the Model class with the attributes interface
|
||||
export class PlatformConfig extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare platform_id: string;
|
||||
|
||||
declare openai_url: string;
|
||||
|
||||
declare api_key: string;
|
||||
|
||||
declare prompt: string;
|
||||
|
||||
declare active: boolean;
|
||||
}
|
||||
|
||||
export function initPlatformConfig(sequelize: Sequelize) {
|
||||
PlatformConfig.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
platform_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
openai_url: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
api_key: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
prompt: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'PlatformConfig',
|
||||
tableName: 'platform_config',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
export class Session extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare username: string;
|
||||
|
||||
declare last_active: Date;
|
||||
|
||||
declare platform: string;
|
||||
|
||||
declare platform_id: string;
|
||||
|
||||
declare goods_name: string;
|
||||
|
||||
declare goods_avatar: string;
|
||||
|
||||
declare created_at: Date;
|
||||
}
|
||||
|
||||
export function initSession(sequelize: Sequelize) {
|
||||
Session.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
last_active: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
platform: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
platform_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
goods_name: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
goods_avatar: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Session',
|
||||
tableName: 'sessions',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class TimeoutError extends Error {}
|
||||
|
||||
export class HumanTaskError extends Error {}
|
||||
@@ -0,0 +1,173 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
// https://github.com/sql-js/sql.js/issues/183
|
||||
import sqlite from 'sqlite3';
|
||||
import { Sequelize } from 'sequelize';
|
||||
import { initConfig } from './entities/config';
|
||||
import { initPlatformConfig } from './entities/platformConfig';
|
||||
import { initSession } from './entities/session';
|
||||
import { initMessage } from './entities/message';
|
||||
import { AutoReply, initAutoReply } from './entities/autoReply';
|
||||
import { initGlobalParam } from './entities/globalParam';
|
||||
|
||||
// Get user's documents directory path
|
||||
const DOCUMENTS_DIR = path.join(os.homedir(), 'Documents');
|
||||
const APP_DIR = path.join(DOCUMENTS_DIR, 'chatgpt-on-cs');
|
||||
fs.mkdirSync(APP_DIR, { recursive: true });
|
||||
|
||||
// Get system's temporary directory path
|
||||
const TEMP_DIR = path.join(os.tmpdir(), 'chatgpt-on-cs');
|
||||
const LOGS_DIR = path.join(TEMP_DIR, 'logs');
|
||||
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
||||
|
||||
const DB_FILE_PATH = path.join(APP_DIR, 'msg.db');
|
||||
|
||||
console.log('DB_FILE_PATH:', DB_FILE_PATH);
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
dialectModule: sqlite,
|
||||
storage: DB_FILE_PATH,
|
||||
logging: console.log,
|
||||
});
|
||||
|
||||
// 初始化模型
|
||||
initConfig(sequelize);
|
||||
initPlatformConfig(sequelize);
|
||||
initSession(sequelize);
|
||||
initMessage(sequelize);
|
||||
initAutoReply(sequelize);
|
||||
initGlobalParam(sequelize);
|
||||
|
||||
// 异步初始化和数据填充函数
|
||||
async function initDb(): Promise<void> {
|
||||
const count = await AutoReply.count();
|
||||
if (count === 0) {
|
||||
const replies = [
|
||||
{
|
||||
keyword:
|
||||
'时候[and]发货|啥时[and]发货|多久[and]发货|今天[and]发货|尽快[and]发货',
|
||||
reply:
|
||||
'您好亲,我们发货是按订单顺序的发货哦。您着急的话我们会优先帮您安排的,不用担心哦。[or]您这边着急是吗,我这边备注一下。帮您先安排可以吗亲[or]懂了,您这边比较急是吗,我们先帮您安排可以吗亲。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '你好|您好|在吗|在不|在么|在?|在?',
|
||||
reply:
|
||||
'您好,在的亲[or]亲,我在的呢[or]在的哦亲,需要点什么呢[or]在哦亲,有什么可以帮您的吗?',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '好呢|好的|好吧|行的|可以哦',
|
||||
reply: '嗯呢[or]嗯嗯[or]好呢',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '谢谢|感谢|谢了|谢你|谢您',
|
||||
reply:
|
||||
'不客气的哦亲[or]没事的呢,很荣幸能帮您[or]不用谢哦亲,应该的呢[or]不客气哦,非常荣幸为您服务哦。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '什么[and]快递',
|
||||
reply:
|
||||
'您好,我们是随机安排的,一般来说可能根据地区稍有区别。放心时效都很快的哦。[or]亲,我们会根据您的地区选择最快时效的快递哦。您放心呢[or]亲,这个不用担心,我们会根据您的地区帮您安排运输最快速稳定的快递哦。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '退款[and]怎么办|怎么[and]退款|退款[and]流程|退款[and]操作',
|
||||
reply:
|
||||
'亲,退款的话需要您在订单详情里申请退款哦,我们会尽快为您处理的。[or]您好,退款流程是在订单详情中提交退款申请,提交后我们会第一时间审核处理。[or]亲爱的,关于退款,您可以直接在订单里操作申请退款,有任何问题我都在这里帮您。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '损坏|破损|有损|坏了',
|
||||
reply:
|
||||
'亲,商品损坏了真的很抱歉。麻烦您提供一下损坏的照片,我们这边帮您处理一下。[or]很抱歉给您带来不便,亲可以发一下损坏部分的照片吗?我们会尽快为您解决。[or]哦不,听闻商品有损真是让人难过,您能否提供损坏的图片呢?我们好进行下一步处理。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '没收到货|怎么还没到|包裹[and]没到|快递[and]没收到',
|
||||
reply:
|
||||
'亲,可能是快递延误了,请您稍等一下,我这边帮您查一查物流信息。[or]很抱歉让您久等了,让我立刻为您查询快递进度,请稍候。[or]亲,有时快递会有些许延迟,我帮您看看具体情况,马上回复您。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '优惠券|折扣|活动|促销',
|
||||
reply:
|
||||
'亲,我们店铺目前有进行促销活动哦,您可以查看我们的活动页面。[or]目前我们有提供优惠券和折扣,详细信息您可以在商品页面查看哦。[or]亲爱的,关于优惠活动,您可以在我们的促销专区看到当前所有的优惠信息哦。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '尺码|大小|合适|不合适',
|
||||
reply:
|
||||
'亲,具体的尺码信息您可以在商品详情页找到尺码表哦,有疑问随时问我。[or]关于尺码问题,我们页面有详细的尺码对照表,您可以参考一下。[or]亲,为了确保合身,建议您根据我们提供的尺码表来选择哦。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '账号登录问题|无法登录|忘记密码',
|
||||
reply:
|
||||
'亲,如果忘记密码,您可以尝试使用忘记密码功能来重置密码。具体操作是在登录页面选择忘记密码,按提示操作。[or]遇到登录问题,建议您先检查一下账号信息是否输入正确,如果忘记密码,可以通过忘记密码进行重置哦。[or]登录时遇到问题,不用担心,可以尝试重置密码或检查网络设置,如还有问题,随时联系我们。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '产品质量怎么样|好用吗|耐用吗',
|
||||
reply:
|
||||
'我们的产品都是经过严格质量检验的,质量上乘,使用起来非常耐用且效果显著。[or]亲,我们的商品质量保证,收到货后如果有任何不满意都可以联系我们哦。[or]我们的产品质量非常好,很多顾客反馈使用效果很好,您可以放心购买。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '发票问题|需要发票|怎么开发票',
|
||||
reply:
|
||||
'亲,需要开具发票的话,在下单时可以选择开具发票,并按照提示填写相关信息。具体发票问题也可以联系客服解决。[or]关于发票,我们支持电子发票和纸质发票,下单时可以根据需求选择,有问题随时联系我们。[or]开发票您不用担心,下单后在订单备注里说明,或者直接联系客服都可以办理。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '商品有货吗|库存情况|现货吗',
|
||||
reply:
|
||||
'亲,我们的商品都是有现货的,下单后会尽快为您发货。[or]目前库存充足,您可以放心下单,我们会及时处理您的订单。[or]商品都是现货供应,您可以直接下单,我们会立即为您安排发货。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
{
|
||||
keyword: '颜色选择|有哪些颜色|颜色款式',
|
||||
reply:
|
||||
'亲,我们店铺的商品有多种颜色可选,具体颜色款式您可以在商品详情页查看。[or]每款商品都有多种颜色,您可以根据自己的喜好选择,详情页有展示哦。[or]关于颜色,我们提供了丰富的选择,您可以在商品页面查看所有可选的颜色哦。',
|
||||
mode: 'fuzzy',
|
||||
platform_id: '',
|
||||
},
|
||||
];
|
||||
|
||||
await AutoReply.bulkCreate(replies);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await sequelize.sync();
|
||||
console.log('Database connection has been established successfully.');
|
||||
await initDb();
|
||||
} catch (error) {
|
||||
console.error('Failed to sync database:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
export default sequelize;
|
||||
@@ -0,0 +1,25 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
export class BroadcastService {
|
||||
private mainWindow: BrowserWindow;
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
public registerHandlers(socket: socketIo.Socket): void {
|
||||
socket.on('broadcastService-sendBroadcast', (msg: any, callback) => {
|
||||
const { event_id: eventId, message } = msg;
|
||||
this.receiveBroadcast(msg);
|
||||
callback({
|
||||
event_id: eventId,
|
||||
event_type: message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public receiveBroadcast(msg: any): void {
|
||||
this.mainWindow.webContents.send('broadcast', msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { ConfigController } from '../controllers/configController';
|
||||
import { PlatformConfigController } from '../controllers/platformConfigController';
|
||||
import { Config } from '../entities/config';
|
||||
|
||||
export class ConfigService {
|
||||
private configController: ConfigController;
|
||||
|
||||
private platformConfigController: PlatformConfigController;
|
||||
|
||||
constructor(
|
||||
configController: ConfigController,
|
||||
platformConfigController: PlatformConfigController,
|
||||
) {
|
||||
this.configController = configController;
|
||||
this.platformConfigController = platformConfigController;
|
||||
}
|
||||
|
||||
public registerHandlers(socket: socketIo.Socket): void {
|
||||
socket.on(
|
||||
'configService-getConfigByPlatformId',
|
||||
async (data: any, callback) => {
|
||||
const { platformId } = data;
|
||||
try {
|
||||
const config = await this.getConfigByPlatformId(platformId);
|
||||
callback(config);
|
||||
} catch (error) {
|
||||
console.error('Failed to get config', error);
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on('configService-getConfig', async (arg, callback) => {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
callback(config);
|
||||
} catch (error) {
|
||||
console.error('Failed to get config', error);
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getConfigByPlatformId(platformId: string): Promise<Config> {
|
||||
const platformConfig =
|
||||
await this.platformConfigController.getByPlatformId(platformId);
|
||||
const config = await this.configController.getConfig();
|
||||
return { ...config, ...platformConfig };
|
||||
}
|
||||
|
||||
public async getConfig(): Promise<Config> {
|
||||
return this.configController.getConfig();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
import fs from 'fs/promises';
|
||||
import { DateTime } from 'luxon';
|
||||
import OpenAI from 'openai';
|
||||
import socketIo from 'socket.io';
|
||||
import axios from 'axios';
|
||||
import { ConfigController } from '../controllers/configController';
|
||||
import { MessageController } from '../controllers/messageController';
|
||||
import { AutoReplyController } from '../controllers/autoReplyController';
|
||||
import { MessageDTO, ReplyDTO } from '../types';
|
||||
import { Session } from '../entities/session';
|
||||
import { Config } from '../entities/config';
|
||||
import { AutoReply } from '../entities/autoReply';
|
||||
import { TimeoutError, HumanTaskError } from '../errors/errors';
|
||||
|
||||
const errorMessages = '抱歉,消息有点多,我稍后再回复您。';
|
||||
|
||||
export class MessageService {
|
||||
private isKeywordMatch: boolean;
|
||||
|
||||
private baseUrl: string;
|
||||
|
||||
private apiKey: string;
|
||||
|
||||
private openaiClient: OpenAI;
|
||||
|
||||
private configController: ConfigController;
|
||||
|
||||
private messageController: MessageController;
|
||||
|
||||
private autoReplyController: AutoReplyController;
|
||||
|
||||
constructor(
|
||||
configController: ConfigController,
|
||||
messageController: MessageController,
|
||||
autoReplyController: AutoReplyController,
|
||||
) {
|
||||
this.isKeywordMatch = true;
|
||||
this.baseUrl = '';
|
||||
this.apiKey = '';
|
||||
this.configController = configController;
|
||||
this.messageController = messageController;
|
||||
this.autoReplyController = autoReplyController;
|
||||
}
|
||||
|
||||
// 提供一个方法用于注册事件处理器
|
||||
public registerHandlers(socket: socketIo.Socket): void {
|
||||
socket.on('messageService-getMessages', async (data, callback) => {
|
||||
const { sess, msgs } = data;
|
||||
try {
|
||||
const session = {
|
||||
id: sess.id,
|
||||
username: sess.uname,
|
||||
platform_id: sess.pid,
|
||||
platform: sess.plat,
|
||||
last_active: sess.last,
|
||||
};
|
||||
|
||||
const messages = msgs.map((msg: any) => ({
|
||||
session_id: msg.sid || session.id, // 处理默认值
|
||||
platform_id: msg.pid || session.platform_id,
|
||||
unique: msg.uniq,
|
||||
content: msg.cnt,
|
||||
role: msg.role,
|
||||
msg_type: msg.type || 'text', // 处理默认值
|
||||
}));
|
||||
|
||||
// @ts-ignore
|
||||
const reply = await this.getMessages(session, messages);
|
||||
callback({
|
||||
msg_type: reply.msg_type,
|
||||
content: reply.content,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error in getMessages: ${error}`);
|
||||
callback({ msg_type: 'text', content: errorMessages });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('messageService-checkMessage', async (data, callback) => {
|
||||
const { unique, sessionId } = data;
|
||||
try {
|
||||
const exists = await this.checkMessage(unique, sessionId);
|
||||
callback(exists);
|
||||
} catch (error) {
|
||||
console.error(`Error in checkMessage: ${error}`);
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async checkApiHealth(data: {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
useDify: boolean;
|
||||
}): Promise<{
|
||||
status: boolean;
|
||||
message: string;
|
||||
}> {
|
||||
if (!data.baseUrl || !data.apiKey) {
|
||||
return {
|
||||
status: false,
|
||||
message: '请输入正确的服务地址和 API Key',
|
||||
};
|
||||
}
|
||||
|
||||
if (data.useDify) {
|
||||
try {
|
||||
const reply = await this.difyRequest({
|
||||
gpt_base_url: data.baseUrl,
|
||||
gpt_key: data.apiKey,
|
||||
query: 'Hello',
|
||||
});
|
||||
|
||||
if (reply === '') {
|
||||
return {
|
||||
status: false,
|
||||
message: '请检查 Dify API Key 和服务地址是否正确',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: true,
|
||||
message: 'Dify API 正常',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error in checkApiHealth: ${error}`);
|
||||
return {
|
||||
status: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : JSON.stringify(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: data.apiKey,
|
||||
baseURL: data.baseUrl,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await client.chat.completions.create({
|
||||
model: data.model,
|
||||
max_tokens: 100,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Tell me a message',
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
temperature: 0.5,
|
||||
top_p: 1,
|
||||
});
|
||||
|
||||
if (
|
||||
!response.choices ||
|
||||
!response.choices.length ||
|
||||
!response.choices[0].message ||
|
||||
!response.choices[0].message.content
|
||||
) {
|
||||
return {
|
||||
status: false,
|
||||
message: '请检查 OpenAI API Key 和服务地址是否正确',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: true,
|
||||
message: 'OpenAI API 正常',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error in checkApiHealth: ${error}`);
|
||||
return {
|
||||
status: false,
|
||||
message: error instanceof Error ? error.message : JSON.stringify(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async checkMessage(unique: string, sessionId: number) {
|
||||
try {
|
||||
const exists = await this.messageController.checkExists(
|
||||
unique,
|
||||
sessionId,
|
||||
);
|
||||
return exists;
|
||||
} catch (e) {
|
||||
console.error(`Error in checkMessage: ${e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
updateKeywordMatch(isKeywordMatch: boolean) {
|
||||
this.isKeywordMatch = isKeywordMatch;
|
||||
}
|
||||
|
||||
async getMessages(session: Session, messages: MessageDTO[]) {
|
||||
if (!messages.length) {
|
||||
throw new Error('messages cannot be empty');
|
||||
}
|
||||
|
||||
console.log('Starting to generate message content...');
|
||||
const config = await this.configController.getConfig();
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
if (lastMessage.role === 'user') {
|
||||
await this.extractDataAsync(config, lastMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
const replyTask = this.getReplyTask(config, messages, session);
|
||||
const reply = await this.waitWithTimeout(
|
||||
replyTask,
|
||||
config.wait_humans_time * 1000,
|
||||
);
|
||||
|
||||
reply.msg_type = this.getMsgType(reply);
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, config.reply_speed * 1000),
|
||||
);
|
||||
messages.push({
|
||||
session_id: session.id,
|
||||
platform_id: session.platform_id,
|
||||
unique: DateTime.now().toFormat('yyyy-MM-dd HH:mm:ss'),
|
||||
content: reply.content,
|
||||
role: 'assistant',
|
||||
msg_type: reply.msg_type,
|
||||
});
|
||||
return reply;
|
||||
} catch (error) {
|
||||
if (error instanceof TimeoutError) {
|
||||
throw new HumanTaskError('Reply timeout, please handle manually.');
|
||||
}
|
||||
console.error(`Error in getMessages: ${error}`);
|
||||
return {
|
||||
msg_type: 'text',
|
||||
content: errorMessages,
|
||||
};
|
||||
} finally {
|
||||
await this.messageController.batchCreateMsgs(session.id, messages);
|
||||
}
|
||||
}
|
||||
|
||||
private async waitWithTimeout(
|
||||
promise: Promise<ReplyDTO>,
|
||||
time: number,
|
||||
): Promise<ReplyDTO> {
|
||||
// Create a new promise that rejects after a timeout
|
||||
let timeoutHandle;
|
||||
const timeoutPromise = new Promise<ReplyDTO>((resolve, reject) => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
reject(new TimeoutError('Operation timed out'));
|
||||
}, time);
|
||||
});
|
||||
|
||||
try {
|
||||
// Race the timeout against the original promise
|
||||
const result = await Promise.race([promise, timeoutPromise]);
|
||||
return result;
|
||||
} finally {
|
||||
// If the original promise or the timeout completes, we clear the timeout
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
}
|
||||
|
||||
private async getReplyTask(
|
||||
config: Config,
|
||||
messages: MessageDTO[],
|
||||
session: Session,
|
||||
) {
|
||||
// 提前定义好回复内容
|
||||
let replyContent = null;
|
||||
|
||||
// 使用关键词匹配尝试回复
|
||||
if (this.isKeywordMatch) {
|
||||
console.log('Attempting to use keyword matching...');
|
||||
const { content, found } = await this.matchAndReply(
|
||||
messages,
|
||||
session.platform_id,
|
||||
);
|
||||
|
||||
if (found && content) {
|
||||
console.log('Keyword match successful, using keyword to reply...');
|
||||
replyContent = content;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果关键词匹配无效或未启用关键词匹配,调用 OpenAI 生成回复
|
||||
if (!replyContent) {
|
||||
console.log(
|
||||
'Keyword matching failed or not used, using OpenAI to generate reply...',
|
||||
);
|
||||
if (config.use_dify) {
|
||||
replyContent = await this.getDifyResponse(
|
||||
config,
|
||||
messages,
|
||||
session.platform_id,
|
||||
);
|
||||
} else {
|
||||
replyContent = await this.getOpenAIResponse(
|
||||
config,
|
||||
messages,
|
||||
session.platform_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
replyContent.msg_type = this.getMsgType(replyContent);
|
||||
return replyContent;
|
||||
}
|
||||
|
||||
private async getOpenAIResponse(
|
||||
cfg: Config,
|
||||
messages: MessageDTO[],
|
||||
prompt: string = 'Tell me a message',
|
||||
maxTokens: number = 100,
|
||||
): Promise<ReplyDTO> {
|
||||
if (
|
||||
!this.openaiClient ||
|
||||
this.baseUrl !== cfg.gpt_base_url ||
|
||||
this.apiKey !== cfg.gpt_key
|
||||
) {
|
||||
this.openaiClient = new OpenAI({
|
||||
apiKey: cfg.gpt_key,
|
||||
baseURL: cfg.gpt_base_url,
|
||||
});
|
||||
}
|
||||
|
||||
const targets = messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}));
|
||||
|
||||
if (prompt !== '') {
|
||||
targets.push({ role: 'system', content: prompt });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.openaiClient.chat.completions.create({
|
||||
model: cfg.gpt_model,
|
||||
max_tokens: maxTokens,
|
||||
messages: targets,
|
||||
stream: false,
|
||||
temperature: cfg.gpt_temperature,
|
||||
top_p: cfg.gpt_top_p,
|
||||
});
|
||||
|
||||
if (
|
||||
!response.choices ||
|
||||
!response.choices.length ||
|
||||
!response.choices[0].message ||
|
||||
!response.choices[0].message.content
|
||||
) {
|
||||
console.error('Error in getOpenAIResponse: response is empty');
|
||||
|
||||
return {
|
||||
msg_type: 'text',
|
||||
content: errorMessages,
|
||||
};
|
||||
}
|
||||
|
||||
const message = response.choices[0].message.content.trim();
|
||||
return { msg_type: 'text', content: message };
|
||||
} catch (error) {
|
||||
console.error(`Error in getOpenAIResponse: ${error}`);
|
||||
return {
|
||||
msg_type: 'text',
|
||||
content: errorMessages,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async extractDataAsync(
|
||||
cfg: Config,
|
||||
message: MessageDTO,
|
||||
): Promise<void> {
|
||||
if (!cfg.extract_phone && !cfg.extract_product) return;
|
||||
if (cfg.save_path === '') return;
|
||||
|
||||
console.log('开始提取用户消息中的数据....');
|
||||
const dataExtracted: { [key: string]: string } = {};
|
||||
const fileName = `${cfg.save_path}/${new Date().toISOString().split('T')[0]}.txt`;
|
||||
|
||||
if (cfg.extract_phone) {
|
||||
const phoneNumbers = message.content.match(/\b1[3-9]\d{9}\b/g);
|
||||
if (phoneNumbers) dataExtracted.phone_numbers = phoneNumbers.join(', ');
|
||||
} else if (cfg.extract_product && message.msg_type === 'goods') {
|
||||
dataExtracted.products = message.content;
|
||||
}
|
||||
|
||||
await fs.appendFile(
|
||||
fileName,
|
||||
`${Object.entries(dataExtracted)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n')}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
private async matchAndReply(
|
||||
messages: MessageDTO[],
|
||||
platformId: string,
|
||||
): Promise<{
|
||||
content: ReplyDTO;
|
||||
found: boolean;
|
||||
}> {
|
||||
if (!messages.length) {
|
||||
return {
|
||||
content: { content: '', msg_type: 'text' },
|
||||
found: false,
|
||||
};
|
||||
}
|
||||
|
||||
const lastMessage = messages[messages.length - 1].content;
|
||||
const keywords = await this.autoReplyController.getKeywords(platformId);
|
||||
|
||||
const foundKeywordObj = keywords.find((keywordObj) => {
|
||||
return keywordObj.keyword.split('|').some((pattern) => {
|
||||
return this.simpleWildcardMatch(pattern, lastMessage);
|
||||
});
|
||||
});
|
||||
|
||||
if (foundKeywordObj) {
|
||||
return {
|
||||
content: this.chooseReply(foundKeywordObj),
|
||||
found: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: { content: '', msg_type: 'text' },
|
||||
found: false,
|
||||
};
|
||||
}
|
||||
|
||||
private simpleWildcardMatch(pattern: string, msg: string) {
|
||||
if (pattern.includes('[and]')) {
|
||||
const keywords = pattern.split('[and]');
|
||||
return keywords.every((keyword) =>
|
||||
this.matchKeyword(keyword.trim(), msg),
|
||||
);
|
||||
}
|
||||
return this.matchKeyword(pattern, msg);
|
||||
}
|
||||
|
||||
private matchKeyword(pattern: string, msg: string) {
|
||||
if (!pattern.includes('*')) {
|
||||
return pattern === msg;
|
||||
}
|
||||
|
||||
const parts = pattern.split('*');
|
||||
let lastIndex = 0;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const part of parts) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (part === '') continue;
|
||||
const index = msg.indexOf(part, lastIndex);
|
||||
if (index === -1 || index < lastIndex) return false;
|
||||
lastIndex = index + part.length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private replaceSpecialTokens(replyMsg: string) {
|
||||
let reply = replyMsg;
|
||||
const randomChoices = [
|
||||
...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
'🌸',
|
||||
'😊',
|
||||
'🌷',
|
||||
'🌹',
|
||||
'💖',
|
||||
'🪷',
|
||||
'💐',
|
||||
'🌺',
|
||||
'🌼',
|
||||
'🌻',
|
||||
];
|
||||
while (reply.includes('[~]')) {
|
||||
const randomChoice =
|
||||
randomChoices[Math.floor(Math.random() * randomChoices.length)];
|
||||
reply = reply.replace('[~]', randomChoice);
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
private chooseReply(keywordObj: AutoReply): ReplyDTO {
|
||||
const replies = keywordObj.reply.split('[or]');
|
||||
let chosenReply = this.replaceSpecialTokens(
|
||||
replies[Math.floor(Math.random() * replies.length)],
|
||||
);
|
||||
|
||||
let msgType = 'text';
|
||||
if (chosenReply.includes('[@]') && chosenReply.includes('[/@]')) {
|
||||
msgType = 'file';
|
||||
const fileStart = chosenReply.indexOf('[@]') + 3;
|
||||
const fileEnd = chosenReply.indexOf('[/@]');
|
||||
const filePath = chosenReply.substring(fileStart, fileEnd);
|
||||
chosenReply = filePath;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return { content: chosenReply, msg_type: msgType };
|
||||
}
|
||||
|
||||
private getMsgType(reply: ReplyDTO) {
|
||||
if (reply.msg_type === 'text') {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
const { content } = reply;
|
||||
if (
|
||||
content.endsWith('.jpg') ||
|
||||
content.endsWith('.png') ||
|
||||
content.endsWith('.gif') ||
|
||||
content.endsWith('.jpeg') ||
|
||||
content.endsWith('.webp')
|
||||
) {
|
||||
return 'image';
|
||||
}
|
||||
if (
|
||||
content.endsWith('.mp4') ||
|
||||
content.endsWith('.mov') ||
|
||||
content.endsWith('.avi') ||
|
||||
content.endsWith('.flv')
|
||||
) {
|
||||
return 'video';
|
||||
}
|
||||
return 'file';
|
||||
}
|
||||
|
||||
// https://docs.dify.ai/v/zh-hans/guides/application-publishing/developing-with-apis
|
||||
// https://github.com/fatwang2/dify2openai
|
||||
private async getDifyResponse(
|
||||
cfg: {
|
||||
gpt_base_url: string;
|
||||
gpt_key: string;
|
||||
},
|
||||
messages: MessageDTO[],
|
||||
prompt: string = 'Tell me a message',
|
||||
): Promise<ReplyDTO> {
|
||||
const targets = messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}));
|
||||
|
||||
if (prompt !== '') {
|
||||
targets.push({ role: 'system', content: prompt });
|
||||
}
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const queryString = `here is our talk history:\n'''\n${messages
|
||||
.slice(0, -1)
|
||||
.map((message) => `${message.role}: ${message.content}`)
|
||||
.join('\n')}\n'''\n\nhere is my question:\n${lastMessage.content}`;
|
||||
|
||||
const answer = await this.difyRequest({
|
||||
query: queryString,
|
||||
gpt_base_url: cfg.gpt_base_url,
|
||||
gpt_key: cfg.gpt_key,
|
||||
});
|
||||
|
||||
if (answer === '') {
|
||||
return {
|
||||
msg_type: 'text',
|
||||
content: errorMessages,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
msg_type: 'text',
|
||||
content: answer,
|
||||
};
|
||||
}
|
||||
|
||||
private async difyRequest(data: {
|
||||
query: string;
|
||||
gpt_base_url: string;
|
||||
gpt_key: string;
|
||||
}) {
|
||||
const resp = await axios.post(
|
||||
`${data.gpt_base_url}/chat-messages`,
|
||||
{
|
||||
inputs: {},
|
||||
query: data.query,
|
||||
response_mode: 'blocking',
|
||||
user: 'apiuser',
|
||||
auto_generate_name: false,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${data.gpt_key}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { answer } = resp.data;
|
||||
return answer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { SessionController } from '../controllers/sessionController';
|
||||
import { Session } from '../entities/session';
|
||||
import { StrategyService } from './strategyService';
|
||||
|
||||
export class SessionService {
|
||||
private sessionController: SessionController;
|
||||
|
||||
private strategyService: StrategyService;
|
||||
|
||||
constructor(
|
||||
sessionController: SessionController,
|
||||
strategyService: StrategyService,
|
||||
) {
|
||||
this.sessionController = sessionController;
|
||||
this.strategyService = strategyService;
|
||||
}
|
||||
|
||||
public registerHandlers(socket: socketIo.Socket): void {
|
||||
socket.on('sessionService-getSession', async (data: any, callback) => {
|
||||
const { platformId, username, goodsName, goodsAvatar } = data;
|
||||
|
||||
const ptf = this.strategyService.getPlatformInfoById(platformId);
|
||||
const startTime = new Date().getTime();
|
||||
try {
|
||||
if (!username) {
|
||||
// 直接新建一个 session
|
||||
const session = await this.createSession(
|
||||
platformId,
|
||||
ptf?.name || '',
|
||||
goodsName,
|
||||
goodsAvatar,
|
||||
);
|
||||
|
||||
callback(session);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await this.getSessionOrCreate(
|
||||
platformId,
|
||||
username,
|
||||
goodsName,
|
||||
goodsAvatar,
|
||||
);
|
||||
callback(session);
|
||||
} catch (error) {
|
||||
console.error('Failed to get session', error);
|
||||
callback(null);
|
||||
} finally {
|
||||
console.log('getSession time:', new Date().getTime() - startTime, 'ms');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async createSession(
|
||||
platformId: string,
|
||||
platformName: string,
|
||||
goodsName?: string,
|
||||
goodsAvatar?: string,
|
||||
): Promise<Session> {
|
||||
return this.sessionController.create({
|
||||
platform_id: platformId,
|
||||
platform: platformName,
|
||||
username: '',
|
||||
last_active: new Date(),
|
||||
goods_name: goodsName || '',
|
||||
goods_avatar: goodsAvatar || '',
|
||||
created_at: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
public async getSessionOrCreate(
|
||||
platformId: string,
|
||||
username: string,
|
||||
goodsName?: string,
|
||||
goodsAvatar?: string,
|
||||
): Promise<Session> {
|
||||
const sessions = await this.sessionController.search(platformId, username);
|
||||
if (sessions.length > 0) {
|
||||
const session = sessions[0];
|
||||
session.last_active = new Date();
|
||||
if (goodsName && goodsAvatar) {
|
||||
session.goods_name = goodsName;
|
||||
session.goods_avatar = goodsAvatar;
|
||||
}
|
||||
return this.sessionController.update(session.id, session);
|
||||
}
|
||||
|
||||
const ptf = this.strategyService.getPlatformInfoById(platformId);
|
||||
|
||||
return this.sessionController.create({
|
||||
platform_id: platformId,
|
||||
platform: ptf?.name || '',
|
||||
username,
|
||||
last_active: new Date(),
|
||||
goods_name: goodsName || '',
|
||||
goods_avatar: goodsAvatar || '',
|
||||
created_at: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { StrategyServiceStatusEnum } from '../constants/constants';
|
||||
import { Platform } from '../types';
|
||||
|
||||
export class StrategyService {
|
||||
private io: socketIo.Server;
|
||||
|
||||
constructor(io: socketIo.Server) {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
async emitAndWait<T>(
|
||||
event: string,
|
||||
data?: any,
|
||||
timeout: number = 5000,
|
||||
): Promise<T> {
|
||||
let response;
|
||||
if (data === undefined) {
|
||||
response = await this.io.timeout(timeout).emitWithAck(event);
|
||||
} else {
|
||||
response = await this.io.timeout(timeout).emitWithAck(event, data);
|
||||
}
|
||||
|
||||
// 判断是否是 Array
|
||||
if (Array.isArray(response) && response.length === 1) {
|
||||
if (typeof response[0] === 'undefined') {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
if (response[0] === 'string') {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return JSON.parse(response[0]);
|
||||
}
|
||||
|
||||
// 判断是否是字符串
|
||||
if (typeof response === 'string') {
|
||||
return JSON.parse(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async updateStrategies(ids: string[]): Promise<any> {
|
||||
try {
|
||||
return await this.emitAndWait('strategyService-updateStrategies', {
|
||||
ids,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update strategies', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async updateStatus(status: StrategyServiceStatusEnum): Promise<any> {
|
||||
try {
|
||||
return await this.emitAndWait('strategyService-updateStatus', { status });
|
||||
} catch (error) {
|
||||
console.error('Failed to update status', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public getPlatformInfoById(id: string): Platform | null {
|
||||
const platforms = this.getAllPlatforms();
|
||||
return platforms.find((platform) => platform.id === id) || null;
|
||||
}
|
||||
|
||||
public getAllPlatforms(): Platform[] {
|
||||
return [
|
||||
{
|
||||
id: 'sannysoft',
|
||||
type: 'OTHER',
|
||||
name: 'Sannysoft',
|
||||
avatar: '',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'bilibili',
|
||||
type: 'HOT',
|
||||
name: '哔哩哔哩',
|
||||
avatar: 'https://bilibili.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'zhihu',
|
||||
type: 'ME_MEDIA',
|
||||
name: '知乎',
|
||||
avatar: 'https://www.zhihu.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'test',
|
||||
type: 'OTHER',
|
||||
name: 'Test',
|
||||
avatar: '',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'jinritemai',
|
||||
type: 'E_COMMERCE',
|
||||
name: '抖店',
|
||||
avatar:
|
||||
'https://lf3-static.bytednsdoc.com/obj/eden-cn/yvahlyj_upfbvk_zlp/ljhwZthlaukjlkulzlp/pc_creator/favicon_v2_7145ff0.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'douyin',
|
||||
type: 'ME_MEDIA',
|
||||
name: '抖音',
|
||||
avatar:
|
||||
'https://lf3-static.bytednsdoc.com/obj/eden-cn/yvahlyj_upfbvk_zlp/ljhwZthlaukjlkulzlp/pc_creator/favicon_v2_7145ff0.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'baijiahao',
|
||||
type: 'ME_MEDIA',
|
||||
name: '百家号',
|
||||
avatar: 'https://baijiahao.baidu.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'buyin_jinritemai',
|
||||
type: 'ME_MEDIA',
|
||||
name: '百应巨量',
|
||||
avatar: 'https://buyin.jinritemai.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'douyin_mp',
|
||||
type: 'ME_MEDIA',
|
||||
name: '抖音企业号',
|
||||
avatar: 'https://buyin.jinritemai.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'ctrip_imvendor',
|
||||
type: 'HOT',
|
||||
name: '携程供应商',
|
||||
avatar: 'https://ctrip.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'dianping',
|
||||
type: 'E_COMMERCE',
|
||||
name: '美团开店宝',
|
||||
avatar:
|
||||
'https://awp-assets.meituan.net/nibfe/epassport-html/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'findlaw',
|
||||
type: 'LAW',
|
||||
name: '找法网',
|
||||
avatar: 'https://findlaw.cn/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'lawtime',
|
||||
type: 'LAW',
|
||||
name: '法律快车',
|
||||
avatar: 'https://lawtime.cn/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'lvlin',
|
||||
type: 'LAW',
|
||||
name: '律临',
|
||||
avatar: 'https://lvlin-base.cdn.bcebos.com/sp-saas/common/ae_logo.png',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'meituan_mtm',
|
||||
type: 'E_COMMERCE',
|
||||
name: '美团电商',
|
||||
avatar:
|
||||
'https://p0.meituan.net/publiconline/62580fcd548dba6697eb64f2769cc9b21804.png',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'meituan_my',
|
||||
type: 'E_COMMERCE',
|
||||
name: '美团买药',
|
||||
avatar: 'https://epassport.meituan.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'meituan',
|
||||
type: 'ME_MEDIA',
|
||||
name: '美团直播助手',
|
||||
avatar: 'https://live.meituan.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'oceanengine',
|
||||
type: 'E_COMMERCE',
|
||||
name: '小6客服',
|
||||
avatar:
|
||||
'https://sf1-cdn-tos.toutiaostatic.com/obj/ttfe/cg/fe/im/im.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'pinduoduo',
|
||||
type: 'E_COMMERCE',
|
||||
name: '拼多多',
|
||||
avatar: 'https://mms.pinduoduo.com/login/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'toutiao',
|
||||
type: 'ME_MEDIA',
|
||||
name: '头条号',
|
||||
avatar: 'https://sf3-cdn-tos.toutiaostatic.com/obj/mp/logo-toutiao.png',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: '51job',
|
||||
type: 'RECRUIT',
|
||||
name: '前程无忧',
|
||||
avatar: 'https://ehire.51job.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: '58',
|
||||
type: 'RECRUIT',
|
||||
name: '58同城',
|
||||
avatar: 'https://passport.58.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: '66law',
|
||||
type: 'LAW',
|
||||
name: '华律',
|
||||
avatar: 'https://login.66law.cn/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: '1688',
|
||||
type: 'E_COMMERCE',
|
||||
name: '1688 找工厂',
|
||||
avatar:
|
||||
'https://img.alicdn.com/tfs/TB1uh..zbj1gK0jSZFuXXcrHpXa-16-16.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: '64365',
|
||||
type: 'LAW',
|
||||
name: '律图律师',
|
||||
avatar: 'https://login.64365.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'weibo',
|
||||
type: 'HOT',
|
||||
name: '微博私信',
|
||||
avatar: 'https://conchfairy.sinajs.cn/chat/favicon.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'whatsapp',
|
||||
type: 'HOT',
|
||||
name: 'Whatsapp',
|
||||
avatar: 'https://web.whatsapp.com/img/favicon/1x/favicon.png',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'weixin_channels',
|
||||
type: 'ME_MEDIA',
|
||||
name: '微信视频号',
|
||||
avatar:
|
||||
'https://res.wx.qq.com/t/wx_fed/finder/helper/finder-helper-web/res/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'xiaohongshu_pro',
|
||||
type: 'ME_MEDIA',
|
||||
name: '小红书私信',
|
||||
avatar: 'https://www.xiaohongshu.com/favicon.ico',
|
||||
desc: '\n### 小红书平台支持\n\n发送关联链接(笔记、商品),可以使用以下规则编写回复:\n\n```\n【g-数字】:关联商品\n【n-数字】:关联笔记\n\n这个数字是商品或者笔记所在页面的排序\n```\n\n例如:【g-1】是指第一条关联商品,【n-2】是第二条关联笔记,\n\n如果要连发两条后再混着一条文案回复可以这样写: `【g-1】|【n-2】|你好`,这样就会先发送 【g-1】商品,然后发送【n-2】商品,最后发送“你好”。\n',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'xiaohongshu',
|
||||
type: 'ME_MEDIA',
|
||||
name: '小红书评论',
|
||||
avatar: 'https://www.xiaohongshu.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'zhuxiaobang',
|
||||
type: 'HOT',
|
||||
name: '住小帮',
|
||||
avatar:
|
||||
'https://lf3-static.bytednsdoc.com/obj/eden-cn/jpahaz_lm_kltvo/ljhwZthlaukjlkulzlp/homed_merchant_admin/favicon.ico',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'nowsecure',
|
||||
type: 'OTHER',
|
||||
name: 'nowsecure',
|
||||
avatar: '',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'antoinevastel',
|
||||
type: 'OTHER',
|
||||
name: 'antoinevastel',
|
||||
avatar: '',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'fingerprintjs',
|
||||
type: 'OTHER',
|
||||
name: 'fingerprintjs',
|
||||
avatar: '',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'incolumitas',
|
||||
type: 'OTHER',
|
||||
name: 'incolumitas',
|
||||
avatar: '',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'recaptcha',
|
||||
type: 'OTHER',
|
||||
name: 'recaptcha',
|
||||
avatar: '',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'win_wechat',
|
||||
type: 'HOT',
|
||||
name: '微信',
|
||||
avatar: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'win_wecom',
|
||||
type: 'HOT',
|
||||
name: '企业微信(Bate版)',
|
||||
avatar:
|
||||
'https://wwcdn.weixin.qq.com/node/wwnl/wwnl/style/images/independent/favicon/favicon_32h$a41a4426.png',
|
||||
desc: '',
|
||||
impl: false,
|
||||
},
|
||||
{
|
||||
id: 'win_qianniu',
|
||||
type: 'E_COMMERCE',
|
||||
name: '千牛',
|
||||
avatar: 'https://qianniu.1688.com/favicon.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
{
|
||||
id: 'win_jinmai',
|
||||
type: 'E_COMMERCE',
|
||||
name: '京卖',
|
||||
avatar: 'https://shop.jd.com/jdm/favicon.ico',
|
||||
desc: '',
|
||||
impl: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import socketIo from 'socket.io';
|
||||
|
||||
export class SystemService {
|
||||
private io: socketIo.Server;
|
||||
|
||||
constructor(io: socketIo.Server) {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
return await this.io.timeout(5000).emitWithAck('systemService-health');
|
||||
} catch (error) {
|
||||
console.error('Failed to check health', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export type RoleType = 'assistant' | 'user' | 'system';
|
||||
export type MessageType =
|
||||
| 'text'
|
||||
| 'image'
|
||||
| 'video'
|
||||
| 'file'
|
||||
| 'mention'
|
||||
| 'goods';
|
||||
|
||||
export interface MessageDTO {
|
||||
session_id: number;
|
||||
platform_id: string;
|
||||
unique: string;
|
||||
content: string; // If it's an image, this is the URL of the image
|
||||
role: RoleType; // assistant, user
|
||||
msg_type: MessageType;
|
||||
}
|
||||
|
||||
export interface ReplyDTO {
|
||||
content: string;
|
||||
msg_type: MessageType;
|
||||
}
|
||||
|
||||
export interface Platform {
|
||||
id: string;
|
||||
name: string;
|
||||
impl: boolean;
|
||||
type?: string;
|
||||
urls?: string[];
|
||||
avatar?: string;
|
||||
desc?: string;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Notification,
|
||||
} from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { setCron } from './system/cron';
|
||||
import type BackendServiceManager from './system/backend';
|
||||
import { getBrowserVersionFromOS } from './system/chrome';
|
||||
@@ -48,6 +50,12 @@ const setupIpcHandlers = (
|
||||
shell.openPath(args);
|
||||
});
|
||||
|
||||
ipcMain.on('open-logger-folder', async () => {
|
||||
const logDir = path.join(os.tmpdir(), 'chatgpt-on-cs');
|
||||
|
||||
shell.openPath(logDir);
|
||||
});
|
||||
|
||||
ipcMain.on('electron-store-get', async (event, val) => {
|
||||
event.returnValue = store.get(val);
|
||||
});
|
||||
@@ -93,7 +101,6 @@ const setupIpcHandlers = (
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Checking health...');
|
||||
const isHealthy = await bsm.check_health();
|
||||
mainWindow.webContents.send('check-health', isHealthy);
|
||||
});
|
||||
|
||||
+42
-19
@@ -8,25 +8,51 @@
|
||||
* When running `npm run build` or `npm run build:main`, this file is compiled to
|
||||
* `./src/main.js` using webpack. This gives us some performance wins.
|
||||
*/
|
||||
import 'source-map-support/register';
|
||||
import './system/logger';
|
||||
import path from 'path';
|
||||
import { app, BrowserWindow, shell } from 'electron';
|
||||
import MenuBuilder from './menu';
|
||||
import { resolveHtmlPath } from './util';
|
||||
import setupIpcHandlers from './ipcHandlers';
|
||||
import BackendServiceManager from './system/backend';
|
||||
import Server from './backend/backend';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let backendServiceManager: BackendServiceManager | null = null;
|
||||
|
||||
/**
|
||||
* Add event listeners...
|
||||
*/
|
||||
|
||||
app.on('window-all-closed', async () => {
|
||||
// Respect the OSX convention of having the application in memory even
|
||||
// after all windows have been closed
|
||||
console.log('window-all-closed');
|
||||
await backendServiceManager?.stop();
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', async () => {
|
||||
console.log('before-quit');
|
||||
await backendServiceManager?.stop();
|
||||
});
|
||||
|
||||
const originalUncaughtException = process.listeners('uncaughtException').pop();
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.on('uncaughtException', async (error, origin) => {
|
||||
console.error('An error occurred in the main process:', error);
|
||||
console.error(error.stack);
|
||||
await backendServiceManager?.stop();
|
||||
originalUncaughtException?.(error, origin);
|
||||
});
|
||||
|
||||
app.commandLine.appendSwitch('lang', 'zh-CN');
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const sourceMapSupport = require('source-map-support');
|
||||
sourceMapSupport.install();
|
||||
}
|
||||
|
||||
const isDebug =
|
||||
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
|
||||
|
||||
@@ -62,6 +88,7 @@ const createWindow = async () => {
|
||||
process.env.BKEXE_PATH || './backend/__main__.exe',
|
||||
),
|
||||
);
|
||||
|
||||
await backendServiceManager.start();
|
||||
|
||||
const getAssetPath = (...paths: string[]): string => {
|
||||
@@ -88,6 +115,16 @@ const createWindow = async () => {
|
||||
}
|
||||
|
||||
setupIpcHandlers(mainWindow, backendServiceManager);
|
||||
const server = new Server(backendServiceManager.getPort(), mainWindow);
|
||||
// 启动服务器
|
||||
server
|
||||
.start()
|
||||
.then(() => {
|
||||
console.log('Server started successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error starting server:', err);
|
||||
});
|
||||
|
||||
mainWindow.loadURL(resolveHtmlPath('index.html'));
|
||||
|
||||
@@ -116,20 +153,6 @@ const createWindow = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add event listeners...
|
||||
*/
|
||||
|
||||
app.on('window-all-closed', async () => {
|
||||
// Respect the OSX convention of having the application in memory even
|
||||
// after all windows have been closed
|
||||
console.log('window-all-closed');
|
||||
await backendServiceManager?.stop();
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ export type Channels =
|
||||
| 'electron-store-remove'
|
||||
| 'refresh-config'
|
||||
| 'open-directory'
|
||||
| 'open-logger-folder'
|
||||
| 'select-file'
|
||||
| 'selected-file'
|
||||
| 'select-directory'
|
||||
@@ -19,6 +20,7 @@ export type Channels =
|
||||
| 'open-url'
|
||||
| 'notification'
|
||||
| 'get-browser-version'
|
||||
| 'broadcast'
|
||||
| 'get-version';
|
||||
|
||||
const electronHandler = {
|
||||
|
||||
+53
-14
@@ -10,10 +10,18 @@ class BackendServiceManager {
|
||||
|
||||
private process: ReturnType<typeof spawn> | null;
|
||||
|
||||
private logStream: fs.WriteStream;
|
||||
|
||||
private logFilePath: string;
|
||||
|
||||
private port: number;
|
||||
|
||||
private autoRestart: boolean; // 新增自动重启标志
|
||||
|
||||
private MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
private MAX_LOG_FILES = 10;
|
||||
|
||||
constructor(executablePath: string, autoRestart: boolean = true) {
|
||||
this.executablePath = executablePath;
|
||||
this.process = null;
|
||||
@@ -42,36 +50,67 @@ class BackendServiceManager {
|
||||
});
|
||||
}
|
||||
|
||||
private rotateLogs() {
|
||||
this.logStream.close();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const newLogFilePath = this.logFilePath.replace(
|
||||
'process.log',
|
||||
`process-${timestamp}.log`,
|
||||
);
|
||||
fs.renameSync(this.logFilePath, newLogFilePath);
|
||||
|
||||
const logFiles = fs.readdirSync(path.dirname(this.logFilePath)).sort();
|
||||
while (logFiles.length > this.MAX_LOG_FILES) {
|
||||
const oldestLogFile = logFiles.shift();
|
||||
if (!oldestLogFile) {
|
||||
break;
|
||||
}
|
||||
fs.unlinkSync(path.join(path.dirname(this.logFilePath), oldestLogFile));
|
||||
}
|
||||
|
||||
this.logFilePath = path.join(path.dirname(this.logFilePath), 'process.log');
|
||||
this.logStream = fs.createWriteStream(this.logFilePath, { flags: 'a' });
|
||||
}
|
||||
|
||||
private checkLogSizeAndRotate() {
|
||||
const logSize = fs.existsSync(this.logFilePath)
|
||||
? fs.statSync(this.logFilePath).size
|
||||
: 0;
|
||||
if (logSize > this.MAX_LOG_SIZE) {
|
||||
this.rotateLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private launchProcess(port: number) {
|
||||
// 获取系统的临时文件夹路径
|
||||
const tempDir = os.tmpdir();
|
||||
// 在临时文件夹中创建一个名为 chatgpt-on-cs 的目录用于存放日志
|
||||
const logDir = path.join(tempDir, 'chatgpt-on-cs');
|
||||
// 如果目录不存在,则创建它
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir);
|
||||
}
|
||||
// 定义日志文件的路径
|
||||
const logFilePath = path.join(logDir, 'process.log');
|
||||
// 创建一个写入流
|
||||
const logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
|
||||
this.logFilePath = path.join(logDir, 'process.log');
|
||||
this.logStream = fs.createWriteStream(this.logFilePath, { flags: 'a' });
|
||||
|
||||
this.process = spawn(this.executablePath, ['--port', port.toString()]);
|
||||
|
||||
this.process.stdout?.on('data', (data) => {
|
||||
console.log(`stdout: ${data}`); // 可选:依然在控制台输出
|
||||
logStream.write(`stdout: ${data}`); // 写入文件
|
||||
this.logStream.write(`stdout: ${data}`);
|
||||
this.checkLogSizeAndRotate();
|
||||
});
|
||||
|
||||
this.process.stderr?.on('data', (data) => {
|
||||
console.error(`stderr: ${data}`); // 可选:依然在控制台输出
|
||||
logStream.write(`stderr: ${data}`); // 写入文件
|
||||
this.logStream.write(`stderr: ${data}`);
|
||||
this.checkLogSizeAndRotate();
|
||||
});
|
||||
|
||||
// 监听进程关闭事件,关闭写入流
|
||||
this.process.on('close', () => {
|
||||
logStream.close();
|
||||
this.logStream.close();
|
||||
});
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
console.error('Failed to start subprocess.', err);
|
||||
});
|
||||
|
||||
console.log(`Backend process started with PID: ${this.process.pid}`);
|
||||
}
|
||||
|
||||
async getAvailablePort(): Promise<number> {
|
||||
@@ -94,7 +133,7 @@ class BackendServiceManager {
|
||||
const {
|
||||
data: { data },
|
||||
} = await axios.get(`http://127.0.0.1:${this.port}/api/v1/base/health`);
|
||||
return data.init_db;
|
||||
return data;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const MAX_LOG_FILES = 10;
|
||||
|
||||
// 获取系统的临时文件夹路径
|
||||
const tempDir = os.tmpdir();
|
||||
// 在临时文件夹中创建一个名为 chatgpt-on-cs 的目录用于存放日志
|
||||
const logDir = path.join(tempDir, 'chatgpt-on-cs');
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir);
|
||||
}
|
||||
|
||||
// 定义日志文件的路径
|
||||
let logFilePath = path.join(logDir, 'renderer.log');
|
||||
|
||||
function rotateLogs() {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const newLogFilePath = path.join(logDir, `renderer-${timestamp}.log`);
|
||||
fs.renameSync(logFilePath, newLogFilePath);
|
||||
|
||||
const logFiles = fs.readdirSync(logDir).sort();
|
||||
while (logFiles.length > MAX_LOG_FILES) {
|
||||
const oldestLogFile = logFiles.shift();
|
||||
|
||||
// 如果没有最旧的日志文件,则退出循环
|
||||
if (!oldestLogFile) {
|
||||
break;
|
||||
}
|
||||
|
||||
fs.unlinkSync(path.join(logDir, oldestLogFile));
|
||||
}
|
||||
|
||||
logFilePath = path.join(logDir, 'renderer.log');
|
||||
}
|
||||
|
||||
function logToFile(message: string) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logMessage = `[${timestamp}] ${message}\n`;
|
||||
|
||||
const logSize = fs.existsSync(logFilePath)
|
||||
? fs.statSync(logFilePath).size
|
||||
: 0;
|
||||
if (logSize + logMessage.length > MAX_LOG_SIZE) {
|
||||
rotateLogs();
|
||||
}
|
||||
|
||||
fs.appendFileSync(logFilePath, logMessage);
|
||||
}
|
||||
|
||||
const isDebug =
|
||||
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
console.log = (...args) => {
|
||||
originalConsoleLog(...args);
|
||||
// if (!isDebug) return;
|
||||
|
||||
logToFile(
|
||||
`INFO: ${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' ')}`,
|
||||
);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
originalConsoleError(...args);
|
||||
logToFile(
|
||||
`ERROR: ${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' ')}`,
|
||||
);
|
||||
};
|
||||
|
||||
console.warn = (...args) => {
|
||||
originalConsoleWarn(...args);
|
||||
logToFile(
|
||||
`WARN: ${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' ')}`,
|
||||
);
|
||||
};
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('An error occurred in the main process:', error);
|
||||
console.error(error.stack);
|
||||
});
|
||||
@@ -14,7 +14,7 @@ import PlatformSettings from './pages/Platforms';
|
||||
import { SettingsProvider } from './pages/Settings/SettingsContext';
|
||||
import Updater from './components/Updater';
|
||||
import SystemCheck from './components/SystemCheck';
|
||||
import { WebSocketProvider } from './hooks/useWebSocketContext';
|
||||
import { BroadcastProvider } from './hooks/useBroadcastContext';
|
||||
import './App.css';
|
||||
import theme from './ui/styles/theme';
|
||||
|
||||
@@ -46,7 +46,7 @@ function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChakraProvider theme={theme}>
|
||||
<WebSocketProvider>
|
||||
<BroadcastProvider>
|
||||
<ErrorBoundary>
|
||||
<Router>
|
||||
{isLoaded ? (
|
||||
@@ -77,7 +77,7 @@ function App() {
|
||||
<Updater />
|
||||
</Router>
|
||||
</ErrorBoundary>
|
||||
</WebSocketProvider>
|
||||
</BroadcastProvider>
|
||||
</ChakraProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
AlertDialogOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useWebSocketContext } from '../../hooks/useWebSocketContext';
|
||||
import { useWebSocketContext } from '../../hooks/useBroadcastContext';
|
||||
import { useSystemStore } from '../../stores/useSystemStore';
|
||||
|
||||
const SystemCheck = () => {
|
||||
@@ -26,13 +26,12 @@ const SystemCheck = () => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const cancelRef = React.useRef<any>();
|
||||
const { registerEventHandler, acknowledgeMessage } = useWebSocketContext();
|
||||
const { registerEventHandler } = useWebSocketContext();
|
||||
const { setDriverSettings, driverSettings } = useSystemStore();
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = registerEventHandler((message) => {
|
||||
if (message.message === 'chrome_download') {
|
||||
acknowledgeMessage('chrome_download', message.event_id);
|
||||
setIsModalOpen(true);
|
||||
} else if (message.message === 'human_task') {
|
||||
if (!message.data) {
|
||||
@@ -56,7 +55,6 @@ const SystemCheck = () => {
|
||||
type: string;
|
||||
};
|
||||
|
||||
acknowledgeMessage('human_task', message.event_id);
|
||||
if (data.type === 'strategy') {
|
||||
setHumanTaskMsg(data.message);
|
||||
onOpen();
|
||||
@@ -69,7 +67,7 @@ const SystemCheck = () => {
|
||||
|
||||
// 组件卸载时注销事件处理器
|
||||
return () => unregister();
|
||||
}, [registerEventHandler, acknowledgeMessage]); // eslint-disable-line
|
||||
}, [registerEventHandler]); // eslint-disable-line
|
||||
|
||||
const confirmDownload = () => {
|
||||
window.electron.ipcRenderer.sendMessage(
|
||||
|
||||
@@ -10,73 +10,71 @@ import {
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { getVersionInfo } from '../../services/system/controller';
|
||||
import Markdown from '../Markdown';
|
||||
|
||||
const Updater = () => {
|
||||
const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false);
|
||||
const [updateInfo, setUpdateInfo] = useState<{
|
||||
version: string;
|
||||
url: string;
|
||||
description: string;
|
||||
} | null>(null);
|
||||
const [updates, setUpdates] = useState<
|
||||
{
|
||||
version: string;
|
||||
url: string;
|
||||
description: string;
|
||||
}[]
|
||||
>([]);
|
||||
const [currentVersion, setCurrentVersion] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const cv = window.electron.ipcRenderer.get('get-version');
|
||||
const info = await getVersionInfo(cv);
|
||||
if (info) {
|
||||
setUpdateInfo(info);
|
||||
const versionUpdates = await getVersionInfo(cv);
|
||||
if (versionUpdates.length > 0) {
|
||||
setUpdates(versionUpdates);
|
||||
setCurrentVersion(cv);
|
||||
setIsUpdateModalOpen(true);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// 确认更新则跳转到下载链接
|
||||
// 确认更新则跳转到最新版本的下载链接
|
||||
const confirmUpdate = () => {
|
||||
if (updateInfo) {
|
||||
window.electron.ipcRenderer.sendMessage('open-url', updateInfo.url);
|
||||
const latestVersion = updates[0]; // 假设第一个总是最新版本
|
||||
if (latestVersion) {
|
||||
window.electron.ipcRenderer.sendMessage('open-url', latestVersion.url);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isUpdateModalOpen}
|
||||
onClose={() => setIsUpdateModalOpen(false)}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>版本更新</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{updateInfo && (
|
||||
<>
|
||||
<Text>
|
||||
当前版本是 {currentVersion} 检查到有最新版本{' '}
|
||||
{updateInfo?.version} 请问您是否立即更新?
|
||||
</Text>
|
||||
|
||||
<Box mt="20px">
|
||||
<Markdown content={updateInfo?.description} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme="blue" mr={3} onClick={confirmUpdate}>
|
||||
立即更新
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setIsUpdateModalOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
<Modal
|
||||
isOpen={isUpdateModalOpen}
|
||||
onClose={() => setIsUpdateModalOpen(false)}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>版本更新</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text>当前版本是 {currentVersion}. 检查到以下更新:</Text>
|
||||
<VStack spacing={4} mt="20px">
|
||||
{updates.map((update, index) => (
|
||||
<Box key={index}>
|
||||
<Markdown content={update.description} />
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme="blue" mr={3} onClick={confirmUpdate}>
|
||||
立即更新到最新版本
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setIsUpdateModalOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
// 定义 Broadcast 消息类型
|
||||
interface BroadcastMessage {
|
||||
message: string;
|
||||
data: any;
|
||||
event_id: string;
|
||||
}
|
||||
|
||||
// 定义上下文类型
|
||||
interface BroadcastContextType {
|
||||
registerEventHandler: (
|
||||
handler: (message: BroadcastMessage) => void,
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
const BroadcastContext = createContext<BroadcastContextType | null>(null);
|
||||
|
||||
export const BroadcastProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [eventHandlers, setEventHandlers] = useState<
|
||||
((message: BroadcastMessage) => void)[]
|
||||
>([]);
|
||||
|
||||
// 注册事件处理器
|
||||
const registerEventHandler = useCallback(
|
||||
(handler: (message: BroadcastMessage) => void) => {
|
||||
setEventHandlers((prevHandlers) => [...prevHandlers, handler]);
|
||||
// 返回注销该处理器的函数
|
||||
return () => {
|
||||
setEventHandlers((prevHandlers) =>
|
||||
prevHandlers.filter((h) => h !== handler),
|
||||
);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
window.electron.ipcRenderer.on('broadcast', (msg) => {
|
||||
const message = msg as BroadcastMessage;
|
||||
eventHandlers.forEach((handler) => handler(message));
|
||||
});
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
registerEventHandler,
|
||||
}),
|
||||
[registerEventHandler],
|
||||
);
|
||||
|
||||
return (
|
||||
<BroadcastContext.Provider value={value}>
|
||||
{children}
|
||||
</BroadcastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to use WebSocket context
|
||||
export const useWebSocketContext = () =>
|
||||
useContext(BroadcastContext) as BroadcastContextType;
|
||||
@@ -1,95 +0,0 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import useWebSocket from 'react-use-websocket';
|
||||
|
||||
// 定义WebSocket消息类型
|
||||
interface WebSocketMessage {
|
||||
message: string;
|
||||
data: any;
|
||||
event_id: string;
|
||||
}
|
||||
|
||||
// 定义上下文类型
|
||||
interface WebSocketContextType {
|
||||
acknowledgeMessage: (event: string, eventId: string) => void;
|
||||
registerEventHandler: (
|
||||
handler: (message: WebSocketMessage) => void,
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
const WebSocketContext = createContext<WebSocketContextType | null>(null);
|
||||
|
||||
export const WebSocketProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const { lastMessage, sendMessage } = useWebSocket(
|
||||
`ws://127.0.0.1:${window.electron.getPort()}/api/v1/event/ws`,
|
||||
{
|
||||
shouldReconnect: () => !isConnected, // 仅当未连接时尝试重连
|
||||
reconnectInterval: 3000,
|
||||
onOpen: () => setIsConnected(true), // 连接成功时设置为已连接
|
||||
onClose: () => setIsConnected(false), // 连接关闭时设置为未连接
|
||||
},
|
||||
);
|
||||
const [eventHandlers, setEventHandlers] = useState<
|
||||
((message: WebSocketMessage) => void)[]
|
||||
>([]);
|
||||
|
||||
const acknowledgeMessage = useCallback(
|
||||
(event: string, eventId: string) => {
|
||||
sendMessage(
|
||||
JSON.stringify({
|
||||
type: 'ack',
|
||||
event_type: event,
|
||||
event_id: eventId,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[sendMessage],
|
||||
);
|
||||
|
||||
// 注册事件处理器
|
||||
const registerEventHandler = useCallback(
|
||||
(handler: (message: WebSocketMessage) => void) => {
|
||||
setEventHandlers((prevHandlers) => [...prevHandlers, handler]);
|
||||
// 返回注销该处理器的函数
|
||||
return () => {
|
||||
setEventHandlers((prevHandlers) =>
|
||||
prevHandlers.filter((h) => h !== handler),
|
||||
);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMessage !== null) {
|
||||
const message: WebSocketMessage = JSON.parse(lastMessage.data);
|
||||
eventHandlers.forEach((handler) => handler(message));
|
||||
}
|
||||
}, [lastMessage, eventHandlers]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
acknowledgeMessage,
|
||||
registerEventHandler,
|
||||
}),
|
||||
[acknowledgeMessage, registerEventHandler],
|
||||
);
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={value}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to use WebSocket context
|
||||
export const useWebSocketContext = () =>
|
||||
useContext(WebSocketContext) as WebSocketContextType;
|
||||
@@ -18,7 +18,8 @@ import LogBox from './LogBox';
|
||||
|
||||
const DriverSettings = () => {
|
||||
const { toast } = useToast();
|
||||
const { driverSettings, setDriverSettings } = useSystemStore();
|
||||
const { driverSettings, setDriverSettings, selectedPlatforms } =
|
||||
useSystemStore();
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('refresh-config', () => {
|
||||
@@ -27,6 +28,7 @@ const DriverSettings = () => {
|
||||
(async () => {
|
||||
try {
|
||||
await updateRunner({
|
||||
ids: selectedPlatforms,
|
||||
is_paused: isPaused,
|
||||
is_keyword_match: isKeywordMatch,
|
||||
});
|
||||
@@ -39,7 +41,8 @@ const DriverSettings = () => {
|
||||
return () => {
|
||||
window.electron.ipcRenderer.remove('refresh-config');
|
||||
};
|
||||
}, [driverSettings, toast]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [toast]);
|
||||
|
||||
const handleFormChange = (field: string) => (event: any) => {
|
||||
const { value, checked, type } = event.target;
|
||||
@@ -54,11 +57,13 @@ const DriverSettings = () => {
|
||||
if (field === 'isPaused') {
|
||||
if (checked) {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: '已经暂停自动回复功能',
|
||||
status: 'warning',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: '已经开启自动回复功能',
|
||||
status: 'success',
|
||||
});
|
||||
@@ -67,11 +72,13 @@ const DriverSettings = () => {
|
||||
updateRunner({
|
||||
is_paused: checked,
|
||||
is_keyword_match: driverSettings.isKeywordMatch,
|
||||
ids: selectedPlatforms,
|
||||
});
|
||||
} else if (field === 'isKeywordMatch') {
|
||||
updateRunner({
|
||||
is_paused: driverSettings.isPaused,
|
||||
is_keyword_match: checked,
|
||||
ids: selectedPlatforms,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,6 +181,7 @@ const EditKeyword = ({
|
||||
handleEdit();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
|
||||
@@ -8,21 +8,20 @@ import {
|
||||
Td,
|
||||
HStack,
|
||||
TableContainer,
|
||||
Text,
|
||||
Button,
|
||||
Box,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { useWebSocketContext } from '../../hooks/useWebSocketContext';
|
||||
import { useWebSocketContext } from '../../hooks/useBroadcastContext';
|
||||
import useGlobalStore from '../../stores/useGlobalStore';
|
||||
|
||||
const LogBox = () => {
|
||||
const { logs, clearLogs, addLog } = useGlobalStore();
|
||||
const { registerEventHandler, acknowledgeMessage } = useWebSocketContext();
|
||||
const { registerEventHandler } = useWebSocketContext();
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = registerEventHandler((message) => {
|
||||
if (message.message === 'log_show') {
|
||||
acknowledgeMessage('log_show', message.event_id);
|
||||
if (message.data) {
|
||||
const log = message.data as {
|
||||
time: string;
|
||||
@@ -38,39 +37,48 @@ const LogBox = () => {
|
||||
|
||||
// 组件卸载时注销事件处理器
|
||||
return () => unregister();
|
||||
}, [registerEventHandler, acknowledgeMessage]); // eslint-disable-line
|
||||
}, [registerEventHandler]); // eslint-disable-line
|
||||
|
||||
const clearLog = () => {
|
||||
clearLogs();
|
||||
};
|
||||
|
||||
const openSelectedFolder = () => {
|
||||
window.electron.ipcRenderer.sendMessage('open-logger-folder');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<TableContainer maxH={'150px'} overflowY="scroll">
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>时间</Th>
|
||||
<Th>
|
||||
<HStack width="full">
|
||||
<Text>内容</Text>
|
||||
<Button size="sm" onClick={clearLog}>
|
||||
清空全部日志
|
||||
</Button>
|
||||
</HStack>
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{logs.map((log, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>{log.time}</Td>
|
||||
<Td>{log.content}</Td>
|
||||
<VStack>
|
||||
{/* 靠左对齐 */}
|
||||
<HStack width="full" justifyContent="flex-start">
|
||||
<Button size="sm" onClick={clearLog}>
|
||||
清空全部日志
|
||||
</Button>
|
||||
<Button size="sm" onClick={openSelectedFolder}>
|
||||
打开日志文件
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<TableContainer maxH={'150px'} overflowY="scroll" width="full">
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>时间</Th>
|
||||
<Th>内容</Th>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{logs.map((log, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>{log.time}</Td>
|
||||
<Td>{log.content}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -59,9 +59,9 @@ const SessionBox = ({
|
||||
</Flex>
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<VStack mt={4} align="stretch">
|
||||
{messages.map((message) => (
|
||||
{messages.map((message, ii) => (
|
||||
<Box
|
||||
key={message.id}
|
||||
key={`${message.id}-${ii}`}
|
||||
p={4}
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
|
||||
@@ -33,6 +33,8 @@ const MsgList = () => {
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [page, setPage] = useState(1); // 新增分页状态
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true); // 新增状态标识首次加载
|
||||
|
||||
const [sessions, setSessions] = useState<
|
||||
{
|
||||
sessionId: string;
|
||||
@@ -49,34 +51,35 @@ const MsgList = () => {
|
||||
isLoading: isMessagesLoading,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useQuery(
|
||||
['messageList', page],
|
||||
() =>
|
||||
getMessageList({
|
||||
page,
|
||||
pageSize: 10,
|
||||
ptfId: ptf,
|
||||
keyword: searchTerm,
|
||||
startTime: startDate,
|
||||
endTime: endDate,
|
||||
}),
|
||||
{
|
||||
keepPreviousData: true, // 保留旧数据,直到新数据加载完成
|
||||
},
|
||||
} = useQuery(['messageList', page], () =>
|
||||
getMessageList({
|
||||
page,
|
||||
pageSize: 10,
|
||||
ptfId: ptf,
|
||||
keyword: searchTerm,
|
||||
startTime: startDate,
|
||||
endTime: endDate,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.data) {
|
||||
// 参考下面的代码,懒加载取得的数据需要和旧的数据合并在一起重新分组
|
||||
if (data?.data && !isFetching) {
|
||||
const newSessions = Object.entries(data.data).map(
|
||||
([sessionId, messages]) => ({
|
||||
sessionId,
|
||||
messages,
|
||||
}),
|
||||
);
|
||||
setSessions((prevSessions) => [...prevSessions, ...newSessions]);
|
||||
|
||||
if (isInitialLoad) {
|
||||
setSessions(newSessions);
|
||||
setIsInitialLoad(false);
|
||||
} else {
|
||||
setSessions((prevSessions) => [...prevSessions, ...newSessions]);
|
||||
}
|
||||
}
|
||||
}, [data]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, isFetching]); // 添加isFetching依赖
|
||||
|
||||
if (isPlatformsLoading || isMessagesLoading) {
|
||||
return (
|
||||
@@ -89,7 +92,9 @@ const MsgList = () => {
|
||||
}
|
||||
|
||||
const handlerSearch = () => {
|
||||
setPage(1); // 搜索时重置页码
|
||||
setIsInitialLoad(true); // 触发新搜索时,标记为初次加载
|
||||
setSessions([]); // 清空现有会话
|
||||
setPage(1); // 重置页码
|
||||
refetch();
|
||||
};
|
||||
|
||||
@@ -118,9 +123,7 @@ const MsgList = () => {
|
||||
isDisabled={isPlatformsLoading}
|
||||
>
|
||||
{platforms?.data.map((platform) => (
|
||||
<option key={platform.id} value={platform.id}>
|
||||
{platform.name}
|
||||
</option>
|
||||
<option value={platform.id}>{platform.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
@@ -128,7 +131,13 @@ const MsgList = () => {
|
||||
placeholder="Search messages"
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<Button colorScheme="orange" variant="solid" onClick={handlerSearch}>
|
||||
<Button
|
||||
colorScheme="orange"
|
||||
variant="solid"
|
||||
onClick={handlerSearch}
|
||||
isLoading={isMessagesLoading || isFetching}
|
||||
disabled={isMessagesLoading || isFetching}
|
||||
>
|
||||
<Search2Icon />
|
||||
</Button>
|
||||
|
||||
@@ -150,7 +159,6 @@ const MsgList = () => {
|
||||
<VStack spacing={4} mt={5} align="stretch">
|
||||
{sessions.map((session, index) => (
|
||||
<SessionBox
|
||||
key={session.sessionId}
|
||||
index={index}
|
||||
sessionId={session.sessionId}
|
||||
messages={session.messages}
|
||||
|
||||
@@ -60,6 +60,7 @@ const PlatformSettings = () => {
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: '更新成功',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
@@ -68,6 +69,7 @@ const PlatformSettings = () => {
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: '更新失败',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
|
||||
@@ -47,6 +47,7 @@ const CustomerServiceSettings = () => {
|
||||
);
|
||||
} else {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: '未选择文件夹',
|
||||
description: '请先选择一个文件夹路径。',
|
||||
status: 'warning',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
@@ -20,13 +20,55 @@ import {
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Button,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
|
||||
import { useSettings } from './SettingsContext';
|
||||
import { checkGptHealth } from '../../services/platform/controller';
|
||||
|
||||
const GptSettings = () => {
|
||||
const { gptSettings, setGptSettings } = useSettings();
|
||||
const [show, setShow] = React.useState(false);
|
||||
const [show, setShow] = useState(false);
|
||||
const [isLoaded, setIsLoaded] = useState(false); // 新增状态标识是否加载完成
|
||||
const toast = useToast();
|
||||
|
||||
const handleCheckGptHealth = async () => {
|
||||
let health = false;
|
||||
let message = '';
|
||||
try {
|
||||
setIsLoaded(true); // 新增标识已加载完成
|
||||
const data = await checkGptHealth({
|
||||
base_url: gptSettings.gptAddress,
|
||||
key: gptSettings.apiKey,
|
||||
use_dify: gptSettings.useDify,
|
||||
model: gptSettings.model,
|
||||
});
|
||||
health = data.status;
|
||||
message = data.message;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoaded(false); // 新增标识已加载完成
|
||||
}
|
||||
|
||||
if (health) {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: 'GPT 服务正常',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: `请检查 GPT 服务设置 ${message}`,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion allowToggle>
|
||||
@@ -47,167 +89,165 @@ const GptSettings = () => {
|
||||
<AccordionPanel pb={4}>
|
||||
<FormControl display="flex" alignItems="center">
|
||||
<FormLabel htmlFor="useLazyTools" mb="0">
|
||||
使用懒人百宝箱
|
||||
使用 Dify
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="useLazyTools"
|
||||
isChecked={gptSettings.useLazyTools}
|
||||
isChecked={gptSettings.useDify}
|
||||
onChange={(e) =>
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
useLazyTools: e.target.checked,
|
||||
useDify: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{!gptSettings.useLazyTools && (
|
||||
<>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="gptAddress" mt="8px">
|
||||
<Highlight
|
||||
query="/v1"
|
||||
styles={{ px: '1', py: '1', bg: 'orange.100' }}
|
||||
>
|
||||
GPT 地址设置(尾部需要加上 /v1)
|
||||
</Highlight>
|
||||
<>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="gptAddress" mt="8px">
|
||||
<Highlight
|
||||
query="/v1"
|
||||
styles={{ px: '1', py: '1', bg: 'orange.100' }}
|
||||
>
|
||||
GPT 地址设置(尾部需要加上 /v1)
|
||||
</Highlight>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
ml="4"
|
||||
onClick={handleCheckGptHealth}
|
||||
loadingText="检查中"
|
||||
isLoading={isLoaded}
|
||||
>
|
||||
检查连接
|
||||
</Button>
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="gptAddress"
|
||||
value={gptSettings.gptAddress}
|
||||
onChange={(e) =>
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
gptAddress: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="model" mt="8px">
|
||||
使用的模型
|
||||
</FormLabel>
|
||||
<Select
|
||||
id="model"
|
||||
value={gptSettings.model}
|
||||
onChange={(e) =>
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
model: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="gpt3">GPT-3</option>
|
||||
<option value="gpt3.5">GPT-3.5</option>
|
||||
<option value="gpt4">GPT-4</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="temperature" mt="8px">
|
||||
Temperature(可选): {gptSettings.temperature}
|
||||
</FormLabel>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
id="temperature"
|
||||
value={gptSettings.temperature}
|
||||
onChange={(value) => {
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
temperature: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="topP" mt="8px">
|
||||
Top P(可选): {gptSettings.topP}
|
||||
</FormLabel>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
id="topP"
|
||||
value={gptSettings.topP}
|
||||
onChange={(value) => {
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
topP: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<HStack mb="4" mt="8px">
|
||||
<FormLabel htmlFor="stream" width="30%">
|
||||
Stream(可选)
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="gptAddress"
|
||||
value={gptSettings.gptAddress}
|
||||
onChange={(e) =>
|
||||
<Switch
|
||||
id="stream"
|
||||
isChecked={gptSettings.stream}
|
||||
onChange={(e) => {
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
gptAddress: e.target.value,
|
||||
})
|
||||
}
|
||||
stream: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="model" mt="8px">
|
||||
使用的模型
|
||||
</FormLabel>
|
||||
<Select
|
||||
id="model"
|
||||
value={gptSettings.model}
|
||||
onChange={(e) =>
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
model: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="gpt3">GPT-3</option>
|
||||
<option value="gpt3.5">GPT-3.5</option>
|
||||
<option value="gpt4">GPT-4</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="temperature" mt="8px">
|
||||
Temperature(可选): {gptSettings.temperature}
|
||||
</FormLabel>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
id="temperature"
|
||||
value={gptSettings.temperature}
|
||||
onChange={(value) => {
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
temperature: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="topP" mt="8px">
|
||||
Top P(可选): {gptSettings.topP}
|
||||
</FormLabel>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
id="topP"
|
||||
value={gptSettings.topP}
|
||||
onChange={(value) => {
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
topP: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<HStack mb="4" mt="8px">
|
||||
<FormLabel htmlFor="stream" width="30%">
|
||||
Stream(可选)
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="stream"
|
||||
isChecked={gptSettings.stream}
|
||||
onChange={(e) => {
|
||||
setGptSettings({
|
||||
...gptSettings,
|
||||
stream: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</FormControl>
|
||||
</>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="apiKey">API Key</FormLabel>
|
||||
{gptSettings.useLazyTools ? (
|
||||
<InputGroup size="md">
|
||||
<Input
|
||||
id="lazyKey"
|
||||
type="password"
|
||||
id="apiKey"
|
||||
pr="4.5rem"
|
||||
type={show ? 'text' : 'password'}
|
||||
value={gptSettings.apiKey}
|
||||
onChange={(e) =>
|
||||
setGptSettings({ ...gptSettings, apiKey: e.target.value })
|
||||
}
|
||||
onChange={(e) => {
|
||||
setGptSettings({ ...gptSettings, apiKey: e.target.value });
|
||||
}}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
) : (
|
||||
<InputGroup size="md">
|
||||
<Input
|
||||
id="apiKey"
|
||||
pr="4.5rem"
|
||||
type={show ? 'text' : 'password'}
|
||||
value={gptSettings.apiKey}
|
||||
onChange={(e) => {
|
||||
setGptSettings({ ...gptSettings, apiKey: e.target.value });
|
||||
<InputRightElement width="4.5rem">
|
||||
<Button
|
||||
h="1.75rem"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShow(!show);
|
||||
}}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
<InputRightElement width="4.5rem">
|
||||
<Button
|
||||
h="1.75rem"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShow(!show);
|
||||
}}
|
||||
>
|
||||
{show ? <ViewIcon /> : <ViewOffIcon />}
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
)}
|
||||
>
|
||||
{show ? <ViewIcon /> : <ViewOffIcon />}
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -34,10 +34,9 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
});
|
||||
|
||||
const [gptSettings, setGptSettings] = useState<GptSettingsForm>({
|
||||
useLazyTools: false,
|
||||
useDify: false,
|
||||
gptAddress: '',
|
||||
apiKey: '',
|
||||
lazyKey: '',
|
||||
model: '',
|
||||
temperature: 0,
|
||||
topP: 0,
|
||||
|
||||
@@ -27,14 +27,13 @@ const SettingsPage = () => {
|
||||
useEffect(() => {
|
||||
if (data && !isLoading) {
|
||||
setGptSettings({
|
||||
useLazyTools: data.data.use_lazy || false,
|
||||
useDify: data.data.use_dify || false,
|
||||
gptAddress: data.data.gpt_base_url || '',
|
||||
model: data.data.gpt_model || '',
|
||||
temperature: data.data.gpt_temperature || 0.7,
|
||||
apiKey: data.data.gpt_key || '',
|
||||
topP: data.data.gpt_top_p || 0.75,
|
||||
stream: data.data.stream || false,
|
||||
lazyKey: data.data.lazy_key || '',
|
||||
});
|
||||
setCustomerServiceSettings({
|
||||
extractPhone: data.data.extract_phone || false,
|
||||
@@ -63,15 +62,16 @@ const SettingsPage = () => {
|
||||
gpt_temperature: gptSettings.temperature,
|
||||
gpt_top_p: gptSettings.topP,
|
||||
stream: gptSettings.stream,
|
||||
use_lazy: gptSettings.useLazyTools,
|
||||
lazy_key: gptSettings.lazyKey,
|
||||
use_dify: gptSettings.useDify,
|
||||
});
|
||||
toast({
|
||||
position: 'top',
|
||||
title: '保存成功',
|
||||
status: 'success',
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
position: 'top',
|
||||
title: '保存失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
|
||||
@@ -8,18 +8,12 @@ export async function getPlatformList() {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getActivePlatformList() {
|
||||
const data = await GET<{
|
||||
data: Platform[];
|
||||
}>('/api/v1/base/platform/active');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updatePlatform(ids: string[]) {
|
||||
await POST('/api/v1/base/platform', ids);
|
||||
}
|
||||
|
||||
export async function updateRunner(data: {
|
||||
ids: string[];
|
||||
is_paused: boolean;
|
||||
is_keyword_match: boolean;
|
||||
}) {
|
||||
@@ -121,3 +115,18 @@ export async function updatePlatformSettings(settings: {
|
||||
}) {
|
||||
await POST('/api/v1/base/platform/settings', settings);
|
||||
}
|
||||
|
||||
export async function checkGptHealth(data: {
|
||||
base_url: string;
|
||||
key: string;
|
||||
model: string;
|
||||
use_dify: boolean;
|
||||
}) {
|
||||
const resp = await GET<{
|
||||
status: boolean;
|
||||
message: string;
|
||||
}>('/api/v1/base/gpt/health', data, {
|
||||
timeout: 5000,
|
||||
});
|
||||
return resp;
|
||||
}
|
||||
|
||||
+4
-4
@@ -56,8 +56,9 @@ export interface Config {
|
||||
gpt_top_p?: number; // GPT服务top_p
|
||||
stream?: boolean; // 是否开启stream
|
||||
|
||||
use_lazy?: boolean; // 是否使用懒人百宝箱
|
||||
lazy_key?: string; // 懒人百宝箱 key
|
||||
use_dify?: boolean; // 是否使用 Dify 百宝箱
|
||||
// use_lazy?: boolean; // 是否使用懒人百宝箱
|
||||
// lazy_key?: string; // 懒人百宝箱 key
|
||||
}
|
||||
|
||||
export interface CustomerServiceSettingsForm {
|
||||
@@ -70,10 +71,9 @@ export interface CustomerServiceSettingsForm {
|
||||
}
|
||||
|
||||
export interface GptSettingsForm {
|
||||
useLazyTools: boolean;
|
||||
useDify: boolean;
|
||||
gptAddress: string;
|
||||
apiKey: string;
|
||||
lazyKey: string;
|
||||
model: string;
|
||||
temperature: number;
|
||||
topP: number;
|
||||
|
||||
@@ -1,22 +1,58 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const getVersionInfo = async (currentVersion: string) => {
|
||||
let result = null;
|
||||
try {
|
||||
const data = await axios.get<{
|
||||
version: string;
|
||||
url: string;
|
||||
description: string;
|
||||
}>('https://update.wizgadg.top/check-update/chatgpt-on-cs');
|
||||
// 一个辅助函数,用于比较两个版本号,考虑非数字部分
|
||||
function isVersionGreater(onlineVersion: string, currentVersion: string) {
|
||||
// 正则表达式用于分离数字部分和后缀
|
||||
const versionRegex = /(\d+\.\d+\.\d+)(.*)/;
|
||||
const [, onlineMain, onlineSuffix] = onlineVersion.match(versionRegex) || [];
|
||||
const [, currentMain, currentSuffix] =
|
||||
currentVersion.match(versionRegex) || [];
|
||||
|
||||
result = data.data;
|
||||
// 检查版本是否需要更新
|
||||
if (result.version === currentVersion) {
|
||||
result = null;
|
||||
// 先比较主版本号
|
||||
const onlineParts = onlineMain.split('.').map(Number);
|
||||
const currentParts = currentMain.split('.').map(Number);
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < Math.max(onlineParts.length, currentParts.length); i++) {
|
||||
const onlinePart = onlineParts[i] || 0;
|
||||
const currentPart = currentParts[i] || 0;
|
||||
if (onlinePart > currentPart) {
|
||||
return true;
|
||||
}
|
||||
if (onlinePart < currentPart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果主版本号相同,比较后缀,视 "beta" 等作为较小
|
||||
if (onlineSuffix && currentSuffix) {
|
||||
return onlineSuffix > currentSuffix;
|
||||
}
|
||||
if (onlineSuffix) {
|
||||
return false; // online 有后缀但 current 无后缀,online 为较小版本
|
||||
}
|
||||
if (currentSuffix) {
|
||||
return true; // current 有后缀但 online 无后缀,online 为较大版本
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export const getVersionInfo = async (currentVersion: string) => {
|
||||
let updates: { version: string; url: string; description: string }[] = [];
|
||||
try {
|
||||
const response = await axios.get<
|
||||
Array<{ version: string; url: string; description: string }>
|
||||
>('https://update.wizgadg.top/check-update/chatgpt-on-cs');
|
||||
|
||||
const { data } = response;
|
||||
// 过滤出所有比当前版本新的版本
|
||||
updates = data.filter((versionInfo) =>
|
||||
isVersionGreater(versionInfo.version, currentVersion),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return result;
|
||||
return updates;
|
||||
};
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare module 'node-windows';
|
||||
// node_modules\.pnpm\node-windows@1.0.0-beta.8\node_modules\node-windows\lib\cmd.js
|
||||
@@ -0,0 +1,4 @@
|
||||
# socket.io
|
||||
|
||||
# 发起 WebSocket 连接
|
||||
GET http://localhost:9999/socket.io/?EIO=4&transport=polling
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
# POST https://api.openai.com/v1/chat/completions
|
||||
|
||||
|
||||
POST https://chat.lazaytools.top/api/v1/chat/completions
|
||||
content-type: application/json
|
||||
Authorization: Bearer lazybox-YfJipx8QHYp8OpEKVjoHos88g
|
||||
|
||||
{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who won the world series in 2020?"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The Los Angeles Dodgers won the World Series in 2020."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Where was it played?"
|
||||
}
|
||||
],
|
||||
"temperature": 1,
|
||||
"top_p": 1,
|
||||
"n": 1,
|
||||
"stream": false,
|
||||
"max_tokens": 250,
|
||||
"presence_penalty": 0,
|
||||
"frequency_penalty": 0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST https://api.dify.ai/v1/chat-messages
|
||||
content-type: application/json
|
||||
Authorization: Bearer app-5uPNxwQ6Q8bU6FzO3WZ72tqV
|
||||
|
||||
{
|
||||
"inputs": {},
|
||||
"response_mode": "blocking",
|
||||
"user": "apiuser",
|
||||
"query": "Who won the world series in 2020?"
|
||||
}
|
||||
@@ -9,8 +9,11 @@
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strictPropertyInitialization": false, // 关闭严格属性初始化
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"allowJs": true,
|
||||
"outDir": ".erb/dll"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user