add trans hack

This commit is contained in:
unknown
2025-08-25 09:29:31 +08:00
parent e6a446e39f
commit 343a766312
15 changed files with 623 additions and 128 deletions

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

@ -47,6 +47,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
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

@ -295,7 +295,7 @@ const ipcMainListener = () => {
const { platform, msgCount } = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
// 校验主窗口是否存在且未被销毁
if (!mainWin || mainWin.isDestroyed()) {
return; // 主窗口不存在时终止处理
@ -353,6 +353,27 @@ const ipcMainListener = () => {
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处理器
// 头像变化通知

View File

@ -72,9 +72,11 @@ const initializeDatabase = async () => {
chineseDetectionStatus: "TEXT",
translatePreview: "TEXT",
interceptChinese: "TEXT",
interceptLanguages: "TEXT",
translateHistory: "TEXT",
autoTranslateGroupMessage: "TEXT",
historyTranslateRoute: "TEXT",
usePersonalConfig: 'TEXT DEFAULT "true"',
},
constraints: [],
},

View File

@ -152,11 +152,45 @@ 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 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);
const targets = list.length ? list : ['zh'];
const found = detectLanguageSet(text);
return targets.some(code=>found.has(code));
};
const sendMsg = ()=> {
let sendButton = document.querySelectorAll('button.Button.send.main-button.default.secondary.round.click-allowed')[0]
if (sendButton) {
@ -178,7 +212,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);
}
@ -372,7 +407,8 @@ const addTranslateListener = () => {
styledTextarea.setContent('...');
},500)
}else {
styledTextarea.setContent(res.message);
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
styledTextarea.setContent('...');
isProcessing = false;
return;
}
@ -630,7 +666,8 @@ const monitorMainNode = ()=> {
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
leftDiv.textContent = '翻译失败';
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
rightDiv.style.display = '';
}
});

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 {
@ -527,7 +529,8 @@ const monitorMainNode = ()=> {
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
leftDiv.textContent = '翻译失败';
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
rightDiv.style.display = '';
}
});
@ -558,7 +561,8 @@ const monitorMainNode = ()=> {
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
leftDiv.textContent = '翻译失败';
alert('翻译失败,未发送。请稍后重试或更换翻译通道。');
rightDiv.style.display = '';
}
}

View File

@ -303,15 +303,98 @@ 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);
// 粗略英语:仅 ASCII 可打印字符,且不含其他脚本
const isLikelyEnglish = (text) => {
if (!text) return false;
// 若包含其它明显脚本则不判为英文
if (containsChinese(text) || containsJapanese(text) || containsKorean(text) || containsArabic(text) || containsRussian(text)) {
return false;
}
// 只要包含英文字母即可(允许存在表情、全角标点等非 ASCII
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');
// 其它拉丁语种fr/es/pt等可根据重音字符大致判断
if (/[àâäæçéèêëîïôœùûüÿñ¡¿]/i.test(text)) {
// 常见重音组合,合并处理为拉丁扩展族
// 为简单起见将其归到 fr/es/pt 共同集合
if (/ñ|¡|¿|á|é|í|ó|ú/i.test(text)) set.add('es');
if (/ç|à|â|ä|é|è|ê|ë|î|ï|ô|œ|ù|û|ü|ÿ/i.test(text)) set.add('fr');
if (/ã|õ|á|é|í|ó|ú/i.test(text)) set.add('pt');
}
return set;
};
const normalizeLang = (code) => {
const s = String(code || '').toLowerCase();
if (!s) return '';
if (s.startsWith('zh')) return 'zh';
return s.split('-')[0];
};
const parseInterceptLanguages = (val) => {
if (!val) return [];
if (Array.isArray(val)) return val.map(normalizeLang).filter(Boolean);
// 以逗号存储
return String(val)
.split(',')
.map((s) => s.trim())
.map(normalizeLang)
.filter(Boolean);
};
const shouldInterceptByLanguages = async (text) => {
if (!trcConfig || trcConfig.interceptChinese !== 'true') return false;
const list = parseInterceptLanguages(trcConfig.interceptLanguages);
const targets = list.length ? list : ['zh'];
try {
const res = await ipc.detectLanguage({ text });
if (res && res.code === 2000 && res.data) {
const lang = String(res.data.lang || 'und').toLowerCase();
if (lang !== 'und') {
return targets.includes(lang);
}
}
} catch (e) {}
// 回退到本地粗略判断
const found = detectLanguageSet(text);
return targets.some((code) => found.has(code));
};
// 错误消息检测:拦截把错误当消息发送
const isErrorText = (text) => {
if (!text) return false;
const s = String(text);
const patterns = [
'翻译失败', '请求失败', 'Client Error', 'Unauthorized',
'HTTPConnectionPool', 'timeout', '超时', '错误', 'Error',
];
return patterns.some((p) => s.includes(p));
};
const sendMsg = async () => {
let sendButton = getSendBtn();
// 新增:拦截中文逻辑
if (trcConfig.interceptChinese === "true") {
if (trcConfig.interceptChinese === "true" || true) {
// 从 footer 开始查找输入框
const footer = document.querySelector("footer._ak1i");
if (!footer) {
@ -322,7 +405,22 @@ const sendMsg = () => {
const richTextInput = footer.querySelector(
'.lexical-rich-text-input div[contenteditable="true"]'
);
if (containsChinese(richTextInput.textContent)) {
let content = richTextInput?.textContent?.trim() || '';
if (!content) {
const plainInput = document.querySelector('footer div[aria-owns="emoji-suggestion"][contenteditable="true"]');
content = plainInput?.textContent?.trim() || '';
}
// 1) 多语言拦截(兼容旧:仅中文)
try {
if (trcConfig.interceptChinese === "true" && (await shouldInterceptByLanguages(content))) {
alert("检测到被拦截语言内容,已阻止发送");
return;
}
} catch (e) {}
// 2) 错误内容拦截(翻译失败/接口错误等)
if (isErrorText(content)) {
alert("翻译失败,未发送。请稍后重试或更换翻译通道。");
return;
}
}
@ -374,7 +472,9 @@ 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);
}
@ -648,23 +748,16 @@ const addTranslateListener = () => {
//获取whatsapp输入框内容
const whatsappContent = async () => {
let element = document.evaluate(
'//*[@id="main"]/footer/div[1]/div[2]/div/span/div/div[2]/div/div[3]',
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
if (element) {
return element.innerText
.split("\n")
.map((line) => line.trim()) // 去掉首尾空格
.filter((line) => line.length) // 去掉空行
.join("\n");
// 优先:富文本输入框
const footer = document.querySelector("footer._ak1i");
const rich = footer?.querySelector('.lexical-rich-text-input div[contenteditable="true"]');
let text = rich?.textContent?.trim() || '';
if (!text) {
// 退化:普通 contenteditable
const plain = document.querySelector('footer div[aria-owns="emoji-suggestion"][contenteditable="true"]');
text = plain?.textContent?.trim() || '';
}
return "";
return text;
};
//判断是否需要翻译后发送
async function handleSendTranslateFlow() {
@ -676,10 +769,16 @@ const addTranslateListener = () => {
const sendTranslateStatus = trcConfig.sendTranslateStatus === "true";
const sendPreview = trcConfig.translatePreview === "true";
const translateStatus = styledTextarea.getTranslateStatus();
const textContent = await whatsappContent();
const textContent = (await whatsappContent())?.trim();
// 拦截:在翻译/发送前就判断原始输入语言
if (trcConfig.interceptChinese === "true" && (await shouldInterceptByLanguages(textContent))) {
alert("检测到被拦截语言内容,已阻止发送");
return;
}
// 跳过空消息
if (!textContent || textContent.trim() === "") return;
if (!textContent) return;
styledTextarea.setIsProcessing(true);
updateSendButtonState(true);
@ -1381,8 +1480,10 @@ const monitorMainNode = () => {
// 插入到消息元素右侧
msgSpan.appendChild(translateDiv);
// 检查是否已有翻译缓存并显示
await checkAndDisplayExistingTranslation(text, leftDiv, rightDiv);
// 检查是否已有翻译缓存并显示(仅在允许显示时)
if (trcConfig && (trcConfig.receiveTranslateStatus === "true" || trcConfig.translateHistory === "true")) {
await checkAndDisplayExistingTranslation(text, leftDiv, rightDiv);
}
const receiveTranslateStatus = trcConfig.receiveTranslateStatus;
if (receiveTranslateStatus === "true") {

View File

@ -111,6 +111,17 @@ class TranslateService {
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 })
@ -180,6 +191,7 @@ class TranslateService {
chineseDetectionStatus: "false",
translatePreview: "false",
interceptChinese: "false",
interceptLanguages: "",
translateHistory: "false",
autoTranslateGroupMessage: "false",
historyTranslateRoute: defaultHistoryTranslateRoute
@ -316,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)
// 执行更新
@ -931,9 +952,11 @@ class TranslateService {
chineseDetectionStatus: "false",
translatePreview: "false",
interceptChinese: "false",
interceptLanguages: "",
translateHistory: "false",
autoTranslateGroupMessage: "false",
historyTranslateRoute: defaultHistoryTranslateRoute
historyTranslateRoute: defaultHistoryTranslateRoute,
usePersonalConfig: 'true'
}
await app.sdb.insert('translate_config', initialData);
}
@ -957,9 +980,6 @@ class TranslateService {
enName: "Youdao Translate",
otherArgs: `{"apiUrl": "https://openapi.youdao.com/api","apiKey": "","secretKey": ""}`,
enable: 1
},
otherArgs: `{"apiUrl": "https://openapi.youdao.com/api","appId": "","apiKey": ""}`,
enable: 1
},
{
name: "tengXun",

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 {
@ -860,6 +860,106 @@ class WindowService {
}
}
/**
* 测试代理连接是否可用(不修改现有会话配置)
* @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实例

View File

@ -37,6 +37,7 @@ const ipcApiRoute = {
editProxyInfo: 'controller/window/editProxyInfo',
editGlobalProxyInfo: 'controller/window/editGlobalProxyInfo',
saveProxyInfo: 'controller/window/saveProxyInfo',
testProxy: 'controller/window/testProxy',
openSessionDevTools: 'controller/window/openSessionDevTools',
closeGlobalProxyPasswordVerification: 'controller/window/closeGlobalProxyPasswordVerification',

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',
@ -224,9 +224,13 @@ 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',
@ -309,7 +313,7 @@ export default {
proxyConfig: 'Proxy Config',
devtools: 'Developer Tools',
expand: 'Expand',
collapse: 'Collapse',
collapse: 'Collapse',
screenshot: 'Screenshot',
},
quickReplyConfig: {
@ -333,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

@ -187,7 +187,10 @@ export default {
batchCount: '数量',
batchProxySettings: '批量代理设置',
batchProxySetSuccess: '批量代理设置成功',
selectSessionFirst: '请先勾选会话'
selectSessionFirst: '请先勾选会话',
testProxy: '测试代理',
proxyTestSuccess: '代理连接成功',
proxyTestFailed: '代理连接失败'
},
translate: {
google: '谷歌翻译',
@ -217,9 +220,12 @@ export default {
currentContact: '当前联系人',
globalContact: '全局联系人',
notFoundPersonalizedTranslation: '未找到个性化翻译设置',
interceptChinese: '拦截中文',
interceptChinese: '启用发送拦截',
interceptLanguagesLabel: '拦截语言',
selectInterceptLanguages: '请选择要拦截的语言',
translateHistory: '翻译历史消息',
autoTranslateGroupMessage: '自动翻译群消息',
usePersonalConfig: '启用当前联系人配置',
createPersonalizedTranslation: '新建个性化翻译',
chooseContactFirst: '请先选择联系人',
noUserId: '无法获取用户ID请选择会话或刷新页面',

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

@ -39,6 +39,16 @@
</el-radio-group>
</div>
</div>
<!-- 仅在当前联系人标签下显示是否启用个人配置 -->
<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-container-select">
<div class="content-left">
<el-text>{{ t('translate.route') }}</el-text>
@ -152,12 +162,23 @@
</div>
</div>
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.interceptChinese') }}</el-text>
<div class="content-left intercept-left">
<el-text>{{ t('translate.interceptLanguagesLabel') }}</el-text>
</div>
<div class="content-right">
<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>
<el-divider />
@ -233,6 +254,9 @@ const getConfigInfoIsNull = computed(() => {
return Object.keys(configInfo.value).length === 0;
});
// 加载/归一化阶段守卫,避免把初始化过程的值写回数据库
const isApplyingConfig = ref(false);
// watch(
// () => configInfo.value.friendTranslateStatus,
// async (newValue, oldValue) => {
@ -267,9 +291,11 @@ const propertiesToWatch = [
"chineseDetectionStatus",
"translatePreview",
"interceptChinese",
"interceptLanguages",
"translateHistory",
"autoTranslateGroupMessage",
"historyTranslateRoute"
"historyTranslateRoute",
"usePersonalConfig"
];
let watchers = []; // 存储所有字段的监听器
@ -281,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);
}
@ -290,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()); // 调用每个监听器的停止方法
@ -314,6 +354,7 @@ watch(activeTab, async (newVal) => {
const getConfigInfo = async () => {
loading.value = true; // 开始加载
isApplyingConfig.value = true;
removeWatchers();
let type = activeTab.value;
@ -325,17 +366,26 @@ const getConfigInfo = async () => {
if (type === 'global') {
args.platform = menuStore.platform;
// args.partitionId = '';
} else {
args.userId = menuStore.currentUserId;
// args.partitionId = menuStore.currentPartitionId;
}
try {
const res = await ipc.invoke(ipcApiRoute.getTrsConfig, args);
if (res.status) {
// Object.assign(configInfo.value, res.data); // 更新表单数据
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 = {};
}
@ -343,6 +393,7 @@ const getConfigInfo = async () => {
configInfo.value = {};
console.log(err)
} finally {
isApplyingConfig.value = false;
addWatchers();
loading.value = false; // 加载结束
}
@ -429,72 +480,92 @@ 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 = () => {
// 处理翻译路线变更的逻辑
@ -514,7 +585,11 @@ const handleCreatePersonalConfig = async () => {
try {
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') || '创建失败');
}
@ -552,7 +627,7 @@ const refreshTranslateRoutes = async () => {
<style scoped lang="less">
.translate-config {
width: 300px;
width: 320px;
height: 100%;
padding: 20px;
display: flex;
@ -636,6 +711,28 @@ const refreshTranslateRoutes = 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

@ -12,6 +12,32 @@ 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();
@ -57,6 +83,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,6 +122,7 @@ const saveProxyConfig = async () => {
};
// 调用主进程方法
const res = await ipc.invoke(ipcApiRoute.saveProxyInfo, args);
// 根据返回结果处理
if (res.status) {
ElMessage({
@ -200,6 +250,7 @@ const startAll = async () => {
menuStore.addChildrenMenu(item);
menuStore.updateChildrenMenu(res.data);
}
}
}
const closeAll = async () => {
@ -383,7 +434,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 +498,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 +549,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 +586,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 +607,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">