Compare commits

...

27 Commits

Author SHA1 Message Date
7b16e3ed5c 1修改接口地址 2025-09-08 09:25:05 +08:00
b9065d4fce dev_tp bugfix合并 2025-08-30 16:21:00 +08:00
edc03710b0 1图标icon 2025-08-30 16:20:20 +08:00
0537b55c24 update 拦截 2025-08-30 14:52:59 +08:00
8fca906f0b 1、 2025-08-28 11:21:47 +08:00
6e49dcf58f fix catch en 2025-08-26 12:17:46 +08:00
9e94987255 add proxy check 2025-08-25 18:45:19 +08:00
93a685e6b7 Merge branch 'dev_tp' of 47.109.78.3:dev/seabox_fanyi_application into dev_tp 2025-08-25 18:03:50 +08:00
94a8e3928f add update prox 2025-08-25 18:03:30 +08:00
3d44efacb7 1获取默认api 2025-08-25 17:47:13 +08:00
343a766312 add trans hack 2025-08-25 09:29:31 +08:00
e6a446e39f Merge branch 'dev_tp' of 47.109.78.3:dev/seabox_fanyi_application into dev_tp 2025-08-24 00:09:23 +08:00
f928bdd39d change default translation 2025-08-24 00:09:02 +08:00
2ef1f80014 1、修改api接口配置 2025-08-22 21:45:35 +08:00
1e8c629266 1、图标icon 重复处理 2025-08-22 16:29:16 +08:00
0accb47375 fix and add fea v1 2025-08-16 21:47:55 +08:00
85635c1b30 新增 修改联系人昵称页面同步更新 2025-07-29 01:26:40 +08:00
5be250f88d 新增 获取默认翻译路线配置 2025-07-21 22:51:10 +08:00
4f5b2585f4 新增 不同场景的翻译路线选择 2025-07-20 21:04:26 +08:00
9fde1d96de 新增 translate_route表新增enable字段 2025-07-18 12:01:25 +08:00
16e07600ff 新增 便签 2025-07-16 02:40:13 +08:00
01ad40e3b9 修改 翻译路线 2025-07-14 16:00:22 +08:00
df697d79be 新增 云端获取翻译路线 2025-07-13 20:30:07 +08:00
302ba16c13 修复 翻译时查询不到翻译配置 2025-07-12 02:44:09 +08:00
8bc6a746a5 修复 切换右侧工具栏 翻译配置查询失败 2025-07-10 06:22:05 +08:00
5f407c5ee5 新增 截图工具 2025-07-10 05:04:30 +08:00
a86a2f9ba8 修改 去除代理设置隐藏密码开关 2025-07-08 16:54:38 +08:00
41 changed files with 3845 additions and 594 deletions

157
.augmentignore Normal file
View File

@ -0,0 +1,157 @@
# Node.js dependencies
node_modules/
frontend/node_modules/
# Build outputs and distributions
dist/
build/
frontend/dist/
public/dist/
# Electron binaries
electron/dist/
# Logs
logs/
*.log
# Database files
*.db
*.sqlite
*.sqlite3
# Temporary files
tmp/
temp/
.tmp/
# Cache directories
.cache/
.npm/
.yarn/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Package manager lock files (optional - uncomment if you want to exclude)
package-lock.json
pnpm-lock.yaml
# yarn.lock
# Build tools and binaries
app-builder-bin/
better-sqlite3/build/
bufferutil/build/
google-gax/build/
# Large vendor libraries
@google-cloud/
@electron/
javascript-obfuscator/dist/
@esbuild/
# Development and test files
coverage/
.nyc_output/
test-results/
# Documentation build
docs/build/
docs/dist/
# Environment files with sensitive data
.env.local
.env.production
.env.development
# Backup files
*.bak
*.backup
*.old
# Binary executables and libraries
*.exe
*.dll
*.so
*.dylib
*.bin
*.dmg
*.pkg
*.msi
*.deb
*.rpm
*.iobj
*.ipdb
# Large media files (if any)
*.mp4
*.avi
*.mov
*.wmv
*.flv
*.webm
*.mkv
*.m4v
# Archive files
*.zip
*.rar
*.7z
*.tar
*.tar.gz
*.tar.bz2
# SSL certificates and keys
ssl/
*.pem
*.key
*.crt
*.cert
# Data directories that might contain large files
data/
myUserData/
out/
run/
data/cache/
data/temp/
data/logs/
# Specific large directories found in this project
**/electron/dist/
**/app-builder-bin/
**/better-sqlite3/build/
**/bufferutil/build/
**/google-gax/build/
**/@google-cloud/translate/build/
**/@img/sharp-*/
**/javascript-obfuscator/dist/
# Frontend build artifacts
frontend/dist/
public/dist/
# Any .git directories (if nested repos exist)
**/.git/
# Large dependency patterns
**/@electron/
**/@esbuild/
**/element-plus/dist/
**/vite/dist/
**/rollup/dist/
**/protobufjs/dist/
**/vue/dist/

View File

@ -1 +1,22 @@
Telegram 协议版多开
# 量子翻译应用
Telegram 协议版多开翻译应用,支持多平台消息翻译和管理。
## 开发规范
### 多语言国际化
本项目支持多语言切换,所有开发者必须遵循多语言开发规范:
📖 **[多语言国际化开发规范](../INTERNATIONALIZATION_GUIDELINES.md)**
**重要提醒**
- 禁止在代码中硬编码中文或其他语言文本
- 所有用户可见的文本必须使用国际化机制
- 新功能开发前请先阅读国际化开发规范
- 提交代码前必须测试语言切换功能
### 核心要求
1. **前端**:使用 Vue I18n 进行国际化
2. **后端/主进程**:使用 `getI18nText()` 函数
3. **webview**:实现语言更新函数和事件监听
4. **测试**:确保语言切换后所有功能正常

View File

@ -1,7 +1,7 @@
'use strict';
"use strict";
const path = require('path');
const { getBaseDir } = require('ee-core/ps');
const path = require("path");
const { getBaseDir } = require("ee-core/ps");
/**
* 默认配置
@ -11,7 +11,7 @@ module.exports = () => {
openDevTools: false,
singleLock: true,
windowsOption: {
title: 'liangzi',
title: "liangzi",
width: 1000,
height: 700,
minWidth: 1000,
@ -23,22 +23,22 @@ module.exports = () => {
nodeIntegration: true,
// preload: path.join(getElectronDir(), 'bridge.js'),
},
titleBarStyle: 'hidden',
titleBarStyle: "hidden",
frame: true,
show: false,
icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'),
show: true,
icon: path.join(getBaseDir(), "public", "images", "logo-32.png"),
},
logger: {
rotator: 'day',
level: 'INFO',
rotator: "day",
level: "INFO",
outputJSON: false,
appLogName: 'ee.log',
coreLogName: 'ee-core.log',
errorLogName: 'ee-error.log',
appLogName: "ee.log",
coreLogName: "ee-core.log",
errorLogName: "ee-error.log",
},
remote: {
enable: false,
url: ''
url: "haiapp.org",
},
socketServer: {
enable: false,
@ -52,21 +52,21 @@ module.exports = () => {
cors: {
origin: true,
},
channel: 'socket-channel'
channel: "socket-channel",
},
httpServer: {
enable: false,
https: {
enable: false,
key: '/public/ssl/localhost+1.key',
cert: '/public/ssl/localhost+1.pem'
key: "/public/ssl/localhost+1.key",
cert: "/public/ssl/localhost+1.pem",
},
host: '127.0.0.1',
host: "127.0.0.1",
port: 7071,
},
mainServer: {
indexPath: '/public/dist/index.html',
channelSeparator: '/',
}
}
}
indexPath: "/public/dist/index.html",
channelSeparator: "/",
},
};
};

View File

@ -1,4 +1,4 @@
'use strict';
"use strict";
/**
* Development environment configuration, coverage config.default.js
@ -7,7 +7,12 @@ module.exports = () => {
return {
openDevTools: true,
jobs: {
messageLog: false
}
messageLog: false,
},
api: {
baseUrl: "http://127.0.0.1:8000/api",
wsBaseUrl: "ws://127.0.0.1:8000",
timeout: 30000,
},
};
};

View File

@ -1,4 +1,4 @@
'use strict';
"use strict";
/**
* coverage config.default.js
@ -6,5 +6,11 @@
module.exports = () => {
return {
openDevTools: false,
// 生产环境 API 配置
remote: {
enable: false,
//暂时当成接口endpoint使用
url: "haiapp.org",
}
};
};

View File

@ -1,32 +1,39 @@
'use strict';
const { logger } = require('ee-core/log');
const {contactInfoService} = require("../service/contactInfo");
const { contactInfoService } = require("../service/contactInfo");
const { app } = require("electron");
/**
* 联系人信息api
* @class
*/
class ContactInfoController {
async getContactInfo(args,event) {
return await contactInfoService.getContactInfo(args,event);
async getContactInfo(args, event) {
return await contactInfoService.getContactInfo(args, event);
}
async updateContactInfo(args,event) {
return await contactInfoService.updateContactInfo(args,event);
async updateContactInfo(args, event) {
return await contactInfoService.updateContactInfo(args, event);
}
async getFollowRecord(args,event) {
return await contactInfoService.getFollowRecord(args,event);
async getFollowRecord(args, event) {
return await contactInfoService.getFollowRecord(args, event);
}
async addFollowRecord(args,event) {
return await contactInfoService.addFollowRecord(args,event);
async addFollowRecord(args, event) {
return await contactInfoService.addFollowRecord(args, event);
}
async updateFollowRecord(args,event) {
return await contactInfoService.updateFollowRecord(args,event);
async updateFollowRecord(args, event) {
return await contactInfoService.updateFollowRecord(args, event);
}
async deleteFollowRecord(args,event) {
return await contactInfoService.deleteFollowRecord(args,event);
async deleteFollowRecord(args, event) {
return await contactInfoService.deleteFollowRecord(args, event);
}
async updateContactRemark(args, event) {
let view = app.viewsMap.get(args.partitionId);
if (view && !view.webContents.isDestroyed()) {
view.webContents.send("message-from-main", args.nickName);
}
}
}
ContactInfoController.toString = () => '[class ContactInfoController]';

View File

@ -61,6 +61,10 @@ class SystemController {
async changePassword(args, event) {
return await systemService.changePassword(args, event);
}
async captureScreen(args, event) {
return await systemService.captureScreen(args, event);
}
}
SystemController.toString = () => '[class SystemController]';

View File

@ -56,11 +56,36 @@ class TranslateController {
async translateText(args,event) {
return await translateService.translateText(args,event);
}
async refreshTranslateRoutes(args,event) {
return await translateService.refreshTranslateRoutes(args,event);
}
async createTranslateConfig(args,event) {
return await translateService.createTranslateConfig(args,event);
}
async listNote(args,event) {
return await translateService.listNote(args,event);
}
async createNote(args,event) {
return await translateService.createNote(args,event);
}
async updateNote(args,event) {
return await translateService.updateNote(args,event);
}
async deleteNote(args,event) {
return await translateService.deleteNote(args,event);
}
async clearTranslateCache(args, event) {
return await translateService.clearTranslateCache(args, event);
}
async refreshSessionTranslateButtons(args, event) {
return await translateService.refreshSessionTranslateButtons(args, event);
}
}
TranslateController.toString = () => '[class TranslateController]';

View File

@ -79,6 +79,10 @@ class WindowController {
async saveProxyInfo(args, event) {
return await windowService.saveProxyInfo(args, event);
}
// 测试代理连通性
async testProxy(args, event) {
return await windowService.testProxy(args, event);
}
//打开当前会话控制台
async openSessionDevTools(args, event) {
return await windowService.openSessionDevTools(args, event);

View File

@ -25,7 +25,7 @@ app.register("preload", preload);
// 现在 customUserDataPath 安装目录同级
// const customUserDataPath = path.join(appInstallDir, 'liangzi_data');
const customUserDataPath = path.join(path.resolve(process.cwd(), '..'), 'liangzi_data');
console.log('customUserDataPath', customUserDataPath);
electronApp.setPath('userData', customUserDataPath);
// run

View File

@ -31,4 +31,26 @@ contextBridge.exposeInMainWorld("electronAPI", {
getLanguage: (args) => {
return ipcRenderer.invoke("get-language", args);
},
// 增强监控功能的IPC方法
avatarChangeNotify: (args) => {
return ipcRenderer.invoke("avatar-change-notify", args);
},
profileChangeNotify: (args) => {
return ipcRenderer.invoke("profile-change-notify", args);
},
statusChangeNotify: (args) => {
return ipcRenderer.invoke("status-change-notify", args);
},
aboutChangeNotify: (args) => {
return ipcRenderer.invoke("about-change-notify", args);
},
statusUpdateNotify: (args) => {
return ipcRenderer.invoke("status-update-notify", args);
},
detectLanguage: (args) => {
return ipcRenderer.invoke("detect-language", args);
},
onMessageFromMain: (callback) => ipcRenderer.on('message-from-main', (event, message) => callback(message)),
});

View File

@ -163,6 +163,7 @@ const ipcMainListener = () => {
isFilter = "false",
mode,
} = args;
console.log("text-translate", args);
if (text && text.trim() && to && route) {
//查询判断翻译服务商是否支持这个编码
const languageObj = await app.sdb.selectOne("language_list", {
@ -236,13 +237,65 @@ const ipcMainListener = () => {
}
});
// 语言设置变更处理
ipcMain.handle("language-change", async (event, args) => {
const { language } = args;
if (language) {
// 存储语言设置到应用配置
app.globalLanguage = language;
// 通知所有webview更新语言设置
const { BrowserWindow } = require('electron');
const windows = BrowserWindow.getAllWindows();
for (const window of windows) {
const webContents = window.webContents;
// 通知主窗口
if (webContents === getMainWindow().webContents) {
webContents.send("global-language-changed", { language });
}
// 通知所有子视图
for (const [partitionId, view] of app.viewsMap.entries()) {
if (view && !view.webContents.isDestroyed()) {
try {
await view.webContents.executeJavaScript(`
if (window.updateLanguage) {
window.updateLanguage('${language}');
}
// 触发自定义事件
window.dispatchEvent(new CustomEvent('languageChanged', {
detail: { language: '${language}' }
}));
`);
} catch (error) {
console.warn(`Failed to update language for partition ${partitionId}:`, error);
}
}
}
}
return { status: true, message: 'Language updated successfully' };
}
return { status: false, message: 'Invalid language parameter' };
});
// 获取当前语言设置
ipcMain.handle("get-current-language", async (event, args) => {
return {
status: true,
data: { language: app.globalLanguage || 'zh' }
};
});
//消息数量变更
ipcMain.handle("msg-count-change", async (event, args) => {
logger.info("msg-count-change", args);
const { platform, msgCount } = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
// 校验主窗口是否存在且未被销毁
if (!mainWin || mainWin.isDestroyed()) {
return; // 主窗口不存在时终止处理
@ -299,6 +352,237 @@ const ipcMainListener = () => {
ipcMain.handle("get-ws-base-url", async (event, args) => {
return { status: true, data: app.wsBaseUrl };
});
// 语言检测(转发到后端)
ipcMain.handle("detect-language", async (event, args) => {
try {
const text = args?.text;
const texts = args?.texts;
const params = texts ? {} : { params: { text } };
const url = app.baseUrl + '/detect_language';
const { get, post } = require('axios');
let res;
if (texts) {
res = await post(url, { texts }, { timeout: 15000 });
} else {
res = await get(url, params, { timeout: 10000 });
}
return res.data || { code: 5000, message: 'no data' };
} catch (e) {
return { code: 5000, message: String(e) };
}
});
// 增强监控功能的IPC处理器
// 头像变化通知
ipcMain.handle("avatar-change-notify", async (event, args) => {
logger.info("avatar-change-notify", args);
const { platform, phoneNumber, oldAvatarUrl, newAvatarUrl, changeTime } = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
if (!mainWin || mainWin.isDestroyed()) {
return;
}
// 记录头像变化到数据库
try {
await app.sdb.insert("follow_record", {
userId: phoneNumber,
platform: platform,
content: JSON.stringify({
type: "avatar_change",
oldAvatarUrl: oldAvatarUrl,
newAvatarUrl: newAvatarUrl,
changeTime: changeTime
}),
timestamp: changeTime
});
console.log(`${platform}: 头像变化已记录 - ${phoneNumber}`);
// 通知前端
if (!mainWin.isDestroyed()) {
mainWin.webContents.send("avatar-change-notify", {
platform,
phoneNumber,
oldAvatarUrl,
newAvatarUrl,
changeTime
});
}
} catch (error) {
logger.error("记录头像变化失败", error);
}
});
// 用户资料变化通知
ipcMain.handle("profile-change-notify", async (event, args) => {
logger.info("profile-change-notify", args);
const { platform, changeType, oldValue, newValue, changeTime } = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
if (!mainWin || mainWin.isDestroyed()) {
return;
}
try {
await app.sdb.insert("follow_record", {
userId: "current_user", // 可以根据需要调整
platform: platform,
content: JSON.stringify({
type: "profile_change",
changeType: changeType,
oldValue: oldValue,
newValue: newValue,
changeTime: changeTime
}),
timestamp: changeTime
});
console.log(`${platform}: 用户资料变化已记录 - ${changeType}`);
// 通知前端
if (!mainWin.isDestroyed()) {
mainWin.webContents.send("profile-change-notify", {
platform,
changeType,
oldValue,
newValue,
changeTime
});
}
} catch (error) {
logger.error("记录用户资料变化失败", error);
}
});
// 状态变化通知
ipcMain.handle("status-change-notify", async (event, args) => {
logger.info("status-change-notify", args);
const { platform, changeType, oldValue, newValue, changeTime } = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
if (!mainWin || mainWin.isDestroyed()) {
return;
}
try {
await app.sdb.insert("follow_record", {
userId: "current_user",
platform: platform,
content: JSON.stringify({
type: "status_change",
changeType: changeType,
oldValue: oldValue,
newValue: newValue,
changeTime: changeTime
}),
timestamp: changeTime
});
console.log(`${platform}: 状态变化已记录 - ${changeType}`);
// 通知前端
if (!mainWin.isDestroyed()) {
mainWin.webContents.send("status-change-notify", {
platform,
changeType,
oldValue,
newValue,
changeTime
});
}
} catch (error) {
logger.error("记录状态变化失败", error);
}
});
// 关于信息变化通知
ipcMain.handle("about-change-notify", async (event, args) => {
logger.info("about-change-notify", args);
const { platform, changeType, oldValue, newValue, changeTime } = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
if (!mainWin || mainWin.isDestroyed()) {
return;
}
try {
await app.sdb.insert("follow_record", {
userId: "current_user",
platform: platform,
content: JSON.stringify({
type: "about_change",
changeType: changeType,
oldValue: oldValue,
newValue: newValue,
changeTime: changeTime
}),
timestamp: changeTime
});
console.log(`${platform}: 关于信息变化已记录 - ${changeType}`);
// 通知前端
if (!mainWin.isDestroyed()) {
mainWin.webContents.send("about-change-notify", {
platform,
changeType,
oldValue,
newValue,
changeTime
});
}
} catch (error) {
logger.error("记录关于信息变化失败", error);
}
});
// 状态更新通知(动态/Stories
ipcMain.handle("status-update-notify", async (event, args) => {
logger.info("status-update-notify", args);
const { platform, updateType, timestamp, details } = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
if (!mainWin || mainWin.isDestroyed()) {
return;
}
try {
await app.sdb.insert("follow_record", {
userId: "current_user",
platform: platform,
content: JSON.stringify({
type: "status_update",
updateType: updateType,
details: details,
timestamp: timestamp
}),
timestamp: timestamp
});
console.log(`${platform}: 状态更新已记录 - ${updateType}`);
// 通知前端
if (!mainWin.isDestroyed()) {
mainWin.webContents.send("status-update-notify", {
platform,
updateType,
timestamp,
details
});
}
} catch (error) {
logger.error("记录状态更新失败", error);
}
});
};
/**

View File

@ -9,13 +9,13 @@ const { getTimeStr } = require("../utils/CommonUtils");
// const { initUpdater } = require("../updater");
const { checkUpdate } = require("../updater/index");
const { initializeTranslationCodes } = require("./iniit_language");
const { post, get, put } = require("axios");
const endpoint=process.env.BASE_URL || "haiapp.org"
const wsBaseUrl = `ws://${endpoint}`;
// const wsBaseUrl = `ws://192.168.2.22:8000`;
// const baseUrl = "http://192.168.2.22:8000/api";
// const baseUrl = "http://haiapp.org/api";
const baseUrl =`http://${endpoint}/api`;
// 从配置文件获取API配置
// const { remote } = getConfig();
// const endpoint =remote ? remote.url:"hiapp.org"
// const wsBaseUrl = `ws://${endpoint}`;
// const baseUrl = `http://${endpoint}}/api`;
const initializeDatabase = async () => {
// 定义表结构
const tables = {
@ -72,8 +72,11 @@ const initializeDatabase = async () => {
chineseDetectionStatus: "TEXT",
translatePreview: "TEXT",
interceptChinese: "TEXT",
interceptLanguages: "TEXT",
translateHistory: "TEXT",
autoTranslateGroupMessage: "TEXT",
historyTranslateRoute: "TEXT",
usePersonalConfig: 'TEXT DEFAULT "true"',
},
constraints: [],
},
@ -84,6 +87,7 @@ const initializeDatabase = async () => {
zhName: "TEXT",
enName: "TEXT",
otherArgs: "TEXT",
enable: "INTEGER",
},
constraints: [],
},
@ -126,9 +130,9 @@ const initializeDatabase = async () => {
timestamp: "TEXT",
tengXun: "TEXT",
deepl: "TEXT",
deepseek:"TEXT",
deepseek: "TEXT",
bing: "TEXT",
chatGpt4o:"TEXT"
chatGpt4o: "TEXT",
},
constraints: [],
},
@ -227,80 +231,119 @@ const initializeTableData = async () => {
});
}
// 初始化翻译线路
const translationRoute = [
{
name: "deepl",
zhName: "DeepL翻译",
enName: "DeepL Translate",
otherArgs: `{"apiUrl": "https://api-free.deepl.com/v2/translate","apiKey": "","secretKey": ""}`,
},
{
name: "deepseek",
zhName: "DeepSeek翻译",
enName: "DeepSeek Translate",
otherArgs: `{"apiUrl": "https://api.deepseek.com/api/v1/translate","apiKey": "","secretKey": ""}`,
},
{
name: "youDao",
zhName: "有道翻译",
enName: "Youdao Translate",
otherArgs: `{"apiUrl": "https://openapi.youdao.com/api","appId": "","apiKey": ""}`,
},
{
name: "tengXun",
zhName: "腾讯翻译",
enName: "Tencent Translate",
otherArgs: `{"apiUrl": "https://fanyi.qq.com/api","appId": "","apiKey": ""}`,
},{
name: "baidu",
zhName: "百度翻译",
enName: "Baidu Translate",
otherArgs: `{"apiUrl": "https://fanyi.baidu.com/v2transapi","appId": "","apiKey": "","secretKey": ""}`,
},{
name:"huoShan",
zhName:"火山翻译",
enName:"HuoShan Translate",
otherArgs:`{"apiUrl": "https://api.hushan.ai/v1/translation","appId": "","apiKey": "","secretKey": ""}`,
},{
name:"google",
zhName:"谷歌翻译",
enName:"Google Translate",
otherArgs:`{"apiUrl": "https://translation.googleapis.com/language/translate/v2","appId": "","apiKey": "","secretKey": ""}`,
},{
name:"bing",
zhName:"必应翻译",
enName:"Bing Translate",
otherArgs:`{"apiUrl": "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0","appId": "","apiKey": "","secretKey": ""}`,
},{
name:"chatGpt4o",
zhName:"ChatGpt4o翻译",
enName:"ChatGpt4o Translate",
otherArgs:`{"apiUrl": "https://api.chatgpt4o.com/translate","appId": "","apiKey": "","secretKey": ""}`,
}
];
const { remote } = getConfig();
const endpoint = remote ? remote.url : "hiapp.org";
const baseUrl = `http://${endpoint}/api`;
const url = baseUrl + "/translate/list_route";
console.log("url", url);
const res = await get(url, {}, { timeout: 30000 });
console.log("res======:", res);
let translationRoute = [];
const { code, data } = res.data;
if (code === 2000) {
translationRoute = data;
} else {
// 初始化翻译线路
translationRoute = [
{
name: "deepl",
zhName: "DeepL翻译",
enName: "DeepL Translate",
otherArgs: `{"apiUrl": "https://api-free.deepl.com/v2/translate","apiKey": "","secretKey": ""}`,
enable: 1,
},
{
name: "deepseek",
zhName: "DeepSeek翻译",
enName: "DeepSeek Translate",
otherArgs: `{"apiUrl": "https://api.deepseek.com/api/v1/translate","apiKey": "","secretKey": ""}`,
enable: 1,
},
{
name: "youDao",
zhName: "有道翻译",
enName: "Youdao Translate",
otherArgs: `{"apiUrl": "https://openapi.youdao.com/api","appId": "","apiKey": ""}`,
enable: 1,
},
{
name: "xiaoNiu",
zhName: "小牛翻译",
enName: "XiaoNiu Translate",
otherArgs: `{"apiUrl": "https://api.xiaoniu.com/translate","appId": "","apiKey": "","secretKey": ""}`,
enable: 1,
},
{
name: "tengXun",
zhName: "腾讯翻译",
enName: "Tencent Translate",
otherArgs: `{"apiUrl": "https://fanyi.qq.com/api","appId": "","apiKey": ""}`,
enable: 1,
},
{
name: "baidu",
zhName: "百度翻译",
enName: "Baidu Translate",
otherArgs: `{"apiUrl": "https://fanyi.baidu.com/v2transapi","appId": "","apiKey": "","secretKey": ""}`,
enable: 1,
},
{
name: "huoShan",
zhName: "火山翻译",
enName: "HuoShan Translate",
otherArgs: `{"apiUrl": "https://api.hushan.ai/v1/translation","appId": "","apiKey": "","secretKey": ""}`,
enable: 1,
},
{
name: "google",
zhName: "谷歌翻译",
enName: "Google Translate",
otherArgs: `{"apiUrl": "https://translation.googleapis.com/language/translate/v2","appId": "","apiKey": "","secretKey": ""}`,
enable: 1,
},
{
name: "bing",
zhName: "必应翻译",
enName: "Bing Translate",
otherArgs: `{"apiUrl": "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0","appId": "","apiKey": "","secretKey": ""}`,
enable: 1,
},
{
name: "chatGpt4o",
zhName: "ChatGpt4o翻译",
enName: "ChatGpt4o Translate",
otherArgs: `{"apiUrl": "https://api.chatgpt4o.com/translate","appId": "","apiKey": "","secretKey": ""}`,
enable: 1,
},
];
}
// 清空translate_route表
await app.sdb.deleteAll("translate_route");
for (let item of translationRoute) {
const args = {
name: item.name,
zhName: item.zhName,
enName: item.enName,
otherArgs: item.otherArgs,
enable: item.enable,
};
const route = await app.sdb.selectOne("translate_route", {
name: item.name,
});
if (!route) {
// 初始化翻译线路
await app.sdb.insert("translate_route", args);
}
await app.sdb.insert("translate_route", args);
// const route = await app.sdb.selectOne("translate_route", {
// name: item.name,
// });
// if (!route) {
// // 初始化翻译线路
// await app.sdb.insert("translate_route", args);
// }
}
//初始化翻译编码信息
// 插入中文(简体)
await initializeTranslationCodes(app.sdb);
//1.0.51单独设置数据
//1.0.51单独设置数据
// await app.sdb.update("translate_config",{translateRoute:"youDao"},{});
};
class Lifecycle {
@ -315,13 +358,21 @@ class Lifecycle {
* electron app ready
*/
async electronAppReady() {
logger.info("[lifecycle] electron-app-ready");
logger.info("[lifecycle] ---- electron-app-ready");
const { remote } = getConfig();
const endpoint = remote ? remote.url : "hiapp.org";
const wsBaseUrl = `ws://${endpoint}`;
const baseUrl = `http://${endpoint}/api`;
// 将配置保存到app对象供其他模块使用
app.baseUrl = baseUrl;
app.wsBaseUrl = wsBaseUrl;
await initializeDatabase();
await initializePlatform();
await initializeTableData();
app.viewsMap = new Map();
app.baseUrl = baseUrl;
app.wsBaseUrl = wsBaseUrl;
}
ready;
/**

View File

@ -6,6 +6,29 @@ const quickReply = async (args)=>{
console.log("指纹屏蔽脚本执行");
// 全局语言设置
window.currentAppLanguage = 'zh'; // 默认中文
// 语言更新函数
window.updateLanguage = function(language) {
window.currentAppLanguage = language;
console.log('CustomWeb language updated to:', language);
// 更新页面语言设置
document.documentElement.lang = language;
// 触发自定义语言更新事件
window.dispatchEvent(new CustomEvent('appLanguageChanged', {
detail: { language }
}));
};
// 监听语言变更事件
window.addEventListener('languageChanged', function(event) {
const { language } = event.detail;
window.updateLanguage(language);
});
// 禁用webdriver
Object.defineProperty(navigator, "webdriver", {
get: () => false,

View File

@ -152,12 +152,94 @@ const getNewMsgCount = () => {
onlineStatusCheck();
//========================用户基本信息获取结束============
//中文检测
const containsChinese = (text)=> {
const regex = /[\u4e00-\u9fa5]/; // 匹配中文字符的正则表达式
return regex.test(text); // 如果包含中文字符返回 true否则返回 false
// 语言检测与拦截支持
const containsChinese = (text)=> /[\u4e00-\u9fa5]/.test(text);
const containsJapanese = (text)=> /[\u3040-\u30ff]/.test(text);
const containsKorean = (text)=> /[\uac00-\ud7af]/i.test(text);
const containsArabic = (text)=> /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/.test(text);
const containsRussian = (text)=> /[\u0400-\u04FF\u0500-\u052F]/.test(text);
const isLikelyEnglish = (text)=> {
if (!text) return false;
if (/[^\x00-\x7F]/.test(text)) return false;
return /[A-Za-z]/.test(text);
}
const sendMsg = ()=> {
const detectLanguageSet = (text = '') => {
const set = new Set();
if (!text) return set;
if (containsChinese(text)) set.add('zh');
if (containsJapanese(text)) set.add('ja');
if (containsKorean(text)) set.add('ko');
if (containsArabic(text)) set.add('ar');
if (containsRussian(text)) set.add('ru');
if (isLikelyEnglish(text)) set.add('en');
if (/[àâäæçéèêëîïôœùûüÿñ¡¿]/i.test(text)) {
if (/ñ|¡|¿|á|é|í|ó|ú/i.test(text)) set.add('es');
if (/ç|à|â|ä|é|è|ê|ë|î|ï|ô|œ|ù|û|ü|ÿ/i.test(text)) set.add('fr');
if (/ã|õ|á|é|í|ó|ú/i.test(text)) set.add('pt');
}
return set;
};
const parseInterceptLanguages = (val) => {
if (!val) return [];
if (Array.isArray(val)) return val.filter(Boolean);
return String(val).split(',').map(s=>s.trim()).filter(Boolean);
};
const shouldInterceptByLanguages = (text) => {
if (!trcConfig || trcConfig.interceptChinese !== 'true') return false;
const list = parseInterceptLanguages(trcConfig.interceptLanguages);
// 修复:如果没有选择任何语言,则不拦截任何内容
if (list.length === 0) return false;
const targets = list;
const found = detectLanguageSet(text);
return targets.some(code=>found.has(code));
};
// 详细语言拦截检查:返回是否拦截及原因
const checkInterceptLanguage = async (text) => {
try {
if (!trcConfig || trcConfig.interceptChinese !== 'true') return { blocked: false };
const list = parseInterceptLanguages(trcConfig.interceptLanguages);
// 修复:如果没有选择任何语言,则不拦截任何内容
if (list.length === 0) return { blocked: false };
const targets = list;
const res = await ipc.detectLanguage({ text });
const lang = String(res?.data?.lang || '').toLowerCase();
if (lang && targets.includes(lang)) {
return { blocked: true, reason: `语言 ${lang} 在拦截列表(${targets.join(',')})` };
}
const found = detectLanguageSet(text);
for (const code of targets) {
if (found.has(code)) return { blocked: true, reason: `检测到可能的语言 ${code} 在拦截列表(${targets.join(',')})` };
}
} catch (e) {}
return { blocked: false };
};
// 错误消息检测:拦截把错误当消息发送
const isErrorText = (s) => {
const patterns = [
'HTTPConnectionPool', 'timeout', '超时', '错误', 'Error',
];
return patterns.some((p) => s.includes(p));
};
const sendMsg = async ()=> {
// 最终文本验证(含错误/空文本/语言拦截),仅当开启拦截开关时检查语言
{
// 获取最终文本
const editableDiv = document.getElementById('editable-message-text');
const content = editableDiv?.textContent?.trim() || '';
// 先拦截错误与空消息(始终启用)
const s = content;
if (!s || !s.trim()) { alert('已拦截:消息为空或仅包含空白'); return; }
if (isErrorText(s)) { alert('已拦截:检测到错误内容(翻译失败或错误信息)'); return; }
// 再按配置拦截语言
if (trcConfig.interceptChinese === 'true') {
const chk = await checkInterceptLanguage(s);
if (chk.blocked) { alert(`已拦截:${chk.reason}`); return; }
}
}
let sendButton = document.querySelectorAll('button.Button.send.main-button.default.secondary.round.click-allowed')[0]
if (sendButton) {
sendButton.click();
@ -178,7 +260,8 @@ const sendTranslate = async (text,to)=>{
styledTextarea.setTranslateStatus(true)
styledTextarea.setIsProcessing(false);
}else {
styledTextarea.setContent(res.message);
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
styledTextarea.setContent('...');
styledTextarea.setTranslateStatus(false)
styledTextarea.setIsProcessing(false);
}
@ -350,7 +433,7 @@ const addTranslateListener = () => {
if (status === true && isProcess === false) {
const translateText = styledTextarea.getContent();
await inputMsg(translateText)
sendMsg()
await sendMsg()
styledTextarea.setTranslateStatus(false)
styledTextarea.setContent('...')
return;
@ -366,13 +449,14 @@ const addTranslateListener = () => {
const translateText = res.data;
await inputMsg(translateText)
styledTextarea.setContent(translateText);
setTimeout(()=>{
setTimeout(async ()=>{
isProcessing = false;
sendMsg()
await sendMsg()
styledTextarea.setContent('...');
},500)
}else {
styledTextarea.setContent(res.message);
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
styledTextarea.setContent('...');
isProcessing = false;
return;
}
@ -384,7 +468,7 @@ const addTranslateListener = () => {
return;
}
}
sendMsg();
await sendMsg();
}
},true);
@ -551,6 +635,40 @@ const monitorMainNode = ()=> {
}
}
};
// 检查并显示已有的翻译缓存
const checkAndDisplayExistingTranslation = async (text, leftDiv, rightDiv) => {
try {
const to = trcConfig.receiveTargetLanguage;
// 调用翻译API但设置isFilter为true这样只会查询缓存不会实际翻译
const res = await ipc.translateText({
route: trcConfig.historyTranslateRoute || trcConfig.translateRoute,
text: text,
from: trcConfig.receiveSourceLanguage,
to: to,
refresh: 'false',
mode: trcConfig.mode,
isFilter: 'true' // 关键:只查询缓存,不实际翻译
});
if (res.status && res.data) {
// 找到了缓存的翻译结果,显示它
leftDiv.innerHTML = res.data;
leftDiv.style.color = 'var(--color-text)';
// 缓存存在时,刷新按钮正常显示
rightDiv.style.display = '';
} else {
// 没有缓存,显示默认状态
leftDiv.textContent = '';
rightDiv.style.display = '';
}
} catch (error) {
console.log('检查翻译缓存时出错:', error);
// 出错时显示默认状态
leftDiv.textContent = '';
rightDiv.style.display = '';
}
};
const createTranslateButtonForMessage = async (msgSpan) => {
let text = getMsgText(msgSpan.parentNode);
if (!text) return;
@ -589,13 +707,15 @@ const monitorMainNode = ()=> {
const from = trcConfig.receiveSourceLanguage;
const to = trcConfig.receiveTargetLanguage;
const mode = trcConfig.mode;
const res = await ipc.translateText({route: route, text: text,from:from, to: to,refresh:'true',mode:mode});
// 修复:刷新按钮不自动清理缓存,保留翻译历史
const res = await ipc.translateText({route: route, text: text,from:from, to: to,refresh:'false',mode:mode});
if (res.status) {
leftDiv.innerHTML = res.data;
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
leftDiv.textContent = '翻译失败';
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
rightDiv.style.display = '';
}
});
@ -607,6 +727,9 @@ const monitorMainNode = ()=> {
// 插入到消息元素右侧
msgSpan.parentNode.insertBefore(translateDiv, msgSpan.nextSibling);
// 检查是否已有翻译缓存并显示
await checkAndDisplayExistingTranslation(text, leftDiv, rightDiv);
const receiveTranslateStatus = trcConfig.receiveTranslateStatus;
logger.info('发送消息 当前配置:',trcConfig)
if (receiveTranslateStatus === 'true') {

View File

@ -120,7 +120,8 @@ const sendTranslate = async (text,to)=>{
styledTextarea.setTranslateStatus(true)
styledTextarea.setIsProcessing(false);
}else {
styledTextarea.setContent(res.message);
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
styledTextarea.setContent('...');
styledTextarea.setTranslateStatus(false)
styledTextarea.setIsProcessing(false);
}
@ -320,7 +321,8 @@ const addTranslateListener = () => {
styledTextarea.setTranslateStatus(false)
styledTextarea.setContent('...')
}else {
styledTextarea.setContent(res.message);
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
styledTextarea.setContent('...');
isProcessing = false;
}
}else {
@ -448,6 +450,40 @@ const monitorMainNode = ()=> {
}
}
};
// 检查并显示已有的翻译缓存
const checkAndDisplayExistingTranslation = async (text, leftDiv, rightDiv) => {
try {
const to = trcConfig.receiveTargetLanguage;
// 调用翻译API但设置isFilter为true这样只会查询缓存不会实际翻译
const res = await ipc.translateText({
route: trcConfig.historyTranslateRoute || trcConfig.translateRoute,
text: text,
from: trcConfig.receiveSourceLanguage,
to: to,
refresh: 'false',
mode: trcConfig.mode,
isFilter: 'true' // 关键:只查询缓存,不实际翻译
});
if (res.status && res.data) {
// 找到了缓存的翻译结果,显示它
leftDiv.innerHTML = res.data;
leftDiv.style.color = 'green';
// 缓存存在时,刷新按钮正常显示
rightDiv.style.display = '';
} else {
// 没有缓存,显示默认状态
leftDiv.textContent = '';
rightDiv.style.display = '';
}
} catch (error) {
console.log('检查翻译缓存时出错:', error);
// 出错时显示默认状态
leftDiv.textContent = '';
rightDiv.style.display = '';
}
};
//为每条消息创建翻译按钮
const createTranslateButtonForMessage = async ( msgSpan) => {
let text = msgSpan?.childNodes[0].textContent;
@ -486,13 +522,15 @@ const monitorMainNode = ()=> {
const from = trcConfig.receiveSourceLanguage;
const to = trcConfig.receiveTargetLanguage;
const mode = trcConfig.mode;
const res = await ipc.translateText({route: route, text: text,from:from, to: to,refresh:'true',mode:mode});
// 修复:刷新按钮不自动清理缓存,保留翻译历史
const res = await ipc.translateText({route: route, text: text,from:from, to: to,refresh:'false',mode:mode});
if (res.status) {
leftDiv.innerHTML = res.data;
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
leftDiv.textContent = '翻译失败';
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
rightDiv.style.display = '';
}
});
@ -502,6 +540,9 @@ const monitorMainNode = ()=> {
// 插入到消息元素右侧
msgSpan.appendChild(translateDiv);
// 检查是否已有翻译缓存并显示
await checkAndDisplayExistingTranslation(text, leftDiv, rightDiv);
const receiveTranslateStatus = trcConfig.receiveTranslateStatus;
if (receiveTranslateStatus === 'true') {
let text = msgSpan?.childNodes[0].textContent;
@ -520,7 +561,8 @@ const monitorMainNode = ()=> {
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
leftDiv.textContent = '翻译失败';
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
rightDiv.style.display = '';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,10 @@ class ContactInfoService {
);
// 返回最新配置(可选)
const updatedConfig = await app.sdb.selectOne('contact_info', { id:id });
// 同步到服务器
await this._syncContactInfoToServer(updatedConfig);
return {
status: true,
message:'更新成功',
@ -76,6 +80,29 @@ class ContactInfoService {
return {status:true,message:error.message};
}
}
// 同步联系人信息到服务器
async _syncContactInfoToServer(contactInfo) {
try {
const authInfo = app.authInfo;
if (!authInfo) return; // 如果没有认证信息,跳过同步
const url = app.baseUrl + '/sync_contact_info';
const data = {
username: authInfo.userName,
key: authInfo.authKey,
device: authInfo.machineCode,
contactInfo: contactInfo
};
const { post } = require('ee-core/request');
await post(url, data, { timeout: 10000 });
console.log('联系人信息已同步到服务器');
} catch (error) {
console.warn('同步联系人信息到服务器失败:', error.message);
// 不抛出错误,避免影响本地更新
}
}
async getFollowRecord(args,event) {
const {userId,platform} = args;
if (!platform?.trim() && !userId?.trim()) {

View File

@ -5,6 +5,8 @@ const os = require('os')
const { app, BrowserWindow } = require('electron')
const { post, get, put } = require("axios");
const { timestampToString } = require("../utils/CommonUtils");
const Screenshots = require('electron-screenshots');
class SystemService {
async getBaseInfo(args, event) {
@ -36,15 +38,16 @@ class SystemService {
try {
// 统一转换为小写比较
const lowerCaseCode = machineCode.toLowerCase();
console.log("app",app)
const url = app.baseUrl + '/check_login'
console.log('url======:', url)
const reqData = { key: authKey, device: lowerCaseCode }
const res = await post(url, reqData, { timeout: 30000 })
const { code, message, device_valid_until, user_remaining_chars, device_status, user_name,userApiKey, parent_id, user_id } = res.data
const { code, message, device_valid_until, user_remaining_chars, device_status, user_name, userApiKey, parent_id, user_id } = res.data
// 根据响应code处理不同情况
switch (code) {
case 2000: // 请求成功
@ -129,13 +132,13 @@ class SystemService {
console.log('url======', url)
const reqData = { username, password, device };
const res = await post(url, reqData, { timeout: 30000 });
const { code, message, device_valid_until, user_remaining_chars, device_status, user_name, parent_id, user_id,userApiKey } = res.data
logger.info('响应数据======',res.data)
const { code, message, device_valid_until, user_remaining_chars, device_status, user_name, parent_id, user_id, userApiKey } = res.data
// 根据响应code处理不同情况
switch (code) {
case 2000: // 请求成功
const timestamp = device_valid_until
const data = {
expireTime: timestampToString(timestamp),//失效时间
totalChars: user_remaining_chars,//剩余字符数
@ -240,6 +243,11 @@ class SystemService {
return { status: false, message: res.data.message };
}
}
async captureScreen(args, event) {
const screenshots = new Screenshots();
screenshots.startCapture();
}
}
SystemService.toString = () => '[class SystemService]';

View File

@ -10,6 +10,71 @@ const VolcEngineSDK = require("volcengine-sdk");
const { getTimeStr } = require("../utils/CommonUtils");
const { ApiInfo, ServiceInfo, Credentials, API, Request } = VolcEngineSDK;
// 国际化文本
const i18nTexts = {
zh: {
parameterError: '参数传递错误',
configNotExist: '配置不存在或窗口被关闭',
querySuccess: '查询成功',
missingRequiredParams: '缺少必要参数,编码和名称为必填项',
codeExists: '当前编码已存在,请使用不同的编码',
writeDataFailed: '数据写入失败,请稍后重试',
addSuccess: '语言配置添加成功',
addFailed: '添加失败,系统错误',
idRequired: 'id不能为空',
deleteSuccess: '成功删除{count}条数据',
noDataFound: '没有查询到这条数据',
paramsMissing: '参数缺失,请检查 ID、名称和code。',
updateSuccess: '语言配置更新成功',
updateFailed: '没有找到对应的语言配置,更新失败。',
systemError: '更新失败,系统错误',
translateTextEmpty: '翻译文本或编码不能为空!',
languageNotSupported: '不支持当前翻译语言!请检查翻译编码配置!',
contentOrSessionIdEmpty: '内容或窗口会话ID不能为空',
parameterIncomplete: '参数不完整缺少partitionId',
dataUpdateSuccess: '数据更新成功',
clearCacheConfirm: '确认清理当前会话历史翻译缓存吗?',
clearCacheSuccess: '清理缓存成功'
},
en: {
parameterError: 'Parameter error',
configNotExist: 'Configuration does not exist or window is closed',
querySuccess: 'Query successful',
missingRequiredParams: 'Missing required parameters, code and name are required',
codeExists: 'Code already exists, please use a different code',
writeDataFailed: 'Data write failed, please try again later',
addSuccess: 'Language configuration added successfully',
addFailed: 'Add failed, system error',
idRequired: 'ID cannot be empty',
deleteSuccess: 'Successfully deleted {count} records',
noDataFound: 'No data found',
paramsMissing: 'Parameters missing, please check ID, name and code.',
updateSuccess: 'Language configuration updated successfully',
updateFailed: 'Language configuration not found, update failed.',
systemError: 'Update failed, system error',
translateTextEmpty: 'Translation text or code cannot be empty!',
languageNotSupported: 'Current translation language not supported! Please check translation code configuration!',
contentOrSessionIdEmpty: 'Content or window session ID cannot be empty',
parameterIncomplete: 'Incomplete parameters: missing partitionId',
dataUpdateSuccess: 'Data updated successfully',
clearCacheConfirm: 'Confirm clearing translation cache for current session?',
clearCacheSuccess: 'Cache cleared successfully'
}
};
// 获取国际化文本
function getI18nText(key, language = 'zh', params = {}) {
const lang = language || app.globalLanguage || 'zh';
let text = i18nTexts[lang]?.[key] || i18nTexts.zh[key] || key;
// 替换参数
Object.keys(params).forEach(param => {
text = text.replace(`{${param}}`, params[param]);
});
return text;
}
class TranslateService {
/**
@ -24,15 +89,71 @@ class TranslateService {
*/
async getConfigInfo(args, event) {
let { platform, userId, partitionId } = args;
console.log("getConfigInfo args", args);
// 平台和用户id都为空返回错误
if (!platform?.trim() && !userId?.trim()) {
return {
status: false,
message: '参数不能同时为空,请至少填写一个有效参数'
}
}
// 平台和用户id都不为空哪个有值就查询哪个(用于应用翻译时查询翻译配置,前端会传递两个参数)
if (platform?.trim() && userId?.trim()) {
const configById = await app.sdb.selectOne('translate_config', { userId: userId })
if (configById) {
console.log('找到用户配置:', configById);
// 检查并修复translateRoute为空的情况
if (!configById.translateRoute || configById.translateRoute === 'null') {
console.log('用户配置translateRoute为空正在修复...');
await app.sdb.update('translate_config', { translateRoute: 'youDao' }, { userId: userId });
configById.translateRoute = 'youDao';
console.log('已修复用户配置translateRoute为youDao');
}
// 如果关闭了个人配置,则优先使用平台全局配置
if (configById.usePersonalConfig === 'false') {
const globalConfig = await app.sdb.selectOne('translate_config', { platform: platform })
if (globalConfig) {
if (!globalConfig.translateRoute || globalConfig.translateRoute === 'null') {
await app.sdb.update('translate_config', { translateRoute: 'youDao' }, { platform: platform });
globalConfig.translateRoute = 'youDao';
}
return { status: true, message: '查询成功(使用全局配置)', data: globalConfig }
}
}
return { status: true, message: '查询成功', data: configById }
} else {
const config = await app.sdb.selectOne('translate_config', { platform: platform })
if (config) {
console.log('找到平台配置:', config);
// 检查并修复translateRoute为空的情况
if (!config.translateRoute || config.translateRoute === 'null') {
console.log('平台配置translateRoute为空正在修复...');
await app.sdb.update('translate_config', { translateRoute: 'youDao' }, { platform: platform });
config.translateRoute = 'youDao';
console.log('已修复平台配置translateRoute为youDao');
}
return { status: true, message: '查询成功', data: config }
} else {
console.log('未找到配置,将创建新配置');
return { status: false, message: '查询失败,配置不存在' }
}
}
}
// 如果用户id或平台有一个为空分别判断用于创建个性化翻译配置前端只会传递一个参数
if (userId?.trim()) {
const configById = await app.sdb.selectOne('translate_config', { userId: userId })
if (configById) {
console.log('找到用户配置:', configById);
// 检查并修复translateRoute为空的情况
if (!configById.translateRoute || configById.translateRoute === 'null') {
console.log('用户配置translateRoute为空正在修复...');
await app.sdb.update('translate_config', { translateRoute: 'deepl' }, { userId: userId });
configById.translateRoute = 'deepl';
console.log('已修复用户配置translateRoute为deepl');
}
return { status: true, message: '查询成功', data: configById }
} else {
return { status: false, message: '查询失败,配置不存在' }
@ -40,12 +161,26 @@ class TranslateService {
} else {
const configByPlatform = await app.sdb.selectOne('translate_config', { platform: platform })
if (!configByPlatform && platform?.trim()) {
// 查询默认翻译线路
const url = app.baseUrl + '/get_default_translate_route_config'
const res = await get(url, {}, { timeout: 30000 })
const code = res.data.code
const data1 = res.data.data
let defaultTranslateRoute = 'youDao'
let defaultHistoryTranslateRoute = 'youDao'
if (code === 2000) {
const { real_time_route, history_route } = data1
defaultTranslateRoute = real_time_route
defaultHistoryTranslateRoute = history_route
}
//初始化平台翻译配置信息
const initialData = {
userId: "",
platform: platform,
receiveTranslateStatus: 'false',
translateRoute: 'youDao',
translateRoute: defaultTranslateRoute,
receiveSourceLanguage: "auto",
receiveTargetLanguage: "zh-CN",
sendTranslateStatus: "true",
@ -56,12 +191,24 @@ class TranslateService {
chineseDetectionStatus: "false",
translatePreview: "false",
interceptChinese: "false",
interceptLanguages: "",
translateHistory: "false",
autoTranslateGroupMessage: "false"
autoTranslateGroupMessage: "false",
historyTranslateRoute: defaultHistoryTranslateRoute
}
await app.sdb.insert('translate_config', initialData);
}
const data = await app.sdb.selectOne('translate_config', { platform: platform })
console.log('最终返回的配置数据:', data);
// 检查并修复translateRoute为空的情况
if (data && (!data.translateRoute || data.translateRoute === 'null')) {
console.log('配置存在但translateRoute为空正在修复...');
await app.sdb.update('translate_config', { translateRoute: 'youDao' }, { platform: platform });
data.translateRoute = 'youDao';
console.log('已修复translateRoute为youDao');
}
return { status: true, message: '查询成功2', data: data }
}
// if (partitionId) {
@ -181,9 +328,18 @@ class TranslateService {
if (!key) throw new Error('缺少必要参数key');
if (typeof value === 'undefined') return;
try {
// 规范化特殊字段
let normalizedValue = value;
if (key === 'interceptLanguages') {
if (Array.isArray(value)) {
normalizedValue = value.join(',');
} else if (typeof value === 'object' && value !== null) {
normalizedValue = Object.values(value).join(',');
}
}
// 构建更新对象(使用动态属性名)
const updateData = {
[key]: value
[key]: normalizedValue
};
console.log('updateData', updateData)
// 执行更新
@ -210,7 +366,7 @@ class TranslateService {
if (!status?.trim() && !partitionId?.trim()) {
return {
status: false,
message: '参数传递错误'
message: getI18nText('parameterError')
}
}
//获取窗口对象并查询是否有userid
@ -224,29 +380,29 @@ class TranslateService {
const count = await app.sdb.update('translate_config', { friendTranslateStatus: status }, { id: configById.id })
logger.info('状态修改成功:', args)
if (partitionId) await this._routeUpdateNotify(partitionId)
return { status: true, message: '数据更新成功' }
return { status: true, message: getI18nText('dataUpdateSuccess') }
}
}
}
return { status: false, message: '配置不存在或窗口被关闭' }
return { status: false, message: getI18nText('configNotExist') }
}
async getLanguageList(args, event) {
const list = await app.sdb.select('language_list', {})
if (list) return { status: true, message: '查询成功', data: list }
return { status: true, message: '查询成功', data: [] }
if (list) return { status: true, message: getI18nText('querySuccess'), data: list }
return { status: true, message: getI18nText('querySuccess'), data: [] }
}
async addLanguage(args, event) {
const { code, zhName, enName, youDao, baidu, huoShan, xiaoNiu, google, timestamp } = args;
// 参数验证
if (!code || !zhName) {
return { status: false, message: '缺少必要参数,编码和名称为必填项' };
return { status: false, message: getI18nText('missingRequiredParams') };
}
// 检查编码是否已存在
const info = await app.sdb.selectOne('language_list', { code: code });
if (info) {
return { status: false, message: '当前编码已存在,请使用不同的编码' };
return { status: false, message: getI18nText('codeExists') };
}
// 准备插入的数据
const rows = { code: code, zhName: zhName };
@ -265,23 +421,23 @@ class TranslateService {
const id = await app.sdb.insert('language_list', rows);
if (!id) {
return { status: false, message: '数据写入失败,请稍后重试' };
return { status: false, message: getI18nText('writeDataFailed') };
}
// 查询插入后的数据
const data = await app.sdb.selectOne('language_list', { id: id });
return { status: true, message: '语言配置添加成功', data: data };
return { status: true, message: getI18nText('addSuccess'), data: data };
} catch (error) {
return { status: false, message: `添加失败,系统错误${error.message}` };
return { status: false, message: `${getI18nText('addFailed')}${error.message}` };
}
}
async deleteLanguage(args, event) {
const { id } = args;
if (!id) return { status: false, message: 'id不能为空' }
if (!id) return { status: false, message: getI18nText('idRequired') }
const count = await app.sdb.delete('language_list', { id: id })
if (count > 0) return { status: true, message: `成功删除${count}条数据` }
return { status: false, message: `没有查询到这条数据` }
if (count > 0) return { status: true, message: getI18nText('deleteSuccess', undefined, { count }) }
return { status: false, message: getI18nText('noDataFound') }
}
async editLanguage(args, event) {
const { id, zhName, enName, code, youDao, baidu, huoShan, xiaoNiu, google } = args;
@ -377,6 +533,60 @@ class TranslateService {
}
// 文本分段函数
_splitTextForTranslation(text, maxLength = 2500) {
// 如果文本长度小于限制,直接返回
if (text.length <= maxLength) {
return [text];
}
const segments = [];
let currentSegment = '';
// 按句子分割(优先按句号、问号、感叹号分割)
const sentences = text.split(/([.!?。!?\n])/);
for (let i = 0; i < sentences.length; i++) {
const sentence = sentences[i];
// 如果当前段落加上新句子超过限制
if ((currentSegment + sentence).length > maxLength) {
if (currentSegment.trim()) {
segments.push(currentSegment.trim());
currentSegment = sentence;
} else {
// 如果单个句子就超过限制,强制分割
const words = sentence.split(' ');
let wordSegment = '';
for (const word of words) {
if ((wordSegment + ' ' + word).length > maxLength) {
if (wordSegment.trim()) {
segments.push(wordSegment.trim());
wordSegment = word;
} else {
// 单个词就超过限制,直接添加
segments.push(word);
}
} else {
wordSegment += (wordSegment ? ' ' : '') + word;
}
}
if (wordSegment.trim()) {
currentSegment = wordSegment;
}
}
} else {
currentSegment += sentence;
}
}
if (currentSegment.trim()) {
segments.push(currentSegment.trim());
}
return segments;
}
async translateText(args, event) {
const { route, text, from, to, partitionId, isFilter, mode, sourceTo } = args;
const routeMap = {
@ -387,13 +597,19 @@ class TranslateService {
xiaoNiu: this._xiaoNiuTranslateText
};
if (isFilter === 'false') {
//查询是否存在缓存
const cache = await app.sdb.selectOne('translate_cache', { partitionId: partitionId, toCode: to, text: text });
if (cache) {
return { status: true, message: '翻译成功', data: cache.translateText };
}
// 查询是否存在缓存
const cache = await app.sdb.selectOne('translate_cache', { partitionId: partitionId, toCode: to, text: text });
if (cache) {
return { status: true, message: '翻译成功', data: cache.translateText };
}
// 如果isFilter为true只查询缓存不进行实际翻译
if (isFilter === 'true') {
return { status: false, message: '未找到翻译缓存' };
}
// 检查是否需要分段处理(针对长文本和腾讯翻译)
const needSegmentation = text.length > 2500 || (route === 'tengXun' && text.length > 2000);
// logger.info('文本翻译:', args);
try {
if (mode === 'cloud') {
@ -401,38 +617,114 @@ class TranslateService {
const url = app.baseUrl + '/translate_api';
if (!authInfo) throw new Error('用户信息不存在!')
const { userName, machineCode, authKey } = authInfo;
const data = {
translation_service: route,
username: userName,
source_lang: from,
target_lang: sourceTo,
text: text,
key: authKey,
device: machineCode
if (needSegmentation) {
// 分段翻译
const segments = this._splitTextForTranslation(text, route === 'tengXun' ? 2000 : 2500);
const translatedSegments = [];
for (const segment of segments) {
const data = {
translation_service: route,
username: userName,
source_lang: from,
target_lang: sourceTo,
text: segment,
key: authKey,
device: machineCode
}
const result = await post(url, data);
if (result?.data?.translate_text) {
translatedSegments.push(result.data.translate_text);
} else {
throw new Error(`分段翻译失败: ${result?.message || '未知错误'}`);
}
// 添加短暂延迟避免API限制
await new Promise(resolve => setTimeout(resolve, 100));
}
const finalResult = translatedSegments.join('');
logger.info('translate_api result', finalResult)
if (finalResult && isFilter === 'false') {
//写入翻译消息缓存表
await app.sdb.insert('translate_cache', { route: route, text: text, translateText: finalResult, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() });
}
return { status: true, message: '翻译成功', data: finalResult };
} else {
// 正常翻译
const data = {
translation_service: route,
username: userName,
source_lang: from,
target_lang: sourceTo,
text: text,
key: authKey,
device: machineCode
}
logger.info('translate_api data', data)
const result = await post(url, data)
logger.info('translate_api result', result.data)
if (result?.data && isFilter === 'false') {
//写入翻译消息缓存表
await app.sdb.insert('translate_cache', { route: route, text: text, translateText: result.data.translate_text, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() });
}
return { status: true, message: '翻译成功', data: result.data.translate_text };
}
logger.info('translate_api data', data)
const result = await post(url, data)
if (result?.data && isFilter === 'false') {
//写入翻译消息缓存表
await app.sdb.insert('translate_cache', { route: route, text: text, translateText: result.data.translate_text, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() });
}
return { status: true, message: '翻译成功', data: result.data.translate_text };
}
if (mode === 'local') {
if (!routeMap[route]) {
throw new Error(`不支持的翻译服务: ${route}`);
}
// 调用指定的翻译函数
const result = await routeMap[route].call(this, text, to, route);
// logger.info(result);
if (result && isFilter === 'false') {
//写入翻译消息缓存表
await app.sdb.insert('translate_cache', { route: route, text: text, translateText: result, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() });
if (needSegmentation) {
// 分段翻译
const segments = this._splitTextForTranslation(text, route === 'tengXun' ? 2000 : 2500);
const translatedSegments = [];
for (const segment of segments) {
const result = await routeMap[route].call(this, segment, to, route);
if (result) {
translatedSegments.push(result);
} else {
throw new Error(`分段翻译失败`);
}
// 添加短暂延迟避免API限制
await new Promise(resolve => setTimeout(resolve, 100));
}
const finalResult = translatedSegments.join('');
if (finalResult && isFilter === 'false') {
//写入翻译消息缓存表
await app.sdb.insert('translate_cache', { route: route, text: text, translateText: finalResult, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() });
}
return { status: true, message: '翻译成功', data: finalResult };
} else {
// 正常翻译
const result = await routeMap[route].call(this, text, to, route);
// logger.info(result);
if (result && isFilter === 'false') {
//写入翻译消息缓存表
await app.sdb.insert('translate_cache', { route: route, text: text, translateText: result, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() });
}
return { status: true, message: '翻译成功', data: result };
}
return { status: true, message: '翻译成功', data: result };
}
} catch (err) {
const msg = err.response?.data?.error || err.message;
let msg = err.response?.data?.error || err.message;
// 检查是否是字符不足的错误
if (msg && (msg.includes('账户可用字符不足') || msg.includes('字符不足') || msg.includes('insufficient'))) {
msg = '第三方接口字符已用完,请充值后继续使用';
}
logger.error('翻译请求失败:', msg)
return { status: false, message: msg };
}
@ -443,7 +735,14 @@ class TranslateService {
if (!partitionId) return;
const view = app.viewsMap.get(partitionId);
if (view && !view.webContents.isDestroyed()) {
await view.webContents.executeJavaScript('updateConfigInfo()')
// 设置标志位,避免翻译历史消息
await view.webContents.executeJavaScript(`
window.isConfigUpdating = true;
updateConfigInfo();
setTimeout(() => {
window.isConfigUpdating = false;
}, 1000);
`)
}
}
@ -626,11 +925,25 @@ class TranslateService {
async createTranslateConfig(args, event) {
const { userId } = args;
// 查询默认翻译线路
const url = app.baseUrl + '/get_default_translate_route_config'
const res = await get(url, {}, { timeout: 30000 })
const code = res.data.code
const data1 = res.data.data
let defaultTranslateRoute = 'youDao'
let defaultHistoryTranslateRoute = 'youDao'
if (code === 2000) {
const { real_time_route, history_route } = data1
defaultTranslateRoute = real_time_route
defaultHistoryTranslateRoute = history_route
}
//初始化平台翻译配置信息
const initialData = {
userId: userId,
platform: '',
translateRoute: 'youDao',
translateRoute: defaultTranslateRoute,
receiveTranslateStatus: 'false',
receiveSourceLanguage: "auto",
receiveTargetLanguage: "zh-CN",
@ -642,12 +955,275 @@ class TranslateService {
chineseDetectionStatus: "false",
translatePreview: "false",
interceptChinese: "false",
interceptLanguages: "",
translateHistory: "false",
autoTranslateGroupMessage: "false"
autoTranslateGroupMessage: "false",
historyTranslateRoute: defaultHistoryTranslateRoute,
usePersonalConfig: 'true'
}
await app.sdb.insert('translate_config', initialData);
}
async refreshTranslateRoutes(args, event) {
try {
// 获取远程翻译路由配置
const url = app.baseUrl + '/translate/list_route'
const res = await get(url, {}, { timeout: 30000 })
let translationRoute = []
const { code, data } = res.data
if (code === 2000) {
translationRoute = data
} else {
// 使用默认翻译线路配置
translationRoute = [
{
name: "youDao",
zhName: "有道翻译",
enName: "Youdao Translate",
otherArgs: `{"apiUrl": "https://openapi.youdao.com/api","apiKey": "","secretKey": ""}`,
enable: 1
},
{
name: "tengXun",
zhName: "腾讯翻译",
enName: "Tencent Translate",
otherArgs: `{"apiUrl": "https://fanyi.qq.com/api","appId": "","apiKey": ""}`,
enable: 1
},
{
name: "baidu",
zhName: "百度翻译",
enName: "Baidu Translate",
otherArgs: `{"apiUrl": "https://fanyi.baidu.com/v2transapi","appId": "","apiKey": "","secretKey": ""}`,
enable: 1
},
{
name: "huoShan",
zhName: "火山翻译",
enName: "HuoShan Translate",
otherArgs: `{"apiUrl": "https://api.hushan.ai/v1/translation","appId": "","apiKey": "","secretKey": ""}`,
enable: 1
},
{
name: "google",
zhName: "谷歌翻译",
enName: "Google Translate",
otherArgs: `{"apiUrl": "https://translation.googleapis.com/language/translate/v2","appId": "","apiKey": "","secretKey": ""}`,
enable: 1
},
{
name: "bing",
zhName: "必应翻译",
enName: "Bing Translate",
otherArgs: `{"apiUrl": "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0","appId": "","apiKey": "","secretKey": ""}`,
enable: 1
},
{
name: "chatGpt4o",
zhName: "ChatGpt4o翻译",
enName: "ChatGpt4o Translate",
otherArgs: `{"apiUrl": "https://api.chatgpt4o.com/translate","appId": "","apiKey": "","secretKey": ""}`,
enable: 1
}
];
}
// 清空并重新插入翻译路由
await app.sdb.deleteAll("translate_route");
for (let item of translationRoute) {
const routeArgs = {
name: item.name,
zhName: item.zhName,
enName: item.enName,
otherArgs: item.otherArgs,
enable: item.enable
};
await app.sdb.insert("translate_route", routeArgs);
}
return { status: true, message: '翻译路由刷新成功', data: translationRoute };
} catch (error) {
console.error('刷新翻译路由失败:', error);
return { status: false, message: `刷新翻译路由失败: ${error.message}` };
}
}
async listNote(args, event) {
// args: { userId }
try {
const params = {};
if (args?.userId) params.userId = args.userId;
const res = await get(app.baseUrl + '/note/list_note', { params });
if (res.data && res.data.code === 2000) {
return { status: true, data: res.data.data };
} else {
return { status: false, message: res.data?.msg || '查询失败' };
}
} catch (e) {
return { status: false, message: e.message };
}
}
async createNote(args, event) {
// args: { userId, content }
try {
const res = await post(app.baseUrl + '/note/create_note', {
userId: args.userId,
content: args.content
});
if (res.data && res.data.code === 2000) {
return { status: true, data: res.data.data };
} else {
return { status: false, message: res.data?.msg || '创建失败' };
}
} catch (e) {
return { status: false, message: e.message };
}
}
async updateNote(args, event) {
// args: { id, content }
try {
const res = await post(app.baseUrl + '/note/update_note', {
id: args.id,
content: args.content
});
if (res.data && res.data.code === 2000) {
return { status: true, data: res.data.data };
} else {
return { status: false, message: res.data?.msg || '更新失败' };
}
} catch (e) {
return { status: false, message: e.message };
}
}
async deleteNote(args, event) {
// args: { id }
try {
const res = await get(app.baseUrl + '/note/delete_note', { params: { id: args.id } });
if (res.data && res.data.code === 2000) {
return { status: true };
} else {
return { status: false, message: res.data?.msg || '删除失败' };
}
} catch (e) {
return { status: false, message: e.message };
}
}
// 清除翻译缓存
async clearTranslateCache(args, event) {
const { partitionId, platform } = args;
try {
let deleteCount = 0;
if (partitionId) {
// 清除指定会话的翻译缓存
deleteCount = await app.sdb.delete('translate_cache', { partitionId: partitionId });
} else if (platform) {
// 清除指定平台的翻译缓存
const cacheList = await app.sdb.select('translate_cache', {});
for (const cache of cacheList) {
if (cache.partitionId && cache.partitionId.includes(platform)) {
await app.sdb.delete('translate_cache', { id: cache.id });
deleteCount++;
}
}
} else {
// 清除所有翻译缓存
deleteCount = await app.sdb.delete('translate_cache', {});
}
return {
status: true,
message: `成功清除${deleteCount}条翻译缓存`,
data: { deleteCount }
};
} catch (error) {
return {
status: false,
message: `清除翻译缓存失败: ${error.message}`
};
}
}
// 刷新会话翻译按钮
async refreshSessionTranslateButtons(args, event) {
const { partitionId } = args;
try {
if (!partitionId) {
return {
status: false,
message: '参数不完整缺少partitionId'
};
}
// 获取当前分区的webContents
const { BrowserWindow } = require('electron');
const windows = BrowserWindow.getAllWindows();
let refreshed = false;
for (const window of windows) {
const webContents = window.webContents;
// 检查是否是目标分区
if (webContents.session.partition === `persist:${partitionId}`) {
// 先弹出确认对话框,然后清理缓存并刷新对话内容区域
webContents.executeJavaScript(`
// 获取当前语言设置
const currentLanguage = window.currentAppLanguage || 'zh';
const confirmText = currentLanguage === 'zh' ?
'确认清理当前会话历史翻译缓存吗?' :
'Confirm clearing translation cache for current session?';
const successText = currentLanguage === 'zh' ?
'清理缓存成功' :
'Cache cleared successfully';
// 弹出确认对话框
const confirmed = confirm(confirmText);
if (confirmed) {
// 用户确认后,显示成功提示
alert(successText);
// 直接刷新整个WhatsApp页面内容这样对话框内容会重新加载
window.location.reload();
}
confirmed; // 返回确认结果
`).then((confirmed) => {
if (confirmed) {
console.log(`用户确认清理分区 ${partitionId} 的翻译缓存,页面已刷新`);
} else {
console.log(`用户取消清理分区 ${partitionId} 的翻译缓存`);
}
}).catch(err => {
console.warn(`向分区 ${partitionId} 发送清理确认对话框失败:`, err);
});
refreshed = true;
break;
}
}
if (!refreshed) {
console.warn(`未找到分区 ${partitionId} 的会话页面`);
}
return {
status: true,
message: refreshed ? '清理缓存操作已发送' : '未找到对应的会话页面'
};
} catch (error) {
console.error('发送清理缓存操作失败:', error);
return {
status: false,
message: `发送清理缓存操作失败: ${error.message}`
};
}
}
}
TranslateService.toString = () => '[class TranslateService]';

View File

@ -16,7 +16,7 @@ const {
} = require("../utils/CommonUtils");
const { getMainWindow } = require("ee-core/electron");
const { get } = require("axios");
const { net } = require("electron");
const { sockProxyRules } = require("electron-session-proxy");
class WindowService {
@ -160,6 +160,10 @@ class WindowService {
view.webContents.setWebRTCIPHandlingPolicy("disable_non_proxied_udp");
await view.webContents.loadURL(webUrl, { userAgent: userAgent });
// 同步语言设置到新创建的webview
this._syncLanguageToWebview(view);
// loadWithTimeout
} catch (err) {
logger.error("加载页面失败:", err);
@ -607,15 +611,14 @@ class WindowService {
) {
// 获取全局代理配置
const globalConfig = await app.sdb.selectOne("global_proxy_config");
if (globalConfig && globalConfig.proxyStatus === "true") {
if (globalConfig && globalConfig.proxyStatus === "true" && globalConfig.proxyIp && globalConfig.proxyPort) {
config = globalConfig;
logger.info(`[${partitionId}] Proxy: Use global proxy config`);
} else {
await view.webContents.session.setProxy({ mode: 'system' });
logger.info(`[${partitionId}] Proxy: 使用系统代理设置`);
return;
}
// else {
// await view.webContents.session.setProxy({ mode: 'system' });
// logger.info(`[${partitionId}] Proxy: 使用系统代理设置`);
// return;
// }
}
let proxyRules;
@ -856,6 +859,139 @@ class WindowService {
return false;
}
}
/**
* 测试代理连接是否可用(不修改现有会话配置)
* @param args { proxyType, proxyIp, proxyPort, userVerifyStatus, username, password }
*/
async testProxy(args, event) {
try {
const {
proxyType = "http",
proxyIp = "",
proxyPort = "",
userVerifyStatus = "false",
username = "",
password = "",
} = args || {};
if (!proxyIp || !proxyPort) {
return { status: false, message: "请填写完整的代理主机和端口" };
}
let server = `${String(proxyIp).trim()}:${String(proxyPort).trim()}`;
let proxyRules;
const needAuth = userVerifyStatus === "true" && username && password;
if (needAuth) {
server = `${String(username).trim()}:${String(password).trim()}@${server}`;
}
switch (proxyType) {
case "http":
case "https":
proxyRules = `http://${server}`;
break;
case "socks4":
proxyRules = needAuth
? await sockProxyRules(`socks4://${server}`)
: `socks4=socks4://${server}`;
break;
case "socks5":
proxyRules = needAuth
? await sockProxyRules(`socks5://${server}`)
: `socks5=socks5://${server}`;
break;
default:
return { status: false, message: `不支持的代理类型:${proxyType}` };
}
// 创建临时会话并设置代理
const partition = `proxy-test-${Date.now()}-${Math.random()}`;
const tmpSession = session.fromPartition(partition, { cache: false });
await tmpSession.setProxy({ mode: "fixed_servers", proxyRules });
const testUrls = [
"https://cp.cloudflare.com/generate_204",
"http://www.msftconnecttest.com/connecttest.txt",
"https://www.baidu.com/",
];
const tryRequest = (url) =>
new Promise((resolve) => {
const req = net.request({ url, session: tmpSession });
const timer = setTimeout(() => {
try { req.abort(); } catch (e) {}
resolve({ ok: false, code: "ETIMEOUT" });
}, 12000);
req.on("response", (res) => {
clearTimeout(timer);
// 2xx/3xx 认为成功
if (res.statusCode >= 200 && res.statusCode < 400) {
resolve({ ok: true, code: res.statusCode });
} else {
resolve({ ok: false, code: res.statusCode });
}
});
req.on("error", (err) => {
clearTimeout(timer);
resolve({ ok: false, code: err.code || err.message });
});
req.end();
});
let last;
for (const u of testUrls) {
last = await tryRequest(u);
if (last.ok) break;
}
// 复原临时会话代理
try { await tmpSession.setProxy({ mode: "system" }); } catch (e) {}
if (last && last.ok) {
return { status: true, message: "代理连接成功" };
}
return { status: false, message: `代理连接失败,错误码:${last?.code ?? "UNKNOWN"}` };
} catch (error) {
logger.error("测试代理失败:", error);
return { status: false, message: `测试代理失败:${error.message}` };
}
}
/**
* 同步语言设置到webview
* @param {BrowserView} view - webview实例
*/
_syncLanguageToWebview(view) {
if (view && !view.webContents.isDestroyed()) {
const currentLanguage = app.globalLanguage || 'zh';
// 等待页面加载完成后再设置语言
view.webContents.once('dom-ready', () => {
try {
view.webContents.executeJavaScript(`
// 设置全局语言变量
if (window.updateLanguage) {
window.updateLanguage('${currentLanguage}');
} else {
window.currentAppLanguage = '${currentLanguage}';
}
// 触发语言变更事件
window.dispatchEvent(new CustomEvent('languageChanged', {
detail: { language: '${currentLanguage}' }
}));
console.log('Language synced to webview:', '${currentLanguage}');
`);
} catch (error) {
logger.warn('Failed to sync language to webview:', error);
}
});
}
}
}
WindowService.toString = () => "[class WindowService]";

View File

@ -30,15 +30,23 @@ import { useMenuStore } from '@/stores/menuStore';
import UpdateProgress from "@/views/components/UpdateProgress.vue";
import router from "@/router"
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
const menuStore = useMenuStore();
const { locale } = useI18n();
const barTitle = ref('');
const appPlatform = ref('');
onMounted(async () => {
const loadingElement = document.getElementById('loadingPage');
if (loadingElement) {
loadingElement.remove();
}
// Initialize language from localStorage
const savedLanguage = localStorage.getItem('language') || 'zh';
locale.value = savedLanguage;
const res = await ipc.invoke(ipcApiRoute.getSystemInfo, {});
const { name, platform, version } = res;
barTitle.value = name + ` v${version}`;
@ -53,27 +61,30 @@ const winControl = async (action) => {
onMounted(async () => {
const res = await ipc.invoke(ipcApiRoute.getRouteList, {});
if (res.status&& res.data) {
const desiredOrder = ['youDao','deepl', 'deepseek','tengXun','huoShan','baidu', 'google','bing'];
if (res.status && res.data) {
// const desiredOrder = ['youDao','deepl', 'deepseek','tengXun','huoShan','baidu', 'google','bing'];
let orderedRoutes = []; // 存储排序后的结果
// let orderedRoutes = []; // 存储排序后的结果
// 使用 Map 提高查找效率,并处理过滤掉没有 name 的项
const routeMap = new Map();
res.data.filter(item => item.name).forEach(item => {
routeMap.set(item.name, item);
});
// // 使用 Map 提高查找效率,并处理过滤掉没有 name 的项
// const routeMap = new Map();
// res.data.filter(item => item.name).forEach(item => {
// routeMap.set(item.name, item);
// });
// 1. 按照 desiredOrder 添加路由
for (const name of desiredOrder) {
if (routeMap.has(name)) {
orderedRoutes.push(routeMap.get(name));
// 不需要从 Map 中删除,因为我们不再处理剩余路由
}
}
// // 1. 按照 desiredOrder 添加路由
// for (const name of desiredOrder) {
// if (routeMap.has(name)) {
// orderedRoutes.push(routeMap.get(name));
// // 不需要从 Map 中删除,因为我们不再处理剩余路由
// }
// }
// 2. 将最终的路由设置为只有 orderedRoutes不包含未指定顺序的路由
const finalRoutes = orderedRoutes;
// // 2. 将最终的路由设置为只有 orderedRoutes不包含未指定顺序的路由
// const finalRoutes = orderedRoutes;
// 根据enable决定是否显示
const finalRoutes = res.data.filter(item => item.enable == 1);
menuStore.setTranslationRoute(finalRoutes)
}

View File

@ -4,6 +4,8 @@
*/
const ipcApiRoute = {
captureScreen: 'controller/system/captureScreen',
getSystemInfo: 'controller/system/getBaseInfo',
login: 'controller/system/login',
logOut: 'controller/system/logOut',
@ -35,6 +37,8 @@ const ipcApiRoute = {
editProxyInfo: 'controller/window/editProxyInfo',
editGlobalProxyInfo: 'controller/window/editGlobalProxyInfo',
saveProxyInfo: 'controller/window/saveProxyInfo',
testProxy: 'controller/window/testProxy',
checkProxyStatus: 'controller/window/checkProxyStatus',
openSessionDevTools: 'controller/window/openSessionDevTools',
closeGlobalProxyPasswordVerification: 'controller/window/closeGlobalProxyPasswordVerification',
@ -53,8 +57,13 @@ const ipcApiRoute = {
getRouteList: 'controller/translate/getRouteList',
testRoute: 'controller/translate/testRoute',
translateText: 'controller/translate/translateText',
refreshTranslateRoutes: 'controller/translate/refreshTranslateRoutes',
createTranslateConfig: 'controller/translate/createTranslateConfig',
listNote: 'controller/translate/listNote',
createNote: 'controller/translate/createNote',
updateNote: 'controller/translate/updateNote',
deleteNote: 'controller/translate/deleteNote',
//联系人信息相关
getContactInfo: 'controller/contactInfo/getContactInfo',
@ -65,6 +74,9 @@ const ipcApiRoute = {
updateFollowRecord: 'controller/contactInfo/updateFollowRecord',
deleteFollowRecord: 'controller/contactInfo/deleteFollowRecord',
// 修改联系人备注,同步更新页面
updateContactRemark: 'controller/contactInfo/updateContactRemark',
//快捷回复相关
getGroups: 'controller/quickreply/getGroups',
getContentByGroupId: 'controller/quickreply/getContentByGroupId',
@ -76,6 +88,10 @@ const ipcApiRoute = {
editReply: 'controller/quickreply/editReply',
deleteReply: 'controller/quickreply/deleteReply',
deleteAllReply: 'controller/quickreply/deleteAllReply',
//翻译缓存相关
clearTranslateCache: 'controller/translate/clearTranslateCache',
refreshSessionTranslateButtons: 'controller/translate/refreshSessionTranslateButtons',
}
export {

View File

@ -1,6 +1,6 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, computed } from 'vue';
import { ref, computed, onMounted } from 'vue';
// 定义props
const props = defineProps({
@ -13,6 +13,13 @@ const props = defineProps({
const { locale } = useI18n();
const currentLanguage = ref(locale.value);
// 在组件挂载时读取保存的语言设置
onMounted(() => {
const savedLanguage = localStorage.getItem('language') || 'zh';
locale.value = savedLanguage;
currentLanguage.value = savedLanguage;
});
// 计算样式
const switchStyle = computed(() => ({
width: `${props.size}px`,
@ -23,9 +30,24 @@ const fontSize = computed(() => ({
fontSize: `${Math.max(props.size * 0.4375, 12)}px` // 保持字体大小为容器大小的0.4375倍最小12px
}));
const handleLanguageChange = (lang) => {
const handleLanguageChange = async (lang) => {
locale.value = lang;
currentLanguage.value = lang;
// 保存到localStorage使用统一的键名
localStorage.setItem('language', lang);
// 通知主进程语言变更
if (window.electronAPI && window.electronAPI.ipcRenderer) {
try {
await window.electronAPI.ipcRenderer.invoke('language-change', { language: lang });
console.log('Language change notified to main process:', lang);
} catch (error) {
console.warn('Failed to notify language change to main process:', error);
}
}
};
</script>

View File

@ -0,0 +1,9 @@
<template>
<svg t="1717130000000" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1234" width="24" height="24">
<path d="M832 128H192c-35.2 0-64 28.8-64 64v640c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64V192c0-35.2-28.8-64-64-64z m0 704H192V192h640v640z m-96-320H288v64h448v-64z m0-128H288v64h448v-64z" fill="#fff" p-id="1235"></path>
</svg>
</template>
<script setup>
// 便签图标
</script>

View File

@ -0,0 +1,11 @@
<template>
<svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor">
<rect x="3" y="5" width="18" height="14" rx="2" ry="2" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M8 3v4M16 3v4M4 11h16" stroke="currentColor" stroke-width="2" fill="none"/>
<circle cx="12" cy="16" r="2" fill="currentColor"/>
</svg>
</template>
<script setup>
// Screenshot/Capture Screen Icon
</script>

View File

@ -34,14 +34,14 @@ export default {
logoutConfirm: 'Are you sure you want to logout?',
copySuccess: 'Copied successfully!',
copyFailed: 'Copy failed!',
// Account Information
accountInfo: 'Account Information',
availableChars: 'Available Characters',
expirationTime: 'Expiration Time',
remainingDays: '{days} days remaining',
deviceId: 'Device ID',
// Core Features
coreFeatures: 'Core Features',
features: {
@ -62,7 +62,7 @@ export default {
desc: 'Local API translation for data privacy'
}
},
// Support and Help
supportAndHelp: 'Support & Help',
officialChannel: 'liangzi Channel',
@ -207,6 +207,7 @@ export default {
appId: 'App ID',
settings: 'Translation Settings',
clearCacheTitle: 'Confirm clearing translation cache for current session?',
clearCacheSuccess: 'Cache cleared successfully',
mode: 'Translation Mode',
localTranslate: 'Local',
cloudTranslate: 'Cloud',
@ -223,12 +224,45 @@ export default {
currentContact: 'Current Contact',
globalContact: 'Global Contact',
notFoundPersonalizedTranslation: 'No personalized translation settings found',
interceptChinese: 'Intercept Chinese',
interceptChinese: 'Enable send interception',
interceptLanguagesLabel: 'Intercept languages',
selectInterceptLanguages: 'Select languages to intercept',
translateHistory: 'Translation History Messages',
autoTranslateGroupMessage: 'Auto Translate Group Messages',
usePersonalConfig: 'Enable personal settings for this contact',
createPersonalizedTranslation: 'Create Personalized Translation',
chooseContactFirst: 'Please select a contact first',
noUserId: 'Unable to get user ID, please select a conversation or refresh the page'
noUserId: 'Unable to get user ID, please select a conversation or refresh the page',
historyTranslateRoute: 'History Message Translation Route',
// Error messages
errors: {
parameterError: 'Parameter error',
configNotExist: 'Configuration does not exist or window is closed',
querySuccess: 'Query successful',
missingRequiredParams: 'Missing required parameters, code and name are required',
codeExists: 'Code already exists, please use a different code',
writeDataFailed: 'Data write failed, please try again later',
addSuccess: 'Language configuration added successfully',
addFailed: 'Add failed, system error',
idRequired: 'ID cannot be empty',
deleteSuccess: 'Successfully deleted {count} records',
noDataFound: 'No data found',
paramsMissing: 'Parameters missing, please check ID, name and code.',
updateSuccess: 'Language configuration updated successfully',
updateFailed: 'Language configuration not found, update failed.',
systemError: 'Update failed, system error',
translateTextEmpty: 'Translation text or code cannot be empty!',
languageNotSupported: 'Current translation language not supported! Please check translation code configuration!',
contentOrSessionIdEmpty: 'Content or window session ID cannot be empty',
parameterIncomplete: 'Incomplete parameters: missing partitionId',
dataUpdateSuccess: 'Data updated successfully'
},
// Input placeholders
placeholders: {
translateToTarget: 'Translate to target language and send.',
translateTo: 'Translate to [{language}] and send.'
}
},
quickReply: {
title: 'Quick Reply',
@ -279,7 +313,8 @@ export default {
proxyConfig: 'Proxy Config',
devtools: 'Developer Tools',
expand: 'Expand',
collapse: 'Collapse',
collapse: 'Collapse',
screenshot: 'Screenshot',
},
quickReplyConfig: {
group: {
@ -302,7 +337,7 @@ export default {
remark: 'Remark',
enterRemark: 'Please enter remark',
type: 'Type',
content: 'Content',
content: 'Content',
enterContent: 'Please enter content',
text: 'Text',
image: 'Image',

View File

@ -3,9 +3,15 @@ import en from './en'
import zh from './zh'
import km from './km'
// 从localStorage获取保存的语言设置默认为中文
const getInitialLocale = () => {
const savedLanguage = localStorage.getItem('app-language');
return savedLanguage || 'zh';
};
const i18n = createI18n({
legacy: false,
locale: 'zh',
locale: getInitialLocale(),
fallbackLocale: 'en',
messages: {
en,
@ -14,4 +20,16 @@ const i18n = createI18n({
}
})
// 监听全局语言变更事件
if (window.electronAPI && window.electronAPI.ipcRenderer) {
window.electronAPI.ipcRenderer.on('global-language-changed', (event, data) => {
const { language } = data;
if (language && i18n.global.locale.value !== language) {
i18n.global.locale.value = language;
localStorage.setItem('language', language);
console.log('Language updated from main process:', language);
}
});
}
export default i18n

View File

@ -221,7 +221,8 @@ export default {
autoTranslateGroupMessage: 'បកប្រែសារក្រុមដោយស្វ័យប្រវត្តិ',
createPersonalizedTranslation: 'បង្កើតការបកប្រែសម្រាប់ទំនាក់ទំនងបច្ចុប្បន្ន',
chooseContactFirst: 'សូមជ្រើសរើសទំនាក់ទំនងជាមុន',
noUserId: 'មិនអាចទទួលបាន ID របស់អ្នកប្រើប្រាស់ទេ សូមជ្រើសរើសជជែកឬផ្ទាល់ខាតលើសពីទីនេះ'
noUserId: 'មិនអាចទទួលបាន ID របស់អ្នកប្រើប្រាស់ទេ សូមជ្រើសរើសជជែកឬផ្ទាល់ខាតលើសពីទីនេះ',
historyTranslateRoute: 'ផ្លូវបកប្រែសារទាំងអស់'
},
quickReply: {
title: 'ឆ្លើយតបរហ័ស',
@ -271,6 +272,7 @@ export default {
devtools: 'ឧបករណ៍អ្នកអភិវឌ្ឍ',
expand: 'ពង្រួម',
collapse: 'បង្រួម',
screenshot: 'រូបភាព',
},
quickReplyConfig: {
group: {

View File

@ -187,7 +187,10 @@ export default {
batchCount: '数量',
batchProxySettings: '批量代理设置',
batchProxySetSuccess: '批量代理设置成功',
selectSessionFirst: '请先勾选会话'
selectSessionFirst: '请先勾选会话',
testProxy: '测试代理',
proxyTestSuccess: '代理连接成功',
proxyTestFailed: '代理连接失败'
},
translate: {
google: '谷歌翻译',
@ -200,6 +203,7 @@ export default {
appId: '应用ID',
settings: '翻译设置',
clearCacheTitle: '确认清理当前会话历史翻译缓存吗?',
clearCacheSuccess: '清理缓存成功',
mode: '翻译模式',
localTranslate: '本地翻译',
cloudTranslate: '云端翻译',
@ -216,12 +220,44 @@ export default {
currentContact: '当前联系人',
globalContact: '全局联系人',
notFoundPersonalizedTranslation: '未找到个性化翻译设置',
interceptChinese: '拦截中文',
interceptChinese: '启用发送拦截',
interceptLanguagesLabel: '拦截语言',
selectInterceptLanguages: '请选择要拦截的语言',
translateHistory: '翻译历史消息',
autoTranslateGroupMessage: '自动翻译群消息',
usePersonalConfig: '启用当前联系人配置',
createPersonalizedTranslation: '新建个性化翻译',
chooseContactFirst: '请先选择联系人',
noUserId: '无法获取用户ID请选择会话或刷新页面'
noUserId: '无法获取用户ID请选择会话或刷新页面',
historyTranslateRoute: '翻译历史消息',
// 错误消息
errors: {
parameterError: '参数传递错误',
configNotExist: '配置不存在或窗口被关闭',
querySuccess: '查询成功',
missingRequiredParams: '缺少必要参数,编码和名称为必填项',
codeExists: '当前编码已存在,请使用不同的编码',
writeDataFailed: '数据写入失败,请稍后重试',
addSuccess: '语言配置添加成功',
addFailed: '添加失败,系统错误',
idRequired: 'id不能为空',
deleteSuccess: '成功删除{count}条数据',
noDataFound: '没有查询到这条数据',
paramsMissing: '参数缺失,请检查 ID、名称和code。',
updateSuccess: '语言配置更新成功',
updateFailed: '没有找到对应的语言配置,更新失败。',
systemError: '更新失败,系统错误',
translateTextEmpty: '翻译文本或编码不能为空!',
languageNotSupported: '不支持当前翻译语言!请检查翻译编码配置!',
contentOrSessionIdEmpty: '内容或窗口会话ID不能为空',
parameterIncomplete: '参数不完整缺少partitionId',
dataUpdateSuccess: '数据更新成功'
},
// 输入框提示
placeholders: {
translateToTarget: '翻译成目标语言后发送。',
translateTo: '翻译成[{language}]后发送。'
}
},
quickReply: {
title: '快捷回复',
@ -271,6 +307,7 @@ export default {
devtools: '开发者工具',
expand: '展开',
collapse: '折叠',
screenshot: '截图',
},
quickReplyConfig: {
group: {

View File

@ -55,6 +55,7 @@ export const useMenuStore = defineStore("platform", {
rightContent: "TranslateConfig",
rightFoldStatus: false,
userInfo: {},
currentUserId: null,
}),
actions: {
setUserInfo(user) {
@ -173,6 +174,9 @@ export const useMenuStore = defineStore("platform", {
// menu.children.push(...newItems);
}
},
setCurrentUserId(userId) {
this.currentUserId = userId;
},
},
getters: {
getCurrentMenu(state) {
@ -205,5 +209,8 @@ export const useMenuStore = defineStore("platform", {
getIsChildMenu: (state) => () => {
return state.isChildMenu;
},
getCurrentUserId: (state) => () => {
return state.currentUserId;
},
},
});

View File

@ -87,6 +87,9 @@
<div class="content-right">
<el-input :placeholder="t('session.enterPassword')" v-model="proxyInfo.password"></el-input>
</div>
<div class="footer-actions">
<el-button type="warning" @click="testGlobalProxy">测试代理</el-button>
</div>
</div>
</div>
</template>
@ -222,6 +225,27 @@ const getConfigInfo = async () => {
}
}
const testGlobalProxy = async () => {
const args = {
proxyType: proxyInfo.value.proxyType,
proxyIp: proxyInfo.value.proxyIp,
proxyPort: proxyInfo.value.proxyPort,
userVerifyStatus: proxyInfo.value.userVerifyStatus,
username: proxyInfo.value.username,
password: proxyInfo.value.password,
};
try {
const res = await ipc.invoke(ipcApiRoute.testProxy, args);
if (res.status) {
ElMessage.success(res.message || '代理连接成功');
} else {
ElMessage.error(res.message || '代理连接失败');
}
} catch (e) {
ElMessage.error('代理连接失败');
}
}
onMounted(() => {
getConfigInfo();
})

View File

@ -21,8 +21,17 @@
</div>
<!-- 搜索框区域 -->
<div class="menu-search-bar" style="padding: 8px 12px 0 12px; margin-bottom: 5px;">
<el-input v-model="menuSearchText" placeholder="搜索联系人/备注/用户名" clearable size="small"
prefix-icon="el-icon-search" />
<el-input
v-model="menuSearchText"
placeholder="搜索联系人/备注/用户名"
clearable
size="small"
@input="handleSearchInput"
@clear="handleSearchClear">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<!-- 开关切换普通区和置顶区 -->
<div class="menu-switch" v-if="isCollapse">
@ -212,6 +221,20 @@
<el-text>{{ t('menu.translateConfig') }}</el-text>
</div>
</div>
<div @click="toggleBottomMenu('Note')"
:class="activeMenu === 'Note' ? 'active-fold-menu-item' : 'default-fold-menu-item'">
<div class="menu-item-line" />
<div class="menu-item-img">
<el-avatar>
<el-icon size="25">
<component :is="NoteIcon" />
</el-icon>
</el-avatar>
</div>
<div v-if="isCollapse" class="menu-item-text">
<el-text>便签</el-text>
</div>
</div>
<!-- <div @click="toggleBottomMenu('MoreSetting')" :class="activeMenu === 'MoreSetting'?'active-fold-menu-item':'default-fold-menu-item'">-->
<!-- <div class="menu-item-line"/>-->
<!-- <div class="menu-item-img">-->
@ -257,6 +280,7 @@ import quickReply from '@/components/icons/QuickReplyIcon.vue'
import translateSetting from '@/components/icons/TranslateSettingIcon.vue'
import logo from '@/components/icons/LogoIcon.vue'
import google from "@/components/icons/GoogleIcon.vue"
import NoteIcon from '@/components/icons/NoteIcon.vue'
import {
ArrowLeft,
ArrowRight,
@ -264,7 +288,8 @@ import {
ArrowDown,
ArrowUp,
Fold,
Expand
Expand,
Search
} from '@element-plus/icons-vue'
import { ref, markRaw, watch, onMounted, onUnmounted, computed, nextTick } from 'vue'
@ -275,6 +300,7 @@ import { ipcApiRoute } from "@/api"
import { ipc } from "@/utils/ipcRenderer"
import router from "@/router"
import { useI18n } from 'vue-i18n'
import Note from "@/views/note/index.vue"
const { logger } = require('ee-core/log');
const menuStore = useMenuStore()
@ -326,8 +352,20 @@ const clearTimer = () => {
const menuSearchText = ref('')
// 搜索处理函数
const handleSearchInput = (value) => {
console.log('搜索输入:', value)
// 由于使用了computed这里不需要额外处理filteredChildren会自动响应
}
const handleSearchClear = () => {
console.log('清空搜索')
menuSearchText.value = ''
}
const filteredChildren = computed(() => (children, menuId) => {
let nMenus = children
let nMenus = children || []
if (currentArea.value === 'top') {
nMenus = nMenus.filter(child => child.isTop === true || child.isTop === 'true')
} else {
@ -515,6 +553,7 @@ const menuItems = [
{ id: 'TikTok', component: markRaw(SessionList) },
{ id: 'QuickReply', component: markRaw(QuickReply) },
{ id: 'TranslateConfig', component: markRaw(TranslateConfig) },
{ id: 'Note', component: markRaw(Note) },
{ id: 'CustomWeb', component: markRaw(SessionList) },
{ id: 'GlobalProxy', component: markRaw(GlobalProxy) },
{ id: 'Unknown', component: markRaw(Unknown) },
@ -732,7 +771,7 @@ const startSession = async (child) => {
/* 菜单项组 */
.menu-item-group {
height: calc(100% - 325px);
height: calc(100% - 325px - 40px);
:deep(.el-scrollbar__wrap) {
overflow-x: hidden;
@ -977,7 +1016,7 @@ const startSession = async (child) => {
/* 底部折叠区域 */
.fold-area {
height: 170px;
height: 230px;
width: 100%;
position: absolute;
bottom: 0;
@ -990,7 +1029,7 @@ const startSession = async (child) => {
/* 底部菜单 */
.fold-menu {
height: 120px;
height: 180px;
/* 通用底部菜单项样式 */
.default-fold-menu-item,

View File

@ -0,0 +1,197 @@
<template>
<div class="note-page">
<div class="note-actions">
<el-button type="primary" @click="openDialog()">新增便签</el-button>
</div>
<el-empty v-if="notes.length === 0" description="暂无便签" />
<div v-else class="note-list">
<el-card v-for="note in notes" :key="note.id" class="note-card" shadow="hover">
<div class="note-card-row">
<div class="note-card-main" @click="openDialog(note)">
<div class="note-card-content">{{ note.content }}</div>
<div class="note-card-meta">
<span class="note-time">创建时间{{ formatTime(note.created_at) }}</span>
</div>
</div>
<div class="note-card-actions">
<el-button type="danger" size="small" @click.stop="deleteNote(note.id)">删除</el-button>
</div>
</div>
</el-card>
</div>
<el-dialog v-model="dialogVisible" :title="editingNote ? '编辑便签' : '新增便签'" width="400px" @close="resetDialog">
<el-input
type="textarea"
v-model="noteContent"
:rows="6"
placeholder="请输入便签内容"
maxlength="500"
show-word-limit
/>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveNote">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ipc } from '@/utils/ipcRenderer'
import { ipcApiRoute } from '@/api'
import { useMenuStore } from '@/stores/menuStore'
const menuStore = useMenuStore()
const notes = ref([])
const dialogVisible = ref(false)
const noteContent = ref('')
const editingNote = ref(null)
function formatTime(ts) {
if (!ts) return ''
const d = new Date(ts)
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
}
async function loadNotes() {
// 优先云端
const userId = menuStore.userInfo.userId
if (!userId) {
notes.value = []
return
}
try {
const res = await ipc.invoke(ipcApiRoute.listNote, { userId })
if (res.status) {
notes.value = res.data || []
// 兜底本地存储
localStorage.setItem('my_notes_list', JSON.stringify(notes.value))
} else {
// 云端失败兜底本地
const data = localStorage.getItem('my_notes_list')
notes.value = data ? JSON.parse(data) : []
}
} catch {
const data = localStorage.getItem('my_notes_list')
notes.value = data ? JSON.parse(data) : []
}
}
function openDialog(note = null) {
if (note) {
editingNote.value = note
noteContent.value = note.content
} else {
editingNote.value = null
noteContent.value = ''
}
dialogVisible.value = true
}
async function saveNote() {
const content = noteContent.value.trim()
if (!content) {
ElMessage.error('内容不能为空')
return
}
const userId = menuStore.userInfo.userId
if (!userId) {
ElMessage.error('未获取到用户ID')
return
}
if (editingNote.value) {
// 编辑
const res = await ipc.invoke(ipcApiRoute.updateNote, { id: editingNote.value.id, content })
if (res.status) {
editingNote.value.content = content
editingNote.value.updatedAt = Date.now()
ElMessage.success('便签已更新')
await loadNotes()
} else {
ElMessage.error(res.message || '更新失败')
}
} else {
// 新增
const res = await ipc.invoke(ipcApiRoute.createNote, { userId, content })
if (res.status) {
ElMessage.success('便签已添加')
await loadNotes()
} else {
ElMessage.error(res.message || '添加失败')
}
}
dialogVisible.value = false
}
async function deleteNote(id) {
const res = await ipc.invoke(ipcApiRoute.deleteNote, { id })
if (res.status) {
ElMessage.success('便签已删除')
await loadNotes()
} else {
ElMessage.error(res.message || '删除失败')
}
}
function resetDialog() {
noteContent.value = ''
editingNote.value = null
}
onMounted(() => {
loadNotes()
})
</script>
<style scoped>
.note-page {
padding: 32px;
text-align: center;
}
.note-actions {
margin-bottom: 16px;
text-align: right;
}
.note-list {
width: 100%;
margin: 0 auto;
text-align: left;
display: flex;
flex-direction: column;
gap: 16px;
}
.note-card {
width: 100%;
box-sizing: border-box;
/* display: flex; */
transition: box-shadow 0.2s;
}
.note-card-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.note-card-main {
flex: 1;
cursor: pointer;
min-width: 0;
}
.note-card-content {
min-height: 40px;
word-break: break-all;
font-size: 15px;
margin-bottom: 8px;
}
.note-card-meta {
color: #888;
font-size: 12px;
margin-bottom: 4px;
}
.note-card-actions {
display: flex;
align-items: center;
margin-left: 16px;
}
</style>

View File

@ -76,11 +76,11 @@
v-model="proxyInfo.password"
:type="showPassword ? 'text' : 'password'"
>
<template #suffix>
<!-- <template #suffix>
<el-icon style="cursor:pointer;" @click="showPassword = !showPassword">
<component :is="showPassword ? Hide : View" />
</el-icon>
</template>
</template> -->
</el-input>
</div>
</div>
@ -114,6 +114,7 @@
</div>
</div>
<div style="text-align:right;margin-top:16px;">
<el-button type="warning" size="small" @click="testProxy" style="margin-right: 8px;">{{ t('session.testProxy') || '测试代理' }}</el-button>
<el-button type="primary" size="small" @click="handleSave">{{ t('common.save') }}</el-button>
</div>
</div>
@ -159,12 +160,48 @@ const getConfigInfo = async () => {
} catch (err) {}
}
const testProxy = async () => {
const args = {
proxyType: proxyInfo.value.proxyType,
proxyIp: proxyInfo.value.proxyIp,
proxyPort: proxyInfo.value.proxyPort,
userVerifyStatus: proxyInfo.value.userVerifyStatus,
username: proxyInfo.value.username,
password: proxyInfo.value.password,
};
try {
const res = await ipc.invoke(ipcApiRoute.testProxy, args);
if (res.status) {
ElMessage.success(res.message || '代理连接成功');
} else {
ElMessage.error(res.message || '代理连接失败');
}
} catch (e) {
ElMessage.error('代理连接失败');
}
}
const handleSave = async () => {
const args = { ...proxyInfo.value };
await ipc.invoke(ipcApiRoute.editProxyInfo, args);
// 刷新对应会话页面(修复:这里应传 partitionId 而不是 currentMenu
await ipc.invoke(ipcApiRoute.refreshSession, {
partitionId: menuStore.currentMenu
partitionId: menuStore.currentPartitionId
});
// 保存后自动刷新当前列表行的代理状态
try {
const pid = menuStore.currentPartitionId;
const res = await ipc.invoke(ipcApiRoute.checkProxyStatus, { partitionId: pid, platform: menuStore.platform });
const menu = menuStore.menus.find(m => m.id === menuStore.currentMenu);
const row = menu?.children?.find(c => c.partitionId === pid);
if (row) {
const should = res?.data?.shouldUseProxy;
const using = res?.data?.usingProxy;
row._proxyShould = typeof should === 'boolean' ? should : !!using;
row._proxyReachable = res?.data?.reachable === null ? null : !!res?.data?.reachable;
}
} catch (e) {}
if (args.defaultLanguage || args.timezone) {
ElMessage({
message: t('session.restartRequired'),
@ -179,6 +216,16 @@ const handleSave = async () => {
})
}
// 监听会话切换,重新获取配置信息
watch(
() => menuStore.currentPartitionId,
async (newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
await getConfigInfo()
}
}
);
onMounted(() => {
getConfigInfo()
})

View File

@ -4,147 +4,185 @@
<el-tab-pane :label="t('translate.currentContact')" name="current" />
<el-tab-pane :label="t('translate.globalContact')" name="global" />
</el-tabs>
<template v-if="Object.keys(configInfo).length > 0">
<div class="header-container">
<div class="header-title">
<el-text tag="b" size="large">{{ t('translate.settings') }}</el-text>
<el-popconfirm :title="t('translate.clearCacheTitle')" :confirm-button-text="t('common.confirm')"
:cancel-button-text="t('common.cancel')" @confirm="cleanMsgCache" trigger="hover" width="200">
<template #reference>
<el-icon class="header-icon">
<CleanIcon />
<el-skeleton v-if="loading" :rows="6" animated />
<template v-else>
<template v-if="!getConfigInfoIsNull">
<div class="header-container">
<div class="header-title">
<el-text tag="b" size="large">{{ t('translate.settings') }}</el-text>
<el-popconfirm :title="t('translate.clearCacheTitle')" :confirm-button-text="t('common.confirm')"
:cancel-button-text="t('common.cancel')" @confirm="cleanMsgCache" trigger="hover" width="200">
<template #reference>
<el-icon class="header-icon">
<CleanIcon />
</el-icon>
</template>
</el-popconfirm>
</div>
</div>
<div class="content-container-radio-group" v-if="showLocalTranslate">
<div class="content-left">
<el-text>{{ t('translate.mode') }}</el-text>
<el-tooltip effect="dark" placement="top">
<template #content>
<div style="max-width: 180px;">{{ t('translate.tooltipContent') }}</div>
</template>
<el-icon>
<QuestionFilled />
</el-icon>
</template>
</el-popconfirm>
</el-tooltip>
</div>
<div class="content-right">
<el-radio-group v-model="configInfo.mode" size="small">
<el-radio-button :label="t('translate.localTranslate')" value="local" />
<el-radio-button :label="t('translate.cloudTranslate')" value="cloud" />
</el-radio-group>
</div>
</div>
</div>
<div class="content-container-radio-group" v-if="showLocalTranslate">
<div class="content-left">
<el-text>{{ t('translate.mode') }}</el-text>
<el-tooltip effect="dark" placement="top">
<template #content>
<div style="max-width: 180px;">{{ t('translate.tooltipContent') }}</div>
</template>
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
<!-- 仅在当前联系人标签下显示是否启用个人配置 -->
<div class="content-container-radio" v-if="activeTab === 'current'">
<div class="content-left">
<el-text>{{ t('translate.usePersonalConfig') }}</el-text>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.usePersonalConfig" />
</div>
</div>
<div class="content-right">
<el-radio-group v-model="configInfo.mode" size="small">
<el-radio-button :label="t('translate.localTranslate')" value="local" />
<el-radio-button :label="t('translate.cloudTranslate')" value="cloud" />
</el-radio-group>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.route') }}</el-text>
</div>
<div class="content-right" style="display: flex; gap: 8px;">
<el-select v-model="configInfo.translateRoute" :placeholder="t('translate.selectRoute')" size="default"
style="flex: 1" @change="handleTranslateRouteChange">
<el-option v-for="item in menuStore.translationRoute" :key="item.name" :label="item[currentLanguageName]"
:value="item.name" />
</el-select>
<el-button size="default" type="primary" :icon="RefreshIcon" @click="refreshTranslateRoutes"
:loading="refreshLoading" title="刷新翻译路由">
</el-button>
</div>
</div>
</div>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.route') }}</el-text>
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.realTimeReceive') }}</el-text>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.receiveTranslateStatus" />
</div>
</div>
<div class="content-right">
<el-select v-model="configInfo.translateRoute" :placeholder="t('translate.selectRoute')" size="default"
style="width: 100%" @change="handleTranslateRouteChange">
<el-option v-for="item in menuStore.translationRoute" :key="item.name" :label="item[currentLanguageName]"
:value="item.name" />
</el-select>
<div class="content-container-radio" v-if="configInfo.receiveTranslateStatus === 'true'">
<div class="content-left">
<el-text>{{ t('translate.translateHistory') }}</el-text>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.translateHistory" />
</div>
</div>
</div>
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.realTimeReceive') }}</el-text>
<div class="content-container-radio" v-if="configInfo.receiveTranslateStatus === 'true'">
<div class="content-left">
<el-text>{{ t('translate.autoTranslateGroupMessage') }}</el-text>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.autoTranslateGroupMessage" />
</div>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.receiveTranslateStatus" />
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.historyTranslateRoute') }}</el-text>
</div>
<div class="content-right">
<el-select v-model="configInfo.historyTranslateRoute" :placeholder="t('translate.selectRoute')" size="default"
style="width: 100%" @change="handleHistoryTranslateRouteChange">
<el-option v-for="item in menuStore.translationRoute" :key="item.name" :label="item[currentLanguageName]"
:value="item.name" />
</el-select>
</div>
</div>
</div>
<div class="content-container-radio" v-if="configInfo.receiveTranslateStatus === 'true'">
<div class="content-left">
<el-text>{{ t('translate.translateHistory') }}</el-text>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.sourceLanguage') }}</el-text>
</div>
<div class="content-right">
<el-select v-model="configInfo.receiveSourceLanguage" :placeholder="t('translate.sourceLanguage')"
size="default" style="width: 100%">
<el-option :label="t('translate.autoDetect')" value="auto" />
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]"
:value="item.code" />
</el-select>
</div>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.translateHistory" />
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.targetLanguage') }}</el-text>
</div>
<div class="content-right">
<el-select v-model="configInfo.receiveTargetLanguage" placeholder="" size="default" style="width: 100%">
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]"
:value="item.code" />
</el-select>
</div>
</div>
</div>
<div class="content-container-radio" v-if="configInfo.receiveTranslateStatus === 'true'">
<div class="content-left">
<el-text>{{ t('translate.autoTranslateGroupMessage') }}</el-text>
<el-divider />
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.realTimeSend') }}</el-text>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.sendTranslateStatus" />
</div>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.autoTranslateGroupMessage" />
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.sourceLanguage') }}</el-text>
</div>
<div class="content-right">
<el-select v-model="configInfo.sendSourceLanguage" :placeholder="t('translate.sourceLanguage')" size="default"
style="width: 100%">
<el-option :label="t('translate.autoDetect')" value="auto" />
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]"
:value="item.code" />
</el-select>
</div>
</div>
</div>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.sourceLanguage') }}</el-text>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.targetLanguage') }}</el-text>
</div>
<div class="content-right">
<el-select v-model="configInfo.sendTargetLanguage" placeholder="" size="default" style="width: 100%">
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]"
:value="item.code" />
</el-select>
</div>
</div>
<div class="content-right">
<el-select v-model="configInfo.receiveSourceLanguage" :placeholder="t('translate.sourceLanguage')"
size="default" style="width: 100%">
<el-option :label="t('translate.autoDetect')" value="auto" />
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]"
:value="item.code" />
</el-select>
<div class="content-container-radio">
<div class="content-left intercept-left">
<el-text>{{ t('translate.interceptLanguagesLabel') }}</el-text>
</div>
<div class="content-right intercept-row">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.interceptChinese" />
<el-select
v-if="configInfo.interceptChinese === 'true'"
v-model="configInfo.interceptLanguages"
multiple
collapse-tags
:placeholder="t('translate.selectInterceptLanguages')"
class="intercept-lang-select"
@change="onInterceptLangChange"
>
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" :value="item.code" />
</el-select>
</div>
</div>
</div>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.targetLanguage') }}</el-text>
</div>
<div class="content-right">
<el-select v-model="configInfo.receiveTargetLanguage" placeholder="" size="default" style="width: 100%">
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]"
:value="item.code" />
</el-select>
</div>
</div>
<el-divider />
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.realTimeSend') }}</el-text>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.sendTranslateStatus" />
</div>
</div>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.sourceLanguage') }}</el-text>
</div>
<div class="content-right">
<el-select v-model="configInfo.sendSourceLanguage" :placeholder="t('translate.sourceLanguage')" size="default"
style="width: 100%">
<el-option :label="t('translate.autoDetect')" value="auto" />
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]"
:value="item.code" />
</el-select>
</div>
</div>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.targetLanguage') }}</el-text>
</div>
<div class="content-right">
<el-select v-model="configInfo.sendTargetLanguage" placeholder="" size="default" style="width: 100%">
<el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]"
:value="item.code" />
</el-select>
</div>
</div>
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.interceptChinese') }}</el-text>
</div>
<div class="content-right">
<el-switch :active-value="'true'" :inactive-value="'false'" size="default"
v-model="configInfo.interceptChinese" />
</div>
</div>
<el-divider />
<!-- <div class="content-container-radio">
<el-divider />
<!-- <div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.friendIndependent') }}</el-text>
</div>
@ -158,7 +196,7 @@
/>
</div>
</div> -->
<!-- <div class="content-container-radio">
<!-- <div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.preview') }}</el-text>
</div>
@ -171,16 +209,17 @@
/>
</div>
</div> -->
</template>
<template v-else-if="Object.keys(configInfo).length === 0 && currentUserId !== null">
<div class="no-personal-config">{{ t('translate.notFoundPersonalizedTranslation') }}</div>
<el-button type="primary" size="small" style="margin: 16px auto 0; display: block;"
@click="handleCreatePersonalConfig">
{{ t('translate.createPersonalizedTranslation') || '新建个性化翻译' }}
</el-button>
</template>
<template v-else>
<div class="no-personal-config">{{ t('translate.chooseContactFirst') }}</div>
</template>
<template v-else-if="getConfigInfoIsNull && menuStore.currentUserId !== null">
<div class="no-personal-config">{{ t('translate.notFoundPersonalizedTranslation') }}</div>
<el-button type="primary" size="small" style="margin: 16px auto 0; display: block;"
@click="handleCreatePersonalConfig">
{{ t('translate.createPersonalizedTranslation') || '新建个性化翻译' }}
</el-button>
</template>
<template v-else>
<div class="no-personal-config">{{ t('translate.chooseContactFirst') }}</div>
</template>
</template>
</div>
</template>
@ -192,7 +231,7 @@ import { ipcApiRoute } from "@/api";
import CleanIcon from "@/components/icons/CleanIcon.vue";
import { useMenuStore } from '@/stores/menuStore';
import { ElMessage } from "element-plus";
import { QuestionFilled } from "@element-plus/icons-vue";
import { QuestionFilled, Refresh } from "@element-plus/icons-vue";
import { useI18n } from 'vue-i18n';
import { config, nextTick } from "process";
@ -201,13 +240,23 @@ const configInfo = ref({})
const { t, locale } = useI18n();
const showLocalTranslate = ref(false);
const activeTab = ref('current');
const currentUserId = ref(null);
// const currentUserId = ref(null); // 移除本地currentUserId
const loading = ref(false); // 新增 loading 状态
const refreshLoading = ref(false); // 刷新按钮loading状态
const RefreshIcon = Refresh; // 刷新图标
// 修改计算属性名称使其更通用
const currentLanguageName = computed(() => {
return locale.value === 'zh' ? 'zhName' : 'enName';
});
const getConfigInfoIsNull = computed(() => {
return Object.keys(configInfo.value).length === 0;
});
// 加载/归一化阶段守卫,避免把初始化过程的值写回数据库
const isApplyingConfig = ref(false);
// watch(
// () => configInfo.value.friendTranslateStatus,
// async (newValue, oldValue) => {
@ -242,8 +291,11 @@ const propertiesToWatch = [
"chineseDetectionStatus",
"translatePreview",
"interceptChinese",
"interceptLanguages",
"translateHistory",
"autoTranslateGroupMessage",
"historyTranslateRoute",
"usePersonalConfig"
];
let watchers = []; // 存储所有字段的监听器
@ -255,6 +307,11 @@ const addWatchers = () => {
watch(
() => unref(configInfo.value[property]),
(newValue, oldValue) => {
if (isApplyingConfig.value) return; // 初始化阶段不写回
if (property === 'interceptLanguages') {
// 由 @change 显式持久化,避免初始化或平台切换时误写空
return;
}
if (newValue !== "" && newValue !== oldValue) {
handlePropertyChange(property, newValue);
}
@ -264,9 +321,18 @@ const addWatchers = () => {
}
// 自定义逻辑
const handlePropertyChange = async (property, value) => {
const args = { key: property, value: value, id: configInfo.value.id, partitionId: menuStore.currentPartitionId };
const id = configInfo.value?.id;
if (!id) return; // 未就绪不提交
const args = { key: property, value: value, id, partitionId: menuStore.currentPartitionId };
await ipc.invoke(ipcApiRoute.updateTranslateConfig, args);
}
// 显式提交拦截语言的更改
const onInterceptLangChange = async (val) => {
if (isApplyingConfig.value) return;
const list = Array.isArray(val) ? val.filter(Boolean) : [];
await handlePropertyChange('interceptLanguages', list);
}
// 移除所有字段的监听器
const removeWatchers = () => {
watchers.forEach((stopWatcher) => stopWatcher()); // 调用每个监听器的停止方法
@ -276,15 +342,19 @@ const removeWatchers = () => {
// 切换会话的回调
const onTranslateConfigUpdate = (event, args) => {
const { data } = args;
currentUserId.value = data.userId;
menuStore.setCurrentUserId(data.userId);
configInfo.value = {};
getConfigInfo()
}
watch(activeTab, (newVal) => {
getConfigInfo()
watch(activeTab, async (newVal) => {
configInfo.value = {}; // 切换tab时先清空内容
await getConfigInfo();
});
const getConfigInfo = async () => {
loading.value = true; // 开始加载
isApplyingConfig.value = true;
removeWatchers();
let type = activeTab.value;
@ -296,23 +366,36 @@ const getConfigInfo = async () => {
if (type === 'global') {
args.platform = menuStore.platform;
// args.partitionId = '';
} else {
args.userId = currentUserId.value;
// args.partitionId = menuStore.currentPartitionId;
args.userId = menuStore.currentUserId;
}
try {
const res = await ipc.invoke(ipcApiRoute.getTrsConfig, args);
if (res.status) {
Object.assign(configInfo.value, res.data); // 更新表单数据
const data = { ...res.data };
// 兼容旧数据:当前联系人缺少 usePersonalConfig 字段时默认启用个人配置
if (activeTab.value === 'current' && (data.usePersonalConfig === undefined || data.usePersonalConfig === null || data.usePersonalConfig === '')) {
data.usePersonalConfig = 'true';
}
// 兼容:逗号字符串 -> 数组
if (typeof data.interceptLanguages === 'string') {
data.interceptLanguages = data.interceptLanguages.length > 0
? data.interceptLanguages.split(',').map(s=>s.trim()).filter(Boolean)
: [];
}
if (!Array.isArray(data.interceptLanguages)) data.interceptLanguages = [];
configInfo.value = data;
} else {
configInfo.value = {};
}
} catch (err) {
configInfo.value = {};
console.log(err)
} finally {
isApplyingConfig.value = false;
addWatchers();
loading.value = false; // 加载结束
}
}
@ -338,8 +421,40 @@ onMounted(async () => {
})
// 清理缓存
const cleanMsgCache = () => {
// 实现清理逻辑
const cleanMsgCache = async () => {
try {
const res = await ipc.invoke(ipcApiRoute.clearTranslateCache, {
partitionId: menuStore.currentPartitionId,
platform: menuStore.currentMenu
});
if (res.status) {
ElMessage.success(res.message || '清理缓存成功');
// 刷新当前会话页面
await refreshCurrentSession();
} else {
ElMessage.error(res.message || '清理缓存失败');
}
} catch (error) {
console.error('清理缓存失败:', error);
ElMessage.error('清理缓存失败: ' + (error.message || '未知错误'));
}
}
// 刷新当前会话页面
const refreshCurrentSession = async () => {
try {
if (menuStore.currentPartitionId) {
// 通知当前会话页面刷新翻译按钮
await ipc.invoke(ipcApiRoute.refreshSessionTranslateButtons, {
partitionId: menuStore.currentPartitionId
});
console.log('当前会话页面已刷新');
}
} catch (error) {
console.warn('刷新当前会话页面失败:', error);
}
}
// 语言列表
@ -365,87 +480,145 @@ const getLanguageList = async () => {
watch(
() => configInfo.value.translateRoute, // 监听的数据源
async (newValue, oldValue) => {
if (!newValue || typeof newValue !== 'string') {
console.warn("watch(translateRoute): 无效的翻译平台值或值为空。");
// 如果平台值无效或为空,可以考虑清空 platformLanguageList
// 并且将所有语言设置重置为 "auto",因为没有可用的平台语言
configInfo.value.receiveSourceLanguage = "auto";
configInfo.value.receiveTargetLanguage = "auto";
return;
}
// 确保 languageList 已经加载,如果为空,可能需要等待数据加载或提示错误
if (languageList.value.length === 0) {
await getLanguageList();
if (languageList.value.length === 0) {
console.warn("watch(translateRoute): 翻译平台列表为空。");
isApplyingConfig.value = true;
try {
if (!newValue || typeof newValue !== 'string') {
console.warn("watch(translateRoute): 无效的翻译平台值或值为空。");
// 如果平台值无效或为空,可以考虑清空 platformLanguageList
// 并且将所有语言设置重置为 "auto",因为没有可用的平台语言
configInfo.value.receiveSourceLanguage = "auto";
configInfo.value.receiveTargetLanguage = "auto";
return;
}
}
// 筛选出当前平台支持的语言列表
const items = languageList.value.filter(item => item[newValue]);
platformLanguageList.value = items; // 更新平台支持的语言列表
// 检查是否有支持的语言
if (items.length > 0) {
const findLanguageInItems = (code) => items.find(item => item.code === code);
// 确保 languageList 已经加载,如果为空,可能需要等待数据加载或提示错误
if (languageList.value.length === 0) {
await getLanguageList();
// 处理接收Receive相关的语言设置
if (configInfo.value.receiveSourceLanguage !== "auto") {
const foundReceiveSourceLanguage = findLanguageInItems(configInfo.value.receiveSourceLanguage);
if (!foundReceiveSourceLanguage) {
configInfo.value.receiveSourceLanguage = "auto";
if (languageList.value.length === 0) {
console.warn("watch(translateRoute): 翻译平台列表为空。");
return;
}
}
const foundReceiveTargetLanguage = findLanguageInItems(configInfo.value.receiveTargetLanguage);
if (!foundReceiveTargetLanguage) {
configInfo.value.receiveTargetLanguage = items[0].code;
}
// 筛选出当前平台支持的语言列表
const items = languageList.value.filter(item => item[newValue]);
platformLanguageList.value = items; // 更新平台支持的语言列表
// 处理发送Send相关的语言设置
if (configInfo.value.sendSourceLanguage !== "auto") {
const foundSendSourceLanguage = findLanguageInItems(configInfo.value.sendSourceLanguage);
if (!foundSendSourceLanguage) {
configInfo.value.sendSourceLanguage = "auto";
// 检查是否有支持的语言
if (items.length > 0) {
const findLanguageInItems = (code) => items.find(item => item.code === code);
// 接收
if (configInfo.value.receiveSourceLanguage !== "auto") {
const ok = findLanguageInItems(configInfo.value.receiveSourceLanguage);
if (!ok) configInfo.value.receiveSourceLanguage = "auto";
}
if (!findLanguageInItems(configInfo.value.receiveTargetLanguage)) {
configInfo.value.receiveTargetLanguage = items[0].code;
}
}
const foundSendTargetLanguage = findLanguageInItems(configInfo.value.sendTargetLanguage);
if (!foundSendTargetLanguage) {
configInfo.value.sendTargetLanguage = items[0].code;
// 发送
if (configInfo.value.sendSourceLanguage !== "auto") {
const ok2 = findLanguageInItems(configInfo.value.sendSourceLanguage);
if (!ok2) configInfo.value.sendSourceLanguage = "auto";
}
if (!findLanguageInItems(configInfo.value.sendTargetLanguage)) {
configInfo.value.sendTargetLanguage = items[0].code;
}
// 兼容:拦截语言多选列表同样使用平台语言(尽量映射而不是直接丢弃)
if (Array.isArray(configInfo.value.interceptLanguages)) {
const toLower = (s) => String(s || '').toLowerCase();
const mapToSupported = (code) => {
const c = toLower(code);
// 1) 先精确匹配
let found = items.find(it => toLower(it.code) === c);
if (found) return found.code;
// 2) 无地域码 -> 用 startsWith 匹配(如 'zh' -> 'zh-CN'
found = items.find(it => toLower(it.code).startsWith(c));
if (found) return found.code;
// 3) 常见兜底
if (c.startsWith('zh')) {
found = items.find(it => toLower(it.code).startsWith('zh'));
if (found) return found.code;
}
if (c.startsWith('en')) {
found = items.find(it => toLower(it.code).startsWith('en'));
if (found) return found.code;
}
return null;
};
const mapped = configInfo.value.interceptLanguages
.map(mapToSupported)
.filter(Boolean);
configInfo.value.interceptLanguages = Array.from(new Set(mapped));
}
} else {
// 如果当前平台没有支持的语言配置,则全部默认设置为 "auto"
console.warn(`当前平台 (${newValue}) 没有配置任何支持的语言,将语言设置重置为 \"auto\"。`);
configInfo.value.receiveSourceLanguage = "auto";
configInfo.value.receiveTargetLanguage = "auto";
configInfo.value.sendSourceLanguage = "auto";
configInfo.value.sendTargetLanguage = "auto";
}
} else {
// 如果当前平台没有支持的语言配置,则全部默认设置为 "auto"
console.warn(`当前平台 (${newValue}) 没有配置任何支持的语言,将语言设置重置为 "auto"。`);
configInfo.value.receiveSourceLanguage = "auto";
configInfo.value.receiveTargetLanguage = "auto";
configInfo.value.sendSourceLanguage = "auto";
configInfo.value.sendTargetLanguage = "auto";
} finally {
isApplyingConfig.value = false;
}
},
{
immediate: true, // 立即执行一次监听器,处理组件挂载时的初始值
deep: false // 对于基本类型或浅层对象,不需要深度监听
}
);
}, { immediate: true })
const handleTranslateRouteChange = () => {
// 处理翻译路线变更的逻辑
console.log('翻译路线已变更:', configInfo.value.translateRoute);
}
const handleHistoryTranslateRouteChange = () => {
// 处理历史翻译路线变更的逻辑
console.log('历史翻译路线已变更:', configInfo.value.historyTranslateRoute);
}
const handleCreatePersonalConfig = async () => {
if (!currentUserId.value) {
if (!menuStore.currentUserId) {
ElMessage({ message: t('translate.noUserId') || '无法获取用户ID', type: 'error' });
return;
}
try {
await ipc.invoke(ipcApiRoute.createTranslateConfig, { userId: currentUserId.value });
await ipc.invoke(ipcApiRoute.createTranslateConfig, { userId: menuStore.currentUserId });
ElMessage.success(t('common.success') || '创建成功');
getConfigInfo();
await getConfigInfo();
if (configInfo.value && configInfo.value.id && (configInfo.value.usePersonalConfig === undefined || configInfo.value.usePersonalConfig === null || configInfo.value.usePersonalConfig === '')) {
await handlePropertyChange('usePersonalConfig', 'true');
configInfo.value.usePersonalConfig = 'true';
}
} catch (e) {
ElMessage.error((e && e.message) || t('common.failed') || '创建失败');
}
}
// 刷新翻译路由
const refreshTranslateRoutes = async () => {
refreshLoading.value = true;
try {
const res = await ipc.invoke(ipcApiRoute.refreshTranslateRoutes, {});
if (res.status) {
// 重新获取翻译路由列表
const routeRes = await ipc.invoke(ipcApiRoute.getRouteList, {});
if (routeRes.status && routeRes.data) {
const finalRoutes = routeRes.data.filter(item => item.enable == 1);
menuStore.setTranslationRoute(finalRoutes);
ElMessage.success('翻译路由刷新成功');
}
} else {
ElMessage.error(res.message || '刷新失败');
}
} catch (error) {
console.error('刷新翻译路由失败:', error);
ElMessage.error('刷新翻译路由失败: ' + (error.message || '未知错误'));
} finally {
refreshLoading.value = false;
}
}
// onUnmounted(() => {
// ipc.removeAllListeners('translate-config-update')
// })
@ -454,7 +627,7 @@ const handleCreatePersonalConfig = async () => {
<style scoped lang="less">
.translate-config {
width: 300px;
width: 320px;
height: 100%;
padding: 20px;
display: flex;
@ -538,6 +711,28 @@ const handleCreatePersonalConfig = async () => {
flex: 1;
align-items: center;
}
.intercept-left {
flex: 0 0 90px; /* 左侧固定更窄,为右侧选择框腾出空间 */
}
.intercept-row {
gap: 6px; /* 再缩小中间空隙 */
flex: 1 1 auto; /* 右侧区域最大化 */
}
.intercept-lang-select {
flex: 1;
width: 100%;
min-width: 0; /* 允许弹性收缩 */
}
:deep(.intercept-row .el-select) {
width: 100% !important; /* 强制选择器撑满右侧 */
}
:deep(.el-select__popper) {
min-width: 320px !important; /* 下拉浮层更宽,避免文字截断 */
}
:deep(.el-select__tags) {
max-width: 100%;
}
}
.content-container-radio-group {

View File

@ -220,8 +220,13 @@ const addWatchers = ()=> {
// 自定义逻辑
const handlePropertyChange = async (property, value) => {
if (userInfo && userInfo.value?.id){
const args = {key: property, value: value,id:userInfo.value.id};
const args = {key: property, value: value, id: userInfo.value.id};
await ipc.invoke(ipcApiRoute.updateContactInfo, args);
if (property === 'nickName') {
// 更新昵称时调用 updateContactRemark
const args = {partitionId: menuStore.currentPartitionId, nickName: value};
await ipc.invoke(ipcApiRoute.updateContactRemark, args);
}
}
}
// 移除所有字段的监听器
@ -252,6 +257,16 @@ const getUserInfo = async () => {
addWatchers()
}
}
// 监听会话切换,重新获取用户信息
watch(
() => menuStore.currentPartitionId,
async (newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
await getUserInfo()
}
}
);
onMounted(async () => {
await getUserInfo()
})

View File

@ -67,6 +67,7 @@ import UserInfo from "@/views/right-menu/UserInfo.vue";
import QuickReply from "@/views/right-menu/QuickReply.vue";
import ProxyConfig from "@/views/right-menu/ProxyConfig.vue";
import KefuContact from '@/views/right-menu/KefuContact.vue'
import ScreenShotIcon from '@/components/icons/ScreenShotIcon.vue';
import { useMenuStore } from '@/stores/menuStore';
import { ipcApiRoute } from "@/api";
import { ipc } from "@/utils/ipcRenderer";
@ -92,6 +93,7 @@ const menuItems = computed(() => [
{ icon: markRaw(User), action: 'UserInfo' },
{ icon: markRaw(QuickReplyIcon), action: 'QuickReply' },
{ icon: markRaw(ServerIcon), action: 'ProxyConfig' },
{ icon: markRaw(ScreenShotIcon), action: 'ScreenShot' },
])
// 响应式状态
@ -123,7 +125,11 @@ const processWidth = () => {
watch(() => isCollapsed.value, processWidth, { immediate: false });
// 菜单项选择处理
const selectMenuItem = (action) => {
const selectMenuItem = async (action) => {
if (action === 'ScreenShot') {
await ipc.invoke(ipcApiRoute.captureScreen);
return;
}
if (action === activeMenu.value) {
// 当点击相同的菜单项时,切换折叠状态
isCollapsed.value = !isCollapsed.value;
@ -207,6 +213,7 @@ const menuTooltips = {
UserInfo: t('rightMenu.userInfo'),
QuickReply: t('rightMenu.quickReply'),
ProxyConfig: t('rightMenu.proxyConfig'),
ScreenShot: t('rightMenu.screenshot') || '截图',
};
const tooltip = ref({ visible: false, text: '', x: 0, y: 0, direction: 'bottom' });

View File

@ -12,9 +12,49 @@ const { t } = useI18n();
const menuStore = useMenuStore();
// 存储选中ID的集合
const selectedRows = ref([])
// 搜索关键字(用于顶部搜索弹窗)
const searchQuery = ref('');
// 代理测试结果(当前弹窗): null | 'success' | 'error'
const proxyTestResult = ref(null);
// 过滤会话数据(受搜索影响)
const filteredTableData = computed(() => {
const menus = (menuStore.getCurrentChildren() || []);
const raw = typeof menus === 'function' ? menus() : menus; // 兼容 getter 写法
const list = Array.isArray(raw) ? raw : [];
const keyword = (searchQuery.value || '').trim().toLowerCase();
if (!keyword) return list;
return list.filter((row) => {
const nick = String(row?.nickName || '').toLowerCase();
const remarks = String(row?.remarks || '').toLowerCase();
const user = String(row?.userName || '').toLowerCase();
const webUrl = String(row?.webUrl || '').toLowerCase();
return (
nick.includes(keyword) ||
remarks.includes(keyword) ||
user.includes(keyword) ||
webUrl.includes(keyword)
);
});
});
const tableData = ref([]);
const getTableData = async () => {
tableData.value = await menuStore.getCurrentChildren();
// 获取每个会话的代理状态
const rows = Array.isArray(tableData.value) ? tableData.value : [];
for (const row of rows) {
try {
const res = await ipc.invoke(ipcApiRoute.checkProxyStatus, { partitionId: row.partitionId, platform: row.platform });
const should = res?.data?.shouldUseProxy;
const using = res?.data?.usingProxy;
row._proxyShould = typeof should === 'boolean' ? should : !!using;
row._proxyReachable = res?.data?.reachable === null ? null : !!res?.data?.reachable;
} catch (e) {
row._proxyShould = false;
row._proxyReachable = false;
}
}
}
onMounted(async () => {
await getTableData();
@ -57,6 +97,29 @@ const getProxyInfo = async (row) => {
}
proxyDialogVisible.value = true;
}
const testProxyConfig = async () => {
const args = {
proxyStatus: proxyForm.value.proxyStatus,
proxyType: proxyForm.value.proxyType,
proxyIp: proxyForm.value.proxyIp,
proxyPort: proxyForm.value.proxyPort,
userVerifyStatus: proxyForm.value.userVerifyStatus,
username: proxyForm.value.username,
password: proxyForm.value.password,
};
try {
const res = await ipc.invoke(ipcApiRoute.testProxy, args);
proxyTestResult.value = res.status ? 'success' : 'error';
if (res.status) {
ElMessage.success(res.message || t('session.proxyTestSuccess') || '代理连接成功');
} else {
ElMessage.error(res.message || t('session.proxyTestFailed') || '代理连接失败');
}
} catch (e) {
proxyTestResult.value = 'error';
ElMessage.error(t('session.proxyTestFailed') || '代理连接失败');
}
}
const saveProxyConfig = async () => {
// 构建参数对象
const args = {
@ -73,8 +136,23 @@ const saveProxyConfig = async () => {
};
// 调用主进程方法
const res = await ipc.invoke(ipcApiRoute.saveProxyInfo, args);
// 根据返回结果处理
if (res.status) {
// 保存成功后,后台会立即应用代理设置。我们顺便刷新当前行状态。
try {
const pid = proxyForm.value.partitionId || proxyForm.value.id?.partitionId || menuStore.currentPartitionId;
const r = await ipc.invoke(ipcApiRoute.checkProxyStatus, { partitionId: pid, platform: menuStore.platform });
const menu = menuStore.menus.find(m => m.id === menuStore.currentMenu);
const row = menu?.children?.find(c => c.partitionId === pid);
if (row) {
const should = r?.data?.shouldUseProxy;
const using = r?.data?.usingProxy;
row._proxyShould = typeof should === 'boolean' ? should : !!using;
row._proxyReachable = r?.data?.reachable === null ? null : !!r?.data?.reachable;
}
} catch (e) {}
ElMessage({
message: t('session.proxyConfigSaveSuccess'),
type: 'success',
@ -200,6 +278,7 @@ const startAll = async () => {
menuStore.addChildrenMenu(item);
menuStore.updateChildrenMenu(res.data);
}
}
}
const closeAll = async () => {
@ -383,7 +462,7 @@ const handleBatchProxySave = async () => {
</el-dialog>
<!--代理设置弹出层-->
<el-dialog v-model="proxyDialogVisible" :title="t('session.proxyConfig')" width="550">
<el-dialog v-model="proxyDialogVisible" :title="t('session.proxyConfig')" width="580">
<div class="proxy-config-dialog-form">
<!-- 代理开关-->
<div class="content-container-radio">
@ -447,14 +526,22 @@ const handleBatchProxySave = async () => {
<div class="content-right">
<el-input :placeholder="t('session.password')" v-model="proxyForm.password"
:type="showProxyPassword ? 'text' : 'password'">
<template #suffix>
<el-icon style="cursor:pointer;" @click="showProxyPassword = !showProxyPassword">
<component :is="showProxyPassword ? Hide : View" />
</el-icon>
</template>
</el-input>
</div>
</div>
<!-- 连接状态提示 -->
<div class="content-container-input" v-if="proxyTestResult">
<div class="content-left">
<el-text>连接状态</el-text>
</div>
<div class="content-right">
<el-tag :type="proxyTestResult === 'success' ? 'success' : 'danger'">
{{ proxyTestResult === 'success' ? '代理连接成功' : '代理连接失败' }}
</el-tag>
</div>
</div>
<!-- 时区-->
<div class="content-container-input">
<div class="content-left">
@ -490,6 +577,7 @@ const handleBatchProxySave = async () => {
<template #footer>
<span class="proxy-config-dialog-footer">
<el-button @click="proxyDialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="warning" @click="testProxyConfig" style="margin-right: 8px;">{{ t('session.testProxy') || '测试代理' }}</el-button>
<el-button type="primary" @click="saveProxyConfig">{{ t('common.confirm') }}</el-button>
</span>
</template>
@ -526,8 +614,18 @@ const handleBatchProxySave = async () => {
<el-button size="small" type="primary" :icon="Search" circle />
</template>
<div class="search-bth">
<el-input v-if="isCustomWeb" :placeholder="t('session.remarks')" />
<el-input v-else :placeholder="t('session.searchPlaceholder')" />
<el-input
v-if="isCustomWeb"
v-model="searchQuery"
:placeholder="t('session.remarks')"
clearable
/>
<el-input
v-else
v-model="searchQuery"
:placeholder="t('session.searchPlaceholder')"
clearable
/>
</div>
</el-popover>
<el-button size="small" type="primary" @click="batchProxyDialogVisible = true" style="margin-left: 8px;">
@ -537,7 +635,7 @@ const handleBatchProxySave = async () => {
</div>
<!--表格部分-->
<div class="table-data">
<el-table border height="100%" :data="tableData" @selection-change="handleSelectionChange" row-key="partitionId">
<el-table border height="100%" :data="filteredTableData" @selection-change="handleSelectionChange" row-key="partitionId">
<el-table-column type="selection" />
<el-table-column align="center" prop="createTime" :label="t('session.createTime')" />
<el-table-column align="center" :label="t('session.sessionRecord')" min-width="100">
@ -595,6 +693,18 @@ const handleBatchProxySave = async () => {
</div>
</template>
</el-table-column>
<el-table-column align="center" label="代理状态" width="140">
<template #default="{ row }">
<template v-if="row._proxyShould === false">
<el-tag type="info" size="small">未启用</el-tag>
</template>
<template v-else>
<el-tag v-if="row._proxyReachable === true" type="success" size="small">成功</el-tag>
<el-tag v-else-if="row._proxyReachable === false" type="danger" size="small">失败</el-tag>
<el-tag v-else type="warning" size="small">检测中</el-tag>
</template>
</template>
</el-table-column>
<el-table-column align="center" prop="isTop" label="置顶" width="70">
<template #default="{ row }">
<el-checkbox v-model="row.isTop" @change="toggleTop(row)" :true-label="'true'"

View File

@ -1,12 +1,12 @@
{
"name": "liangzi",
"version": "1.0.42",
"version": "1.0.55",
"description": "量子翻译",
"main": "./public/electron/main.js",
"scripts": {
"dev": "set BASE_URL=127.0.0.1:8000&& chcp 65001&&ee-bin dev",
"build": "set BASE_URL=haiapp.org&& npm run build-frontend && npm run build-electron && ee-bin encrypt",
"start": "ee-bin start",
"start": "chcp 65001 && set NODE_OPTIONS=--max_old_space_size=4096 && ee-bin start",
"dev-frontend": "set BASE_URL=127.0.0.1:8000&& ee-bin dev --serve=frontend",
"dev-electron": "set BASE_URL=127.0.0.1:8000&& ee-bin dev --serve=electron",
"build-frontend": "ee-bin build --cmds=frontend && ee-bin move --flag=frontend_dist",
@ -44,6 +44,7 @@
"better-sqlite3": "^11.5.0",
"crypto-js": "^4.2.0",
"ee-core": "^4.0.0",
"electron-screenshots": "^0.5.27",
"electron-session-proxy": "^1.0.2",
"electron-updater": "^6.3.8",
"input": "^1.0.1",

View File

@ -85,8 +85,8 @@
}
</style>
<script type="module" crossorigin src="./assets/index-jqD4rXg9.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-C-v6BnYe.css">
<script type="module" crossorigin src="./assets/index-DyAPP5Kn.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Dq73M9Ka.css">
</head>
<body style="padding: 0; margin: 0;">
<div id="loadingPage">