fix and add fea v1

This commit is contained in:
unknown
2025-08-16 21:47:55 +08:00
parent 85635c1b30
commit 0accb47375
25 changed files with 2064 additions and 132 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

@ -25,7 +25,7 @@ module.exports = () => {
},
titleBarStyle: 'hidden',
frame: true,
show: false,
show: true,
icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'),
},
logger: {

View File

@ -56,6 +56,9 @@ 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);
@ -74,6 +77,14 @@ class TranslateController {
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

@ -31,6 +31,22 @@ 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);
},
onMessageFromMain: (callback) => ipcRenderer.on('message-from-main', (event, message) => callback(message)),
});

View File

@ -237,6 +237,58 @@ 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);
@ -300,6 +352,216 @@ const ipcMainListener = () => {
ipcMain.handle("get-ws-base-url", async (event, args) => {
return { status: true, data: app.wsBaseUrl };
});
// 增强监控功能的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

@ -261,6 +261,12 @@ const initializeTableData = async () => {
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",
@ -273,6 +279,7 @@ const initializeTableData = async () => {
zhName: "百度翻译",
enName: "Baidu Translate",
otherArgs: `{"apiUrl": "https://fanyi.baidu.com/v2transapi","appId": "","apiKey": "","secretKey": ""}`,
enable: 1
}, {
name: "huoShan",
zhName: "火山翻译",
@ -309,7 +316,7 @@ const initializeTableData = async () => {
name: item.name,
zhName: item.zhName,
enName: item.enName,
// otherArgs: item.otherArgs,
otherArgs: item.otherArgs,
enable: item.enable
};
await app.sdb.insert("translate_route", args);

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

@ -551,6 +551,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,7 +623,8 @@ 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 = '';
@ -607,6 +642,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

@ -448,6 +448,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,7 +520,8 @@ 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 = '';
@ -502,6 +537,9 @@ const monitorMainNode = ()=> {
// 插入到消息元素右侧
msgSpan.appendChild(translateDiv);
// 检查是否已有翻译缓存并显示
await checkAndDisplayExistingTranslation(text, leftDiv, rightDiv);
const receiveTranslateStatus = trcConfig.receiveTranslateStatus;
if (receiveTranslateStatus === 'true') {
let text = msgSpan?.childNodes[0].textContent;

View File

@ -3,6 +3,38 @@
console.log("指纹屏蔽脚本执行");
// 全局语言设置
window.currentAppLanguage = 'zh'; // 默认中文
// 语言更新函数
window.updateLanguage = function(language) {
window.currentAppLanguage = language;
console.log('WhatsApp language updated to:', language);
// 更新所有需要国际化的文本
updateTranslateTexts();
};
// 监听语言变更事件
window.addEventListener('languageChanged', function(event) {
const { language } = event.detail;
window.updateLanguage(language);
});
// 更新翻译相关文本
function updateTranslateTexts() {
// 更新输入框placeholder
const placeholderDiv = document.querySelector('footer div[aria-owns="emoji-suggestion"][contenteditable="true"] + div div');
if (placeholderDiv && trcConfig && trcConfig.sendTargetLanguage) {
// 重新设置placeholder文本
setTimeout(() => {
if (window.replacePlaceholder) {
window.replacePlaceholder();
}
}, 100);
}
}
// 禁用webdriver
Object.defineProperty(navigator, "webdriver", {
get: () => false,
@ -45,6 +77,12 @@ const getCurrentUserId = (item) => {
}
return userNum;
};
// 增强的WhatsApp监控系统 - 监控头像变化、动态等
let lastAvatarUrl = "";
let lastStatusText = "";
let lastProfileName = "";
let lastAboutText = "";
const onlineStatusCheck = () => {
setInterval(async () => {
const element = localStorage["last-wid-md"];
@ -52,14 +90,39 @@ const onlineStatusCheck = () => {
const phoneNumber = element.split(":")[0].replace(/"/g, "");
const profilePicUrl = localStorage.WACachedProfilePicEURL;
const avatarUrl = profilePicUrl.replace(/^"|"$/g, "");
// const msgCount = await getNewMsgCount();
// 增强监控:检测头像变化
if (lastAvatarUrl && lastAvatarUrl !== avatarUrl) {
console.log("WhatsApp: 检测到头像变化", {
oldAvatar: lastAvatarUrl,
newAvatar: avatarUrl,
phoneNumber: phoneNumber
});
// 通知主进程头像变化
ipc.avatarChangeNotify({
platform: "WhatsApp",
phoneNumber: phoneNumber,
oldAvatarUrl: lastAvatarUrl,
newAvatarUrl: avatarUrl,
changeTime: new Date().toISOString()
});
}
lastAvatarUrl = avatarUrl;
// 增强监控:获取用户状态信息
const userStatus = await getUserStatusInfo();
const args = {
platform: "WhatsApp",
onlineStatus: true,
avatarUrl: avatarUrl,
nickName: "",
nickName: userStatus.profileName || "",
phoneNumber: phoneNumber,
userName: phoneNumber,
statusText: userStatus.statusText || "",
aboutText: userStatus.aboutText || "",
lastSeen: userStatus.lastSeen || "",
// msgCount: msgCount,
};
ipc.loginNotify(args);
@ -72,6 +135,9 @@ const onlineStatusCheck = () => {
nickName: "",
phoneNumber: "",
userName: "",
statusText: "",
aboutText: "",
lastSeen: "",
// msgCount: 0,
};
ipc.loginNotify(args);
@ -79,6 +145,119 @@ const onlineStatusCheck = () => {
}
}, 60000); // 每隔60秒调用一次
};
/**
* 获取用户状态信息(增强监控功能)
* 监控用户的个人资料变化、状态文本、关于信息等
*/
const getUserStatusInfo = async () => {
try {
const statusInfo = {
profileName: "",
statusText: "",
aboutText: "",
lastSeen: "",
isOnline: false
};
// 尝试从多个可能的位置获取用户信息
// 1. 从localStorage获取缓存的用户信息
try {
const cachedUserInfo = localStorage.getItem('WACachedUserInfo');
if (cachedUserInfo) {
const userInfo = JSON.parse(cachedUserInfo);
statusInfo.profileName = userInfo.name || userInfo.pushname || "";
statusInfo.aboutText = userInfo.status || "";
}
} catch (e) {
console.log("WhatsApp: 获取缓存用户信息失败", e);
}
// 2. 从DOM获取当前显示的用户信息
try {
// 获取用户名
const nameElements = document.querySelectorAll('[data-testid="conversation-info-header-chat-title"]');
if (nameElements.length > 0) {
statusInfo.profileName = nameElements[0].textContent.trim();
}
// 获取状态文本(在线状态、最后在线时间等)
const statusElements = document.querySelectorAll('[data-testid="conversation-info-header-chat-subtitle"]');
if (statusElements.length > 0) {
const statusText = statusElements[0].textContent.trim();
statusInfo.lastSeen = statusText;
statusInfo.isOnline = statusText.includes('在线') || statusText.includes('online');
}
} catch (e) {
console.log("WhatsApp: 从DOM获取用户信息失败", e);
}
// 3. 检测状态变化并记录
if (lastProfileName && lastProfileName !== statusInfo.profileName) {
console.log("WhatsApp: 检测到用户名变化", {
oldName: lastProfileName,
newName: statusInfo.profileName
});
// 通知主进程用户名变化
ipc.profileChangeNotify({
platform: "WhatsApp",
changeType: "profileName",
oldValue: lastProfileName,
newValue: statusInfo.profileName,
changeTime: new Date().toISOString()
});
}
if (lastStatusText && lastStatusText !== statusInfo.lastSeen) {
console.log("WhatsApp: 检测到状态变化", {
oldStatus: lastStatusText,
newStatus: statusInfo.lastSeen
});
// 通知主进程状态变化
ipc.statusChangeNotify({
platform: "WhatsApp",
changeType: "lastSeen",
oldValue: lastStatusText,
newValue: statusInfo.lastSeen,
changeTime: new Date().toISOString()
});
}
if (lastAboutText && lastAboutText !== statusInfo.aboutText) {
console.log("WhatsApp: 检测到关于信息变化", {
oldAbout: lastAboutText,
newAbout: statusInfo.aboutText
});
// 通知主进程关于信息变化
ipc.aboutChangeNotify({
platform: "WhatsApp",
changeType: "aboutText",
oldValue: lastAboutText,
newValue: statusInfo.aboutText,
changeTime: new Date().toISOString()
});
}
// 更新缓存的状态
lastProfileName = statusInfo.profileName;
lastStatusText = statusInfo.lastSeen;
lastAboutText = statusInfo.aboutText;
return statusInfo;
} catch (error) {
console.error("WhatsApp: 获取用户状态信息失败", error);
return {
profileName: "",
statusText: "",
aboutText: "",
lastSeen: "",
isOnline: false
};
}
};
const getNewMsgCount = () => {
try {
let newMsgCount = 0; // 累加消息数量
@ -368,16 +547,26 @@ const addTranslateListener = () => {
if (trcConfig && trcConfig.sendTargetLanguage) {
try {
const res = await ipc.getLanguage({ code: trcConfig.sendTargetLanguage });
if (res && res.status && res.data && res.data.zhName) {
placeholderDiv.innerHTML = `翻译成[${res.data.zhName}]后发送。`;
if (res && res.status && res.data) {
const currentLanguage = window.currentAppLanguage || 'zh';
const languageName = currentLanguage === 'zh' ? res.data.zhName : res.data.enName;
const translateText = currentLanguage === 'zh' ? '翻译成' : 'Translate to';
const afterText = currentLanguage === 'zh' ? '后发送。' : ' and send.';
placeholderDiv.innerHTML = `${translateText}[${languageName || res.data.zhName}]${afterText}`;
} else {
placeholderDiv.innerHTML = '翻译成目标语言后发送。';
const currentLanguage = window.currentAppLanguage || 'zh';
const defaultText = currentLanguage === 'zh' ? '翻译成目标语言后发送。' : 'Translate to target language and send.';
placeholderDiv.innerHTML = defaultText;
}
} catch (e) {
placeholderDiv.innerHTML = '翻译成目标语言后发送。';
const currentLanguage = window.currentAppLanguage || 'zh';
const defaultText = currentLanguage === 'zh' ? '翻译成目标语言后发送。' : 'Translate to target language and send.';
placeholderDiv.innerHTML = defaultText;
}
} else {
placeholderDiv.innerHTML = '翻译成目标语言后发送。';
const currentLanguage = window.currentAppLanguage || 'zh';
const defaultText = currentLanguage === 'zh' ? '翻译成目标语言后发送。' : 'Translate to target language and send.';
placeholderDiv.innerHTML = defaultText;
}
}
replacePlaceholder();
@ -407,7 +596,7 @@ const addTranslateListener = () => {
const handleInput = async () => {
// const flag = trcConfig.translatePreview;
const flag = false;
const textContent = getMsgText(editableDiv);
const textContent = await getMsgText(editableDiv);
const sendStatus = trcConfig.sendTranslateStatus;
if (textContent === "" || !textContent) {
styledTextarea.setContent("...");
@ -439,7 +628,7 @@ const addTranslateListener = () => {
const sendTranslateStatus = trcConfig.sendTranslateStatus === "true";
const sendPreview = trcConfig.translatePreview === "true";
const translateStatus = styledTextarea.getTranslateStatus();
const textContent = getMsgText(editableDiv);
const textContent = await getMsgText(editableDiv);
// 跳过空消息
if (!textContent || textContent.trim() === "") return;
@ -595,19 +784,186 @@ const sessionChange = async () => {
};
const debouncedSessionChange = debounce(sessionChange, 200);
const getMsgText = (node) => {
// 获取所有 span 元素
const spans = node.querySelectorAll("span");
// 用于存储每一行的内容
let content = [];
spans.forEach((span) => {
// 获取 span 的文本内容并去除前后空格
const text = span.textContent.trim();
// 如果内容为空,则添加空字符串,否则添加文本内容
content.push(text === "" ? "" : text);
const getMsgText = async (node) => {
// 首先尝试展开长消息并等待完成
const expanded = await expandLongMessage(node);
// 如果成功展开等待额外时间确保DOM完全更新
if (expanded) {
await new Promise(resolve => setTimeout(resolve, 500));
}
// 更精确的文本提取策略
let fullText = '';
// 策略1尝试获取完整的消息文本展开后
const messageTextElements = node.querySelectorAll('span[dir="ltr"], span[dir="rtl"], span[dir="auto"]');
if (messageTextElements.length > 0) {
const textParts = [];
messageTextElements.forEach((element) => {
const text = element.textContent.trim();
// 排除"查看更多"等按钮文本
if (text && !isExpandButtonText(text)) {
textParts.push(text);
}
});
fullText = textParts.join(' ');
}
// 策略2如果策略1没有获取到内容使用原有方法
if (!fullText) {
const spans = node.querySelectorAll("span");
let content = [];
spans.forEach((span) => {
const text = span.textContent.trim();
// 排除"查看更多"等按钮文本
if (text && !isExpandButtonText(text)) {
content.push(text === "" ? "" : text);
}
});
fullText = content.join("\n");
}
// 策略3如果仍然没有内容直接获取节点的文本内容
if (!fullText) {
fullText = node.textContent.trim();
// 移除"查看更多"等按钮文本
fullText = removeExpandButtonText(fullText);
}
return fullText;
};
// 检查是否是展开按钮的文本
const isExpandButtonText = (text) => {
const expandTexts = [
'more', '查看更多', '展开', 'show more', 'see more', 'read more',
'ver más', 'voir plus', 'mehr anzeigen', 'mostra di più',
'показать больше', 'もっと見る', '더 보기', 'ver mais'
];
const lowerText = text.toLowerCase().trim();
return expandTexts.some(expandText => lowerText.includes(expandText));
};
// 移除展开按钮文本
const removeExpandButtonText = (text) => {
const expandTexts = [
'more', '查看更多', '展开', 'show more', 'see more', 'read more',
'ver más', 'voir plus', 'mehr anzeigen', 'mostra di più',
'показать больше', 'もっと見る', '더 보기', 'ver mais'
];
let cleanText = text;
expandTexts.forEach(expandText => {
const regex = new RegExp(expandText, 'gi');
cleanText = cleanText.replace(regex, '');
});
return cleanText.trim();
};
// 展开长消息的函数
const expandLongMessage = async (node) => {
return new Promise((resolve) => {
try {
// 更精确的"查看更多"按钮文本匹配(支持多语言)
const expandTexts = [
'more', '查看更多', '展开', 'show more', 'see more', 'read more',
'ver más', 'voir plus', 'mehr anzeigen', 'mostra di più',
'показать больше', 'もっと見る', '더 보기', 'ver mais',
'показати більше', 'показать ещё', 'περισσότερα'
];
// 查找"查看更多"按钮的各种可能选择器
const expandSelectors = [
'span[role="button"]',
'div[role="button"]',
'button',
'[data-testid="msg-expand"]',
'.message-expand',
'span[tabindex="0"]',
'div[tabindex="0"]',
// 添加更多可能的选择器
'span[aria-label*="more"]',
'span[aria-label*="查看更多"]',
'div[aria-label*="more"]',
'div[aria-label*="查看更多"]'
];
let expandButton = null;
// 策略1通过选择器和文本内容查找
for (const selector of expandSelectors) {
const buttons = node.querySelectorAll(selector);
for (const button of buttons) {
const buttonText = button.textContent.toLowerCase().trim();
const ariaLabel = button.getAttribute('aria-label') || '';
// 检查按钮文本或aria-label是否包含展开相关的关键词
if (expandTexts.some(text =>
buttonText.includes(text) || ariaLabel.toLowerCase().includes(text)
)) {
expandButton = button;
break;
}
}
if (expandButton) break;
}
// 策略2在整个消息容器中查找展开按钮
if (!expandButton) {
// 扩大搜索范围到消息的父容器
const messageContainer = node.closest('[data-id]') || node.parentElement;
if (messageContainer) {
const allElements = messageContainer.querySelectorAll('span, div, a, button');
for (const element of allElements) {
const text = element.textContent.toLowerCase().trim();
const ariaLabel = element.getAttribute('aria-label') || '';
const hasExpandText = expandTexts.some(expandText =>
text.includes(expandText) || ariaLabel.toLowerCase().includes(expandText)
);
if (hasExpandText) {
const isClickable = element.style.cursor === 'pointer' ||
element.getAttribute('role') === 'button' ||
element.getAttribute('tabindex') === '0' ||
element.onclick !== null ||
element.tagName === 'BUTTON';
if (isClickable) {
expandButton = element;
break;
}
}
}
}
}
// 如果找到展开按钮,点击它
if (expandButton) {
console.log('找到展开按钮,正在展开长消息...', expandButton.textContent);
// 记录展开前的文本长度
const beforeText = node.textContent.length;
expandButton.click();
// 等待DOM更新并检查文本是否真的展开了
setTimeout(() => {
const afterText = node.textContent.length;
const expanded = afterText > beforeText;
console.log(`长消息展开${expanded ? '成功' : '失败'},文本长度:${beforeText} -> ${afterText}`);
resolve(expanded);
}, 500); // 增加等待时间确保DOM完全更新
} else {
console.log('未找到展开按钮,消息可能已经是完整的');
resolve(false);
}
} catch (error) {
console.warn('展开长消息时出错:', error);
resolve(false);
}
});
// 将内容数组拼接成一个字符串,用换行符分隔
return content.join("\n");
};
// 当前选中的会话DOM节点
@ -685,6 +1041,12 @@ const monitorMainNode = () => {
* 为所有消息添加翻译按钮
*/
const addTranslateButtonToAllMessages = async () => {
// 如果正在更新配置,跳过历史消息翻译
if (window.isConfigUpdating) {
console.log('配置更新中,跳过历史消息翻译');
return;
}
// 选择发送和接收的消息元素
const messageElements = document.querySelectorAll(
"div[role='row'] .message-out, div[role='row'] .message-in"
@ -727,11 +1089,76 @@ 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) {
// 找到了缓存的翻译结果,显示它
const translatedText = res.data;
if (translatedText.length > 300) {
// 创建折叠的翻译结果
const shortText = translatedText.substring(0, 300) + '...';
leftDiv.innerHTML = `
<div class="translate-content">
<div class="translate-short">${shortText}</div>
<div class="translate-full" style="display: none;">${translatedText}</div>
<span class="translate-toggle" style="color: #007bff; cursor: pointer; text-decoration: underline; font-size: 12px;">查看更多</span>
</div>
`;
// 添加展开/折叠功能
const toggleBtn = leftDiv.querySelector('.translate-toggle');
const shortDiv = leftDiv.querySelector('.translate-short');
const fullDiv = leftDiv.querySelector('.translate-full');
toggleBtn.addEventListener('click', () => {
if (fullDiv.style.display === 'none') {
shortDiv.style.display = 'none';
fullDiv.style.display = 'block';
toggleBtn.textContent = '收起';
} else {
shortDiv.style.display = 'block';
fullDiv.style.display = 'none';
toggleBtn.textContent = '查看更多';
}
});
} else {
leftDiv.textContent = translatedText;
}
leftDiv.style.color = "var(--WDS-content-action-emphasized)";
// 缓存存在时,刷新按钮正常显示
rightDiv.style.display = "";
} else {
// 没有缓存,显示默认状态
leftDiv.textContent = "";
rightDiv.style.display = "";
}
} catch (error) {
console.log('检查翻译缓存时出错:', error);
// 出错时显示默认状态
leftDiv.textContent = "";
rightDiv.style.display = "";
}
};
//为每条消息创建翻译按钮
const createTranslateButtonForMessage = async (msgSpan, msgDateTime) => {
let text = getMsgText(msgSpan);
let text = await getMsgText(msgSpan);
if (!text) return;
const translateDiv = document.createElement("div");
translateDiv.setAttribute('data-translate-btn', 'true'); // 添加标识
translateDiv.style.cssText = `
min-height: 20px;
justify-content: space-between;
@ -739,11 +1166,21 @@ const monitorMainNode = () => {
border-top:1px dashed var(--message-primary);
color:var(--secondary);
display: flex;`;
const leftDiv = document.createElement("div");
leftDiv.className = 'translate-result'; // 添加类名标识
// 继承原消息的样式属性,但保持翻译结果的基本布局
const originalStyles = window.getComputedStyle(msgSpan);
leftDiv.style.cssText = `
min-height: 20px;
font-size:12px;
color:var(--WDS-content-action-emphasized);`;
font-size: ${originalStyles.fontSize};
font-family: ${originalStyles.fontFamily};
line-height: ${originalStyles.lineHeight};
color: ${originalStyles.color};
flex: 1;
word-wrap: break-word;
white-space: pre-wrap;`;
const rightDiv = document.createElement("div");
rightDiv.style.cssText = `
cursor: pointer;
@ -754,7 +1191,7 @@ const monitorMainNode = () => {
`;
rightDiv.textContent = "🔄";
rightDiv.addEventListener("click", async (e) => {
let text = getMsgText(msgSpan);
let text = await getMsgText(msgSpan);
text = text.replace(/\n/g, "<br>");
leftDiv.style.color = "var(--WDS-content-action-emphasized)";
leftDiv.textContent = `翻译中...`;
@ -767,29 +1204,69 @@ const monitorMainNode = () => {
const from = trcConfig.receiveSourceLanguage;
const to = trcConfig.receiveTargetLanguage;
const mode = trcConfig.mode;
// 修复:刷新按钮不自动清理缓存,保留翻译历史
// 双击刷新按钮可强制清理缓存重新翻译
const isForceRefresh = event.detail === 2; // 检测双击
const res = await ipc.translateText({
route: route,
text: text,
from: from,
to: to,
refresh: "true",
refresh: isForceRefresh ? "true" : "false",
mode: mode,
});
if (res.status) {
leftDiv.innerHTML = res.data;
// 处理翻译结果的显示,保持格式和样式
const translatedText = res.data;
// 处理长翻译结果的折叠显示
if (translatedText.length > 300) {
// 创建折叠的翻译结果
const shortText = translatedText.substring(0, 300) + '...';
leftDiv.innerHTML = `
<div class="translate-content">
<div class="translate-short">${shortText}</div>
<div class="translate-full" style="display: none;">${translatedText}</div>
<span class="translate-toggle" style="color: #007bff; cursor: pointer; text-decoration: underline; font-size: 12px;">查看更多</span>
</div>
`;
// 添加展开/折叠功能
const toggleBtn = leftDiv.querySelector('.translate-toggle');
const shortDiv = leftDiv.querySelector('.translate-short');
const fullDiv = leftDiv.querySelector('.translate-full');
toggleBtn.addEventListener('click', () => {
if (fullDiv.style.display === 'none') {
shortDiv.style.display = 'none';
fullDiv.style.display = 'block';
toggleBtn.textContent = '收起';
} else {
shortDiv.style.display = 'block';
fullDiv.style.display = 'none';
toggleBtn.textContent = '查看更多';
}
});
} else {
leftDiv.textContent = translatedText;
}
rightDiv.style.display = "";
} else {
leftDiv.style.color = "red";
leftDiv.textContent = `${res.message}`;
leftDiv.style.color = "var(--system-message-text, #d73502)";
leftDiv.textContent = `翻译失败: ${res.message}`;
rightDiv.style.display = "";
}
});
// 组装结构
translateDiv.appendChild(leftDiv);
translateDiv.appendChild(rightDiv);
// 插入到消息元素右侧
msgSpan.appendChild(translateDiv);
// 检查是否已有翻译缓存并显示
await checkAndDisplayExistingTranslation(text, leftDiv, rightDiv);
const receiveTranslateStatus = trcConfig.receiveTranslateStatus;
if (receiveTranslateStatus === "true") {
@ -805,7 +1282,7 @@ const monitorMainNode = () => {
return;
}
let text = getMsgText(msgSpan);
let text = await getMsgText(msgSpan);
text = text.replace(/\n/g, "<br>");
leftDiv.style.color = "var(--WDS-content-action-emphasized)";
leftDiv.textContent = `翻译中...`;
@ -823,7 +1300,38 @@ const monitorMainNode = () => {
mode: mode,
});
if (res.status) {
leftDiv.innerHTML = res.data;
// 处理长翻译结果的折叠显示
const translatedText = res.data;
if (translatedText.length > 300) {
// 创建折叠的翻译结果
const shortText = translatedText.substring(0, 300) + '...';
leftDiv.innerHTML = `
<div class="translate-content">
<div class="translate-short">${shortText}</div>
<div class="translate-full" style="display: none;">${translatedText}</div>
<span class="translate-toggle" style="color: #007bff; cursor: pointer; text-decoration: underline; font-size: 12px;">查看更多</span>
</div>
`;
// 添加展开/折叠功能
const toggleBtn = leftDiv.querySelector('.translate-toggle');
const shortDiv = leftDiv.querySelector('.translate-short');
const fullDiv = leftDiv.querySelector('.translate-full');
toggleBtn.addEventListener('click', () => {
if (fullDiv.style.display === 'none') {
shortDiv.style.display = 'none';
fullDiv.style.display = 'block';
toggleBtn.textContent = '收起';
} else {
shortDiv.style.display = 'block';
fullDiv.style.display = 'none';
toggleBtn.textContent = '查看更多';
}
});
} else {
leftDiv.textContent = translatedText;
}
rightDiv.style.display = "";
} else {
leftDiv.style.color = "red";
@ -1011,6 +1519,7 @@ const initWhatsAppObserver = () => {
}
const observerCallback = (mutationsList, observer) => {
// 原有的未读消息监控
const currentTotalUnread = calculateUnreadFromChatList();
if (currentTotalUnread !== lastSentTotalUnread) {
lastSentTotalUnread = currentTotalUnread;
@ -1021,6 +1530,119 @@ const initWhatsAppObserver = () => {
ipc.messageCountUpdate(args);
console.log(`WhatsApp: 未读消息总数更新并发送: ${currentTotalUnread}`);
}
// 增强监控:检测动态更新和其他变化
for (let mutation of mutationsList) {
try {
// 监控状态更新Stories/动态)
if (mutation.type === 'childList') {
// 检测状态/动态相关的DOM变化
const addedNodes = Array.from(mutation.addedNodes);
addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 检测状态更新指示器
if (node.querySelector && (
node.querySelector('[data-testid="status-image"]') ||
node.querySelector('[data-testid="status-ring"]') ||
node.classList.contains('status-update') ||
node.querySelector('.status-update')
)) {
console.log("WhatsApp: 检测到状态/动态更新", {
nodeType: node.tagName,
className: node.className,
timestamp: new Date().toISOString()
});
// 通知主进程状态更新
ipc.statusUpdateNotify({
platform: "WhatsApp",
updateType: "status",
timestamp: new Date().toISOString(),
details: {
nodeType: node.tagName,
className: node.className
}
});
}
// 检测新消息或消息状态变化
if (node.querySelector && (
node.querySelector('[data-testid="msg-check"]') ||
node.querySelector('[data-testid="msg-dblcheck"]') ||
node.querySelector('[data-testid="msg-dblcheck-ack"]')
)) {
console.log("WhatsApp: 检测到消息状态变化", {
timestamp: new Date().toISOString()
});
}
// 检测用户在线状态变化
if (node.querySelector && (
node.querySelector('[data-testid="chat-subtitle"]') ||
node.textContent.includes('在线') ||
node.textContent.includes('online') ||
node.textContent.includes('最后在线') ||
node.textContent.includes('last seen')
)) {
console.log("WhatsApp: 检测到用户状态变化", {
content: node.textContent,
timestamp: new Date().toISOString()
});
}
}
});
}
// 监控属性变化(如头像、状态等)
if (mutation.type === 'attributes') {
const target = mutation.target;
// 监控头像变化
if (mutation.attributeName === 'src' &&
(target.classList.contains('avatar') ||
target.getAttribute('data-testid') === 'avatar' ||
target.closest('[data-testid="avatar"]'))) {
console.log("WhatsApp: 检测到头像DOM变化", {
oldSrc: mutation.oldValue,
newSrc: target.src,
timestamp: new Date().toISOString()
});
}
// 监控状态文本变化
if (mutation.attributeName === 'title' ||
mutation.attributeName === 'aria-label') {
console.log("WhatsApp: 检测到状态文本变化", {
attribute: mutation.attributeName,
oldValue: mutation.oldValue,
newValue: target.getAttribute(mutation.attributeName),
timestamp: new Date().toISOString()
});
}
}
// 监控文本内容变化
if (mutation.type === 'characterData') {
const target = mutation.target;
const parentElement = target.parentElement;
// 检查是否是状态相关的文本变化
if (parentElement && (
parentElement.getAttribute('data-testid') === 'chat-subtitle' ||
parentElement.classList.contains('status-text') ||
parentElement.closest('[data-testid="chat-subtitle"]')
)) {
console.log("WhatsApp: 检测到状态文本内容变化", {
oldData: mutation.oldValue,
newData: target.data,
timestamp: new Date().toISOString()
});
}
}
} catch (error) {
console.error("WhatsApp: 监控DOM变化时发生错误", error);
}
}
};
const observer = new MutationObserver(observerCallback);
@ -1048,6 +1670,8 @@ const initWhatsAppObserver = () => {
}, 1000)
};
// 监听whatsapp消息
// window.addEventListener('DOMContentLoaded',initWhatsAppObserver);
initWhatsAppObserver();
@ -1056,19 +1680,26 @@ onlineStatusCheck();
// 替换用户名
function replaceUsername(contactInfo = null) {
// 定位Whatsapp的用户名元素
const listItem = document.querySelectorAll("div#pane-side div[role='listitem'")
const listItem = document.querySelectorAll("div#pane-side div[role='listitem']")
listItem.forEach(item => {
const titleElement = item.querySelector('div[role="gridcell"] span')
const titleElement = item.querySelector('div[role="gridcell"] span[title]')
if (!titleElement) return;
const title = titleElement.innerText.replace(/\s*/g, "")
const originalTitle = titleElement.getAttribute('title') || title
// 遍历联系人信息,如果联系人信息中包含用户名,则替换用户名
if (contactInfo) {
contactInfo.forEach(info => {
const { userId, nickName } = info
if (title.includes(userId) && nickName) {
// 更精确的匹配逻辑
if ((title.includes(userId) || originalTitle.includes(userId)) && nickName && nickName.trim()) {
titleElement.innerText = nickName
titleElement.style.fontWeight = 'bold'
titleElement.style.color = 'red'
titleElement.style.color = '#1976d2' // 使用更好的颜色
// 保存原始信息到data属性
titleElement.setAttribute('data-original-name', originalTitle)
titleElement.setAttribute('data-remark', nickName)
}
})
}

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

@ -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 {
/**
@ -38,12 +103,29 @@ class TranslateService {
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: 'deepl' }, { userId: userId });
configById.translateRoute = 'deepl';
console.log('已修复用户配置translateRoute为deepl');
}
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: 'deepl' }, { platform: platform });
config.translateRoute = 'deepl';
console.log('已修复平台配置translateRoute为deepl');
}
return { status: true, message: '查询成功', data: config }
} else {
console.log('未找到配置,将创建新配置');
return { status: false, message: '查询失败,配置不存在' }
}
}
@ -53,6 +135,14 @@ class TranslateService {
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: '查询失败,配置不存在' }
@ -97,6 +187,16 @@ class TranslateService {
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: 'deepl' }, { platform: platform });
data.translateRoute = 'deepl';
console.log('已修复translateRoute为deepl');
}
return { status: true, message: '查询成功2', data: data }
}
// if (partitionId) {
@ -245,7 +345,7 @@ class TranslateService {
if (!status?.trim() && !partitionId?.trim()) {
return {
status: false,
message: '参数传递错误'
message: getI18nText('parameterError')
}
}
//获取窗口对象并查询是否有userid
@ -259,29 +359,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 };
@ -300,23 +400,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;
@ -412,6 +512,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 = {
@ -422,13 +576,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') {
@ -436,38 +596,111 @@ 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('');
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)
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 };
}
@ -478,7 +711,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);
`)
}
}
@ -698,6 +938,106 @@ class TranslateService {
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: "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: "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 {
@ -761,6 +1101,117 @@ class TranslateService {
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

@ -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,39 @@ class WindowService {
return false;
}
}
/**
* 同步语言设置到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}`;

View File

@ -55,6 +55,7 @@ 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',
@ -85,6 +86,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

@ -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',
@ -229,7 +230,35 @@ export default {
createPersonalizedTranslation: 'Create Personalized Translation',
chooseContactFirst: 'Please select a contact first',
noUserId: 'Unable to get user ID, please select a conversation or refresh the page',
historyTranslateRoute: 'History Message Translation Route'
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',

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

@ -200,6 +200,7 @@ export default {
appId: '应用ID',
settings: '翻译设置',
clearCacheTitle: '确认清理当前会话历史翻译缓存吗?',
clearCacheSuccess: '清理缓存成功',
mode: '翻译模式',
localTranslate: '本地翻译',
cloudTranslate: '云端翻译',
@ -222,7 +223,35 @@ export default {
createPersonalizedTranslation: '新建个性化翻译',
chooseContactFirst: '请先选择联系人',
noUserId: '无法获取用户ID请选择会话或刷新页面',
historyTranslateRoute: '翻译历史消息'
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: '快捷回复',

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">
@ -279,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'
@ -342,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 {

View File

@ -179,6 +179,16 @@ const handleSave = async () => {
})
}
// 监听会话切换,重新获取配置信息
watch(
() => menuStore.currentPartitionId,
async (newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
await getConfigInfo()
}
}
);
onMounted(() => {
getConfigInfo()
})

View File

@ -43,12 +43,15 @@
<div class="content-left">
<el-text>{{ t('translate.route') }}</el-text>
</div>
<div class="content-right">
<div class="content-right" style="display: flex; gap: 8px;">
<el-select v-model="configInfo.translateRoute" :placeholder="t('translate.selectRoute')" size="default"
style="width: 100%" @change="handleTranslateRouteChange">
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 class="content-container-radio">
@ -207,7 +210,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";
@ -218,6 +221,8 @@ const showLocalTranslate = ref(false);
const activeTab = ref('current');
// const currentUserId = ref(null); // 移除本地currentUserId
const loading = ref(false); // 新增 loading 状态
const refreshLoading = ref(false); // 刷新按钮loading状态
const RefreshIcon = Refresh; // 刷新图标
// 修改计算属性名称使其更通用
const currentLanguageName = computed(() => {
@ -365,8 +370,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);
}
}
// 语言列表
@ -483,6 +520,30 @@ const handleCreatePersonalConfig = async () => {
}
}
// 刷新翻译路由
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')
// })

View File

@ -257,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

@ -1,11 +1,11 @@
<!DOCTYPE html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0" />
<title></title>
<!-- 优化vue渲染未完成之前先加一个css动画 -->
<!DOCTYPE html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0" />
<title></title>
<!-- 优化vue渲染未完成之前先加一个css动画 -->
<style>
#loadingPage {
background-color: #dedede;
@ -84,24 +84,24 @@
}
}
</style>
<script type="module" crossorigin src="./assets/index-jqD4rXg9.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-C-v6BnYe.css">
</head>
<body style="padding: 0; margin: 0;">
<div id="loadingPage">
<div class='base'>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
</div>
</div>
<div id="app"></div>
</body>
</html>
</style>
<script type="module" crossorigin src="./assets/index-UhLAJDIM.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-LGRos5i2.css">
</head>
<body style="padding: 0; margin: 0;">
<div id="loadingPage">
<div class='base'>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
</div>
</div>
<div id="app"></div>
</body>