diff --git a/back/api/cron.ts b/back/api/cron.ts index 2ebe802b..6be6f4a3 100644 --- a/back/api/cron.ts +++ b/back/api/cron.ts @@ -312,8 +312,8 @@ export default (app: Router) => { const logger: Logger = Container.get('logger'); try { const cronService = Container.get(CronService); - const data = await cronService.log(req.params.id); - return res.send({ code: 200, data }); + const result = await cronService.log(req.params.id); + return res.send({ code: 200, data: result.content, logStatus: result.status }); } catch (e) { return next(e); } diff --git a/back/api/dashboard.ts b/back/api/dashboard.ts index 799503aa..5e9438a3 100644 --- a/back/api/dashboard.ts +++ b/back/api/dashboard.ts @@ -9,6 +9,8 @@ import { } from '../data/runningInstance'; import dayjs from 'dayjs'; import os from 'os'; +import { isEmpty } from 'lodash'; +import { t, tf } from '../shared/i18n'; const route = Router(); @@ -181,7 +183,7 @@ export default (app: Router) => { const data = rows.map((r: any, i) => ({ rank: i + 1, - name: nameMap[Number(r.ref_id)] || `任务#${r.ref_id}`, + name: nameMap[Number(r.ref_id)] || tf('任务#%s', r.ref_id), avgTime: Math.round(Number(r.total_time) / Number(r.run_count)), maxTime: Number(r.max_time), })); @@ -223,7 +225,7 @@ export default (app: Router) => { const data = rows.map((r: any, i) => ({ rank: i + 1, - name: nameMap[Number(r.ref_id)] || `任务#${r.ref_id}`, + name: nameMap[Number(r.ref_id)] || tf('任务#%s', r.ref_id), runCount: Number(r.run_count), avgTime: Math.round(Number(r.total_time) / Number(r.run_count)), successRate: @@ -264,9 +266,9 @@ export default (app: Router) => { const crons = cronIds.length > 0 ? await CrontabModel.findAll({ - where: { id: cronIds }, - raw: true, - }) + where: { id: cronIds }, + raw: true, + }) : []; const cronMap = new Map(crons.map((c: any) => [c.id, c])); @@ -276,7 +278,7 @@ export default (app: Router) => { return { instanceId: inst.id, id: inst.cron_id, - name: cron?.name || cron?.command || `任务#${inst.cron_id}`, + name: cron?.name || cron?.command || tf('任务#%s', inst.cron_id), pid: inst.pid, elapsed: inst.started_at ? now - inst.started_at : 0, logPath: inst.log_path, @@ -303,7 +305,7 @@ export default (app: Router) => { running, idleTasks: idleTasks.map((c: any) => ({ id: c.id, - name: c.name || c.command || `任务#${c.id}`, + name: c.name || c.command || tf('任务#%s', c.id), lastRun: c.last_execution_time ? dayjs.unix(c.last_execution_time).format('MM-DD HH:mm') : '-', @@ -324,17 +326,22 @@ export default (app: Router) => { const [crons, stats] = (await Promise.all([ CrontabModel.findAll({ where: { isDisabled: 0 }, raw: true }), CrontabStatModel.findAll({ where: { date: today }, raw: true }), - ])) as any[]; + ])); const statMap: Record = {}; stats.forEach((s: any) => { statMap[s.ref_id] = s; }); const labelMap: Record = {}; - crons.forEach((c: any) => { + crons.forEach((c) => { let rawLabels = c.labels; if (typeof rawLabels === 'string') rawLabels = JSON.parse(rawLabels); - const labels: string[] = Array.isArray(rawLabels) && rawLabels.length > 0 ? rawLabels : ['未分类']; - const st = statMap[c.id]; + const labels: string[] = Array.isArray(rawLabels) + ? [...new Set((rawLabels as string[]).filter((l: string) => !isEmpty(l)))] + : []; + if (labels.length === 0) { + labels.push(t('未分类')); + } + const st = statMap[c.id!]; labels.forEach((label: string) => { if (!labelMap[label]) labelMap[label] = { count: 0, runs: 0, success: 0, totalTime: 0 }; labelMap[label].count += 1; @@ -372,7 +379,7 @@ export default (app: Router) => { code: 200, data: { platform: os.platform(), - uptime: Math.floor(os.uptime()), + uptime: Math.floor(process.uptime()), memTotal: os.totalmem(), memFree: os.freemem(), memUsagePercent: ((1 - os.freemem() / os.totalmem()) * 100).toFixed(1), diff --git a/back/config/index.ts b/back/config/index.ts index 869f5d10..00b2a7b8 100644 --- a/back/config/index.ts +++ b/back/config/index.ts @@ -1,6 +1,5 @@ import dotenv from 'dotenv'; import path from 'path'; -import { createRandomString } from './share'; dotenv.config({ path: path.join(__dirname, '../../.env'), @@ -119,8 +118,6 @@ const confBakDir = path.join(dataPath, 'config/bak/'); const sampleFile = path.join(samplePath, 'config.sample.sh'); const sqliteFile = path.join(samplePath, 'database.sqlite'); -const authError = '错误的用户名密码,请重试'; -const loginFaild = '请先登录!'; const configString = 'config sample crontab shareCode diy'; const versionFile = path.join(rootPath, 'version.yaml'); const dataTgzFile = path.join(tmpPath, 'data.tgz'); @@ -142,8 +139,6 @@ export default { shareShellFile, dependenceProxyFile, configString, - loginFaild, - authError, logPath, extraFile, authConfigFile, diff --git a/back/config/util.ts b/back/config/util.ts index 4a654bb7..f7f123a7 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -536,7 +536,7 @@ export function safeJSONParse(value?: string) { try { return JSON.parse(value); } catch (error) { - Logger.error('[safeJSONParse失败]', error); + Logger.error('[safeJSONParse error]', error); return {}; } } @@ -548,7 +548,7 @@ export async function rmPath(path: string) { await fs.rm(path, { force: true, recursive: true, maxRetries: 5 }); } } catch (error) { - Logger.error('[rmPath失败]', error); + Logger.error('[rmPath error]', error); } } @@ -563,7 +563,7 @@ export async function setSystemTimezone(timezone: string): Promise { return true; } catch (error) { - Logger.error('[setSystemTimezone失败]', error); + Logger.error('[setSystemTimezone error]', error); return false; } } diff --git a/back/data/sock.ts b/back/data/sock.ts index 8dae8e13..bb4d7798 100644 --- a/back/data/sock.ts +++ b/back/data/sock.ts @@ -2,11 +2,13 @@ export class SockMessage { message?: string; type?: SockMessageType; references?: number[]; + status?: number | string; constructor(options: SockMessage) { this.type = options.type; this.message = options.message; this.references = options.references; + this.status = options.status; } } diff --git a/back/loaders/initTask.ts b/back/loaders/initTask.ts index bdc57d42..ed98c0a0 100644 --- a/back/loaders/initTask.ts +++ b/back/loaders/initTask.ts @@ -6,6 +6,7 @@ import SshKeyService from '../services/sshKey'; import config from '../config'; import { fileExist } from '../config/util'; import { join } from 'path'; +import { t } from '../shared/i18n'; export default async () => { const systemService = Container.get(SystemService); @@ -25,7 +26,7 @@ export default async () => { } const cron = { id: NaN, - name: '生成token', + name: t('生成token'), command: tokenCommand, runOrigin: 'system', } as ScheduleTaskType; @@ -44,7 +45,7 @@ export default async () => { if (data.info.logRemoveFrequency) { const rmlogCron = { id: data.id as number, - name: '删除日志', + name: t('删除日志'), command: `ql rmlog ${data.info.logRemoveFrequency}`, runOrigin: 'system' as const, }; diff --git a/back/services/cron.ts b/back/services/cron.ts index 6ecc9088..16e24693 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -712,23 +712,27 @@ export default class CronService { await this.setCrontab(); } - public async log(id: number) { + public async log(id: number): Promise<{ content: string; status: string }> { const doc = await this.getDb({ id }); if (!doc) { - return ''; + return { content: '', status: 'empty' }; } if (doc.log_name === '/dev/null') { - return '日志设置为忽略'; + return { content: t('日志设置为忽略'), status: 'ignored' }; } const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); const logFileExist = doc.log_path && (await fileExist(absolutePath)); if (logFileExist) { - return await getFileContentByName(`${absolutePath}`); + const content = await getFileContentByName(`${absolutePath}`); + const isRunning = + typeof doc.status === 'number' && + [CrontabStatus.running, CrontabStatus.queued].includes(doc.status); + return { content, status: isRunning ? 'running' : 'completed' }; } else { return typeof doc.status === 'number' && [CrontabStatus.queued, CrontabStatus.running].includes(doc.status) - ? '运行中...' - : '日志不存在...'; + ? { content: t('运行中...'), status: 'running' } + : { content: t('日志不存在...'), status: 'notFound' }; } } diff --git a/back/services/dependence.ts b/back/services/dependence.ts index c00422b3..d384ec76 100644 --- a/back/services/dependence.ts +++ b/back/services/dependence.ts @@ -24,6 +24,7 @@ import dayjs from 'dayjs'; import taskLimit from '../shared/pLimit'; import { detectOS } from '../config/util'; import { LINUX_DEPENDENCE_COMMAND } from '../config/const'; +import { t, tf } from '../shared/i18n'; @Service() export default class DependenceService { @@ -232,7 +233,7 @@ export default class DependenceService { } const depIds = [dependency.id!]; let depName = dependency.name.trim(); - const actionText = isInstall ? '安装' : '删除'; + const actionText = isInstall ? t('安装') : t('删除'); const socketMessageType = isInstall ? 'installDependence' : 'uninstallDependence'; @@ -249,15 +250,20 @@ export default class DependenceService { { where: { id: depIds } }, ); const startTime = dayjs(); - const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format( - 'YYYY-MM-DD HH:mm:ss', - )}\n\n当前系统不支持\n\n依赖${actionText}失败,结束时间 ${startTime.format( - 'YYYY-MM-DD HH:mm:ss', - )},耗时 ${startTime.diff(startTime, 'second')} 秒`; + const message = tf( + '开始%s依赖 %s,开始时间 %s\n\n当前系统不支持\n\n依赖%s失败,结束时间 %s,耗时 %s 秒', + actionText, + depName, + startTime.format('YYYY-MM-DD HH:mm:ss'), + actionText, + startTime.format('YYYY-MM-DD HH:mm:ss'), + String(startTime.diff(startTime, 'second')), + ); this.sockService.sendMessage({ type: socketMessageType, message, references: depIds, + status: DependenceStatus.installFailed, }); this.updateLog(depIds, message); return resolve(null); @@ -280,13 +286,17 @@ export default class DependenceService { } const startTime = dayjs(); - const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format( - 'YYYY-MM-DD HH:mm:ss', - )}\n\n`; + const message = tf( + '开始%s依赖 %s,开始时间 %s\n\n', + actionText, + depName, + startTime.format('YYYY-MM-DD HH:mm:ss'), + ); this.sockService.sendMessage({ type: socketMessageType, message, references: depIds, + status, }); this.updateLog(depIds, message); @@ -322,13 +332,19 @@ export default class DependenceService { (!depVersion || depInfo.includes(depVersion)) ) { const endTime = dayjs(); - const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format( - 'YYYY-MM-DD HH:mm:ss', - )},耗时 ${endTime.diff(startTime, 'second')} 秒`; + const _message = tf( + '检测到已经安装 %s\n\n%s\n\n跳过安装\n\n依赖%s成功,结束时间 %s,耗时 %s 秒', + depName, + depInfo, + actionText, + endTime.format('YYYY-MM-DD HH:mm:ss'), + String(endTime.diff(startTime, 'second')), + ); this.sockService.sendMessage({ type: socketMessageType, message: _message, references: depIds, + status: DependenceStatus.installed, }); this.updateLog(depIds, _message); await DependenceModel.update( @@ -353,6 +369,7 @@ export default class DependenceService { type: socketMessageType, message: data.toString(), references: depIds, + status, }); this.updateLog(depIds, data.toString()); }); @@ -362,6 +379,7 @@ export default class DependenceService { type: socketMessageType, message: data.toString(), references: depIds, + status, }); this.updateLog(depIds, data.toString()); }); @@ -371,6 +389,7 @@ export default class DependenceService { type: socketMessageType, message: JSON.stringify(err), references: depIds, + status, }); this.updateLog(depIds, JSON.stringify(err)); }); @@ -378,28 +397,27 @@ export default class DependenceService { cp.on('exit', async (code) => { const endTime = dayjs(); const isSucceed = code === 0; - const resultText = isSucceed ? '成功' : '失败'; + const resultText = isSucceed ? t('成功') : t('失败'); - const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format( - 'YYYY-MM-DD HH:mm:ss', - )},耗时 ${endTime.diff(startTime, 'second')} 秒`; + const message = + '\n' + + tf('依赖%s%s,结束时间 %s,耗时 %s 秒', + actionText, + resultText, + endTime.format('YYYY-MM-DD HH:mm:ss'), + String(endTime.diff(startTime, 'second')), + ); + const exitStatus = isSucceed + ? (isInstall ? DependenceStatus.installed : DependenceStatus.removed) + : (isInstall ? DependenceStatus.installFailed : DependenceStatus.removeFailed); this.sockService.sendMessage({ type: socketMessageType, message, references: depIds, + status: exitStatus, }); this.updateLog(depIds, message); - let status: number; - if (isSucceed) { - status = isInstall - ? DependenceStatus.installed - : DependenceStatus.removed; - } else { - status = isInstall - ? DependenceStatus.installFailed - : DependenceStatus.removeFailed; - } const docs = await DependenceModel.findAll({ where: { id: depIds } }); const _docIds = docs .filter((x) => x.status !== DependenceStatus.cancelled) @@ -407,7 +425,7 @@ export default class DependenceService { if (_docIds.length > 0) { await DependenceModel.update( - { status }, + { status: exitStatus }, { where: { id: _docIds } }, ); } diff --git a/back/services/notify.ts b/back/services/notify.ts index abf1f96c..099aca12 100644 --- a/back/services/notify.ts +++ b/back/services/notify.ts @@ -365,7 +365,7 @@ export default class NotificationService { { title: `${this.title}`, thumb_media_id, - author: `智能助手`, + author: t('智能助手'), content_source_url: ``, content: `${this.content.replace(/\n/g, '
')}`, digest: `${this.content}`, @@ -381,7 +381,7 @@ export default class NotificationService { title: `${this.title}`, description: `${this.content}`, url: 'https://github.com/whyour/qinglong', - btntxt: '更多', + btntxt: t('更多'), }, }; break; @@ -432,7 +432,7 @@ export default class NotificationService { roomName: `${aibotkName}`, message: { type: 1, - content: `【青龙快讯】\n\n${this.title}\n${this.content}`, + content: `【${t('青龙快讯')}】\n\n${this.title}\n${this.content}`, }, }; break; @@ -443,7 +443,7 @@ export default class NotificationService { name: `${aibotkName}`, message: { type: 1, - content: `【青龙快讯】\n\n${this.title}\n${this.content}`, + content: `【${t('青龙快讯')}】\n\n${this.title}\n${this.content}`, }, }; break; @@ -613,7 +613,7 @@ export default class NotificationService { }); const info = await transporter.sendMail({ - from: `"青龙快讯" <${emailUser}>`, + from: `"${t('青龙快讯')}" <${emailUser}>`, to: recipients, subject: `${this.title}`, html: `${this.content.replace(/\n/g, '
')}`, diff --git a/back/services/subscription.ts b/back/services/subscription.ts index ed7f5b40..a387bb1b 100644 --- a/back/services/subscription.ts +++ b/back/services/subscription.ts @@ -24,7 +24,7 @@ import path, { join } from 'path'; import ScheduleService, { TaskCallbacks } from './schedule'; import { SimpleIntervalSchedule } from 'toad-scheduler'; import SockService from './sock'; -import { t } from '../shared/i18n'; +import { t, tf } from '../shared/i18n'; import SshKeyService from './sshKey'; import dayjs from 'dayjs'; import { LOG_END_SYMBOL } from '../config/const'; @@ -131,14 +131,14 @@ export default class SubscriptionService { ); const absolutePath = await handleLogPath( logPath as string, - `## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`, + tf('## 开始执行... %s\n', startTime.format('YYYY-MM-DD HH:mm:ss')), ); // 执行sub_before let beforeStr = ''; try { if (doc.sub_before) { - await logStreamManager.write(absolutePath, `\n## 执行before命令...\n\n`); + await logStreamManager.write(absolutePath, `\n## ${t('执行before命令...')}\n\n`); beforeStr = await promiseExec(doc.sub_before); } } catch (error: any) { @@ -165,7 +165,7 @@ export default class SubscriptionService { let afterStr = ''; try { if (sub.sub_after) { - await logStreamManager.write(absolutePath, `\n\n## 执行after命令...\n\n`); + await logStreamManager.write(absolutePath, `\n\n## ${t('执行after命令...')}\n\n`); afterStr = await promiseExec(sub.sub_after); } } catch (error: any) { @@ -178,9 +178,13 @@ export default class SubscriptionService { await logStreamManager.write( absolutePath, - `\n## 执行结束... ${endTime.format( - 'YYYY-MM-DD HH:mm:ss', - )} 耗时 ${diff} 秒${LOG_END_SYMBOL}`, + '\n' + + tf( + '## 执行结束... %s 耗时 %s 秒', + endTime.format('YYYY-MM-DD HH:mm:ss'), + String(diff), + ) + + LOG_END_SYMBOL, ); // Close the stream after task completion diff --git a/back/services/system.ts b/back/services/system.ts index 4f658622..0bd6aef8 100644 --- a/back/services/system.ts +++ b/back/services/system.ts @@ -78,8 +78,8 @@ export default class SystemService { const code = Math.random().toString().slice(-6); const isSuccess = await this.notificationService.testNotify( notificationInfo, - '青龙', - `【蛟龙】测试通知 https://t.me/jiao_long`, + t('青龙'), + t('【蛟龙】测试通知 https://t.me/jiao_long'), ); if (isSuccess) { const result = await this.updateAuthDb({ @@ -100,7 +100,7 @@ export default class SystemService { }); const cron = { id: result.id as number, - name: '删除日志', + name: t('删除日志'), command: `ql rmlog ${info.logRemoveFrequency}`, runOrigin: 'system' as const, }; @@ -179,6 +179,7 @@ export default class SystemService { this.sockService.sendMessage({ type: 'updateNodeMirror', message: 'update node mirror end', + status: 'completed', }); }, onError: async (message: string) => { @@ -232,6 +233,7 @@ export default class SystemService { this.sockService.sendMessage({ type: 'updateLinuxMirror', message: 'update linux mirror end', + status: 'completed', }); onEnd?.(); if (!hasError) { @@ -340,6 +342,15 @@ export default class SystemService { this.sockService.sendMessage({ type: 'updateSystemVersion', message: JSON.stringify(err), + status: 'failed', + }); + }); + + cp.on('exit', (code) => { + this.sockService.sendMessage({ + type: 'updateSystemVersion', + message: '', + status: code === 0 ? 'success' : 'failed', }); }); diff --git a/back/services/user.ts b/back/services/user.ts index 31d66864..88f00a11 100644 --- a/back/services/user.ts +++ b/back/services/user.ts @@ -25,7 +25,7 @@ import uniq from 'lodash/uniq'; import pickBy from 'lodash/pickBy'; import isNil from 'lodash/isNil'; import { shareStore } from '../shared/store'; -import { t } from '../shared/i18n'; +import { t, tf } from '../shared/i18n'; @Service() export default class UserService { @@ -67,7 +67,7 @@ export default class UserService { ); return { code: 410, - message: `失败次数过多,请${waitTime}秒后重试`, + message: tf('失败次数过多,请%s秒后重试', waitTime), data: waitTime, }; } @@ -128,10 +128,19 @@ export default class UserService { isTwoFactorChecking: false, }); this.notificationService.notify( - '登录通知', - `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${ - req.platform - }端 登录成功,ip地址 ${ip}`, + t('登录通知'), + t('你于') + + dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss') + + t('在') + + address + + ' ' + + req.platform + + t('端') + + ' ' + + t('登录成功') + + t(',ip地址') + + ' ' + + ip, ); await this.insertDb({ type: AuthDataType.loginLog, @@ -164,10 +173,19 @@ export default class UserService { platform: req.platform, }); this.notificationService.notify( - '登录通知', - `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${ - req.platform - }端 登录失败,ip地址 ${ip}`, + t('登录通知'), + t('你于') + + dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss') + + t('在') + + address + + ' ' + + req.platform + + t('端') + + ' ' + + t('登录失败') + + t(',ip地址') + + ' ' + + ip, ); await this.insertDb({ type: AuthDataType.loginLog, @@ -184,11 +202,11 @@ export default class UserService { const waitTime = Math.round(Math.pow(3, retries + 1)); return { code: 410, - message: `失败次数过多,请${waitTime}秒后重试`, + message: tf('失败次数过多,请%s秒后重试', waitTime), data: waitTime, }; } else { - return { code: 400, message: config.authError }; + return { code: 400, message: t('错误的用户名密码,请重试') }; } } } @@ -389,8 +407,8 @@ export default class UserService { const code = Math.random().toString().slice(-6); const isSuccess = await this.notificationService.testNotify( notificationInfo, - '青龙', - `【蛟龙】测试通知 https://t.me/jiao_long`, + t('青龙'), + t('【蛟龙】测试通知 https://t.me/jiao_long'), ); if (isSuccess) { const result = await this.updateAuthDb({ diff --git a/back/shared/i18n.ts b/back/shared/i18n.ts index 04eaa769..b8d73847 100644 --- a/back/shared/i18n.ts +++ b/back/shared/i18n.ts @@ -80,6 +80,55 @@ const messages: Record> = { '订阅执行完成': 'Subscription completed', 'wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行': 'wxPusher requires at least one of TopicIds or Uids', 'Url 或者 Body 中必须包含 $title': 'Url or Body must contain $title', + '绝对路径必须在日志目录内或使用 /dev/null': + 'Absolute path must be within log directory or use /dev/null', + '请先登录!': 'Please login first!', + '运行中...': 'Running...', + '日志不存在...': 'Log does not exist...', + '未分类': 'Uncategorized', + '任务重复运行': 'Duplicate task execution', + '日志设置为忽略': 'Log set to ignore', + '定时规则不能为空': 'Schedule rule cannot be empty', + '无效的定时规则': 'Invalid schedule rule', + '日志名称只能包含字母、数字、下划线和连字符': + 'Log name can only contain letters, numbers, underscores, and hyphens', + '日志名称不能超过100个字符': 'Log name cannot exceed 100 characters', + '错误的用户名密码,请重试': 'Incorrect username or password, please try again', + '青龙快讯': 'QingLong', + '登录通知': 'Login Notification', + '你于': 'You at ', + '在': ' in ', + 端: '', + 登录失败: 'login failed', + ',ip地址': ', IP: ', + '任务#%s': 'Task#%s', + 安装: 'Install', + 删除: 'Uninstall', + 成功: 'Succeeded', + 失败: 'Failed', + '失败次数过多,请%s秒后重试': + 'Too many failed attempts, please retry in %s seconds', + 智能助手: 'Smart Assistant', + 更多: 'More', + '开始%s依赖 %s,开始时间 %s\n\n当前系统不支持\n\n依赖%s失败,结束时间 %s,耗时 %s 秒': + 'Start %s dependency %s, start time %s\n\nCurrent system not supported\n\nDependency %s failed, end time %s, elapsed %s seconds', + '检测到已经安装 %s\n\n%s\n\n跳过安装\n\n依赖%s成功,结束时间 %s,耗时 %s 秒': + 'Already installed %s\n\n%s\n\nSkipping install\n\nDependency %s succeeded, end time %s, elapsed %s seconds', + '开始%s依赖 %s,开始时间 %s\n\n': + 'Start %s dependency %s, start time %s\n\n', + '依赖%s%s,结束时间 %s,耗时 %s 秒': + 'Dependency %s%s, end time %s, elapsed %s seconds', + '任务:%s,命令:%s,定时:%s,处于运行中的超过 %d 个,请检查定时设置': + 'Task: %s, command: %s, schedule: %s, more than %d instances running, please check schedule settings', + 青龙: 'QingLong', + '【蛟龙】测试通知 https://t.me/jiao_long': + '[JiaoLong] Test notification https://t.me/jiao_long', + '生成token': 'Generate token', + '删除日志': 'Delete logs', + '## 开始执行... %s\n': '## Start executing... %s\n', + '执行before命令...': 'Execute before command...', + '执行after命令...': 'Execute after command...', + '## 执行结束... %s 耗时 %s 秒': '## Execution finished... %s elapsed %s seconds', }, }; @@ -100,3 +149,10 @@ export function t(key: string, lang?: string): string { } return key; } + +export function tf(key: string, ...args: (string | number)[]): string { + return args.reduce( + (str, arg) => str.replace(/%s|%d/, String(arg)), + t(key), + ); +} diff --git a/back/shared/pLimit.ts b/back/shared/pLimit.ts index 7d75678f..47f8b6c8 100644 --- a/back/shared/pLimit.ts +++ b/back/shared/pLimit.ts @@ -4,6 +4,7 @@ import { AuthDataType, SystemModel } from '../data/system'; import Logger from '../loaders/logger'; import { Dependence } from '../data/dependence'; import NotificationService from '../services/notify'; +import { t, tf } from '../shared/i18n'; import { ICronFn, IDependencyFn, @@ -152,8 +153,14 @@ class TaskLimit { this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1); this.client.systemNotify( { - title: '任务重复运行', - content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`, + title: t('任务重复运行'), + content: tf( + '任务:%s,命令:%s,定时:%s,处于运行中的超过 %d 个,请检查定时设置', + cron.name || '', + cron.command || '', + cron.schedule || '', + 5, + ), }, (err, res) => { if (err) { diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 762342a8..30ec0dd5 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -594,5 +594,11 @@ "黑名单": "Blacklist", "默认为 CPU 个数": "Default is the number of CPUs", ",保存后不可恢复": ", it can't be recovered after saving.", - ",删除后不可恢复": ", it can't be recovered after deletion" + ",删除后不可恢复": ", it can't be recovered after deletion", + "日志不存在": "Log does not exist", + "日志设置为忽略": "Log set to ignore", + "确认保存": "Confirm to save", + "上传失败": "Upload failed", + "成功上传": "Successfully uploaded", + "个环境变量": " environment variables" } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 14b03eee..3c6deddb 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -594,5 +594,11 @@ "黑名单": "黑名单", "默认为 CPU 个数": "默认为 CPU 个数", ",保存后不可恢复": ",保存后不可恢复", - ",删除后不可恢复": ",删除后不可恢复" + ",删除后不可恢复": ",删除后不可恢复", + "日志不存在": "日志不存在", + "日志设置为忽略": "日志设置为忽略", + "确认保存": "确认保存", + "上传失败": "上传失败", + "成功上传": "成功上传", + "个环境变量": "个环境变量" } diff --git a/src/pages/crontab/detail.tsx b/src/pages/crontab/detail.tsx index 880180da..031d0ddb 100644 --- a/src/pages/crontab/detail.tsx +++ b/src/pages/crontab/detail.tsx @@ -298,7 +298,7 @@ const CronDetailModal = ({ const saveFile = () => { Modal.confirm({ - title: `确认保存`, + title: intl.get('确认保存'), content: ( <> {intl.get('确认保存文件')} @@ -323,7 +323,7 @@ const CronDetailModal = ({ .then(({ code, data }) => { if (code === 200) { setValue(content); - message.success(`保存成功`); + message.success(intl.get('保存成功')); } resolve(null); }) diff --git a/src/pages/crontab/logModal.tsx b/src/pages/crontab/logModal.tsx index b23af1bf..aa83c23c 100644 --- a/src/pages/crontab/logModal.tsx +++ b/src/pages/crontab/logModal.tsx @@ -46,17 +46,15 @@ const CronLogModal = ({ } request .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`) - .then(({ code, data }) => { + .then(({ code, data, logStatus }) => { if ( code === 200 && localStorage.getItem("logCron") === uniqPath && data !== value ) { - const log = data as string; - setValue(log || intl.get("暂无日志")); - const hasNext = Boolean( - log && !logEnded(log) && !log.includes("日志不存在") && !log.includes("日志设置为忽略"), - ); + const log = (data as string) || intl.get("暂无日志"); + setValue(log); + const hasNext = logStatus === 'running'; if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) { setTimeout(() => { autoScroll(); diff --git a/src/pages/dependence/index.tsx b/src/pages/dependence/index.tsx index 7aac607c..1538af4d 100644 --- a/src/pages/dependence/index.tsx +++ b/src/pages/dependence/index.tsx @@ -465,49 +465,33 @@ const Dependence = () => { }, [logDependence]); const handleMessage = useCallback((payload: any) => { - const { message, references } = payload; - let status: number | undefined = undefined; - if (message.includes('开始时间') && references.length > 0) { - status = message.includes('安装') ? Status.安装中 : Status.删除中; - } - if (message.includes('结束时间') && references.length > 0) { - if (message.includes('安装')) { - status = message.includes('成功') ? Status.已安装 : Status.安装失败; - } else { - status = message.includes('成功') ? Status.已删除 : Status.删除失败; - } + const { references, status } = payload; + if (typeof status !== 'number' || !references?.length) return; - if (status === Status.已删除) { - setTimeout(() => { - setValue((p) => { - const _result = [...p]; - for (let i = 0; i < references.length; i++) { - const index = p.findIndex((x) => x.id === references[i]); - if (index !== -1) { - _result.splice(index, 1); - } - } - return _result; - }); - }, 300); - return; - } - } - if (typeof status === 'number') { - setValue((p) => { - const result = [...p]; - for (let i = 0; i < references.length; i++) { - const index = p.findIndex((x) => x.id === references[i]); - if (index !== -1) { - result.splice(index, 1, { - ...p[index], - status, - }); + if (status === Status.已删除) { + setTimeout(() => { + setValue((p) => { + const _result = [...p]; + for (const refId of references) { + const index = p.findIndex((x) => x.id === refId); + if (index !== -1) _result.splice(index, 1); } - } - return result; - }); + return _result; + }); + }, 300); + return; } + + setValue((p) => { + const result = [...p]; + for (const refId of references) { + const index = p.findIndex((x) => x.id === refId); + if (index !== -1) { + result.splice(index, 1, { ...p[index], status }); + } + } + return result; + }); }, []); useEffect(() => { diff --git a/src/pages/dependence/logModal.tsx b/src/pages/dependence/logModal.tsx index 69ec8b4e..66aad46e 100644 --- a/src/pages/dependence/logModal.tsx +++ b/src/pages/dependence/logModal.tsx @@ -54,8 +54,11 @@ const DependenceLogModal = ({ ) { const log = (data?.log || []).join('') as string; setValue(log); - setExecuting(!log.includes('结束时间')); - setIsRemoveFailed(log.includes('删除失败')); + const dbStatus = data?.status as number | undefined; + setExecuting( + dbStatus === Status.安装中 || dbStatus === Status.删除中, + ); + setIsRemoveFailed(dbStatus === Status.删除失败); } }) .finally(() => { @@ -94,16 +97,17 @@ const DependenceLogModal = ({ }, [dependence]); const handleMessage = (payload: any) => { - const { message, references } = payload; + const { message, references, status } = payload; if ( - references.length > 0 && - references.includes(dependence.id) && - [Status.删除中, Status.安装中].includes(dependence.status) - ) { - if (message.includes('结束时间')) { - setExecuting(false); - setIsRemoveFailed(message.includes('删除失败')); - } + !references?.length || + !references.includes(dependence.id) + ) return; + + if (typeof status === 'number') { + setExecuting(status === Status.安装中 || status === Status.删除中); + setIsRemoveFailed(status === Status.删除失败); + } + if (message) { setValue((p) => `${p}${message}`); } }; diff --git a/src/pages/env/index.tsx b/src/pages/env/index.tsx index 0cb2c610..7659abc1 100644 --- a/src/pages/env/index.tsx +++ b/src/pages/env/index.tsx @@ -603,7 +603,7 @@ const Env = () => { ); if (code === 200) { - message.success(`成功上传${data.length}个环境变量`); + message.success(`${intl.get('成功上传')}${data.length}${intl.get('个环境变量')}`); getEnvs(); } setImportLoading(false); diff --git a/src/pages/log/index.tsx b/src/pages/log/index.tsx index 6cdf6f43..83ba585f 100644 --- a/src/pages/log/index.tsx +++ b/src/pages/log/index.tsx @@ -131,7 +131,7 @@ const Log = () => { const deleteFile = () => { Modal.confirm({ - title: `确认删除`, + title: intl.get('确认删除'), content: ( <> {intl.get('确认删除')} @@ -155,7 +155,7 @@ const Log = () => { }) .then(({ code }) => { if (code === 200) { - message.success(`删除成功`); + message.success(intl.get('删除成功')); let newData = [...data]; if (currentNode.parent) { newData = depthFirstSearch( diff --git a/src/pages/script/index.tsx b/src/pages/script/index.tsx index 8f605d58..f341c0f5 100644 --- a/src/pages/script/index.tsx +++ b/src/pages/script/index.tsx @@ -249,7 +249,7 @@ const Script = () => { const saveFile = () => { Modal.confirm({ - title: `确认保存`, + title: intl.get('确认保存'), content: ( <> {intl.get('确认保存文件')} @@ -273,7 +273,7 @@ const Script = () => { }) .then(({ code, data }) => { if (code === 200) { - message.success(`保存成功`); + message.success(intl.get('保存成功')); setValue(content); handleIsEditing(currentNode.title, false); } @@ -287,7 +287,7 @@ const Script = () => { const deleteFile = () => { Modal.confirm({ - title: `确认删除`, + title: intl.get('确认删除'), content: ( <> {intl.get('确认删除')} @@ -311,7 +311,7 @@ const Script = () => { }) .then(({ code }) => { if (code === 200) { - message.success(`删除成功`); + message.success(intl.get('删除成功')); let newData = [...data]; if (currentNode.parent) { newData = depthFirstSearch( diff --git a/src/pages/setting/checkUpdate.tsx b/src/pages/setting/checkUpdate.tsx index 16e320fc..228148e7 100644 --- a/src/pages/setting/checkUpdate.tsx +++ b/src/pages/setting/checkUpdate.tsx @@ -13,6 +13,7 @@ const CheckUpdate = ({ systemInfo }: any) => { const [updateLoading, setUpdateLoading] = useState(false); const [value, setValue] = useState(""); const modalRef = useRef(); + const lastStatusRef = useRef(); const checkUpgrade = () => { if (updateLoading) return; @@ -166,7 +167,7 @@ const CheckUpdate = ({ systemInfo }: any) => { useEffect(() => { if (!value) return; - const updateFailed = value.includes("失败,请检查"); + const updateFailed = lastStatusRef.current === 'failed'; modalRef.current.update({ maskClosable: updateFailed, @@ -184,26 +185,29 @@ const CheckUpdate = ({ systemInfo }: any) => { }, [value]); const handleMessage = useCallback((payload: any) => { - let { message: _message } = payload; - const updateFailed = _message.includes("失败,请检查"); + const { message: _message, status } = payload; - if (updateFailed) { + if (status === 'failed') { + lastStatusRef.current = 'failed'; message.error(intl.get("更新失败,请检查网络及日志或稍后再试")); } + if (status === 'success') { + lastStatusRef.current = 'success'; + setTimeout(() => { + showReloadModal(); + }, 1000); + } + setTimeout(() => { document .querySelector("#log-identifier") ?.scrollIntoView({ behavior: "smooth" }); }, 600); - if (_message.includes("更新包下载成功")) { - setTimeout(() => { - showReloadModal(); - }, 1000); + if (_message) { + setValue((p) => `${p}${_message}`); } - - setValue((p) => `${p}${_message}`); }, []); useEffect(() => { diff --git a/src/pages/setting/dependence.tsx b/src/pages/setting/dependence.tsx index 9a0974aa..684e4449 100644 --- a/src/pages/setting/dependence.tsx +++ b/src/pages/setting/dependence.tsx @@ -75,12 +75,9 @@ const Dependence = () => { }; const handleMessage = (payload: any) => { - const { message } = payload; - setLog((p) => `${p}${message}`); - if ( - message.includes('update node mirror end') || - message.includes('update linux mirror end') - ) { + const { message, status } = payload; + if (message) setLog((p) => `${p}${message}`); + if (status === 'completed') { setLoading(false); } }; @@ -109,7 +106,7 @@ const Dependence = () => { ws.subscribe('updateLinuxMirror', handleMessage); return () => { - ws.subscribe('updateNodeMirror', handleMessage); + ws.unsubscribe('updateNodeMirror', handleMessage); ws.unsubscribe('updateLinuxMirror', handleMessage); }; }, []); diff --git a/src/pages/setting/other.tsx b/src/pages/setting/other.tsx index 8ff0674f..60ed959e 100644 --- a/src/pages/setting/other.tsx +++ b/src/pages/setting/other.tsx @@ -379,7 +379,7 @@ const Other = ({ showReloadModal(); } if (file.status === 'error') { - message.error('上传失败'); + message.error(intl.get('上传失败')); } }} name="data" diff --git a/src/pages/setting/systemLog.tsx b/src/pages/setting/systemLog.tsx index 2e710d83..57235b72 100644 --- a/src/pages/setting/systemLog.tsx +++ b/src/pages/setting/systemLog.tsx @@ -51,7 +51,7 @@ const SystemLog = ({ height, theme }: any) => { const deleteLog = () => { request.delete(`${config.apiPrefix}system/log`).then((x) => { - message.success('删除成功'); + message.success(intl.get('删除成功')); refresh(); }); }; diff --git a/src/pages/subscription/modal.tsx b/src/pages/subscription/modal.tsx index d5b6afb4..d57dffaa 100644 --- a/src/pages/subscription/modal.tsx +++ b/src/pages/subscription/modal.tsx @@ -205,21 +205,47 @@ const SubscriptionModal = ({ ); }; + const parseArgs = (text: string): string[] => { + const args: string[] = []; + let current = ''; + let inDouble = false; + let inSingle = false; + let justClosed = false; + for (const ch of text) { + if (inDouble) { + if (ch === '"') { inDouble = false; justClosed = true; continue; } + current += ch; + } else if (inSingle) { + if (ch === "'") { inSingle = false; justClosed = true; continue; } + current += ch; + } else if (ch === '"') { + inDouble = true; + justClosed = false; + } else if (ch === "'") { + inSingle = true; + justClosed = false; + } else if (ch === ' ' || ch === '\t') { + if (current || justClosed) { args.push(current); current = ''; justClosed = false; } + } else { + current += ch; + justClosed = false; + } + } + if (current || justClosed) args.push(current); + return args; + }; + const onPaste = useCallback((e: any) => { const text = e.clipboardData.getData('text') as string; if (text.startsWith('ql ')) { - const [ - , - type, - url, - whitelist, - blacklist, - dependences, - branch, - extensions, - ] = text - .split(' ') - .map((x) => x.trim().replace(/\"/g, '').replace(/\'/, '')); + const args = parseArgs(text); + const type = args[1]; + const url = args[2] || ''; + const whitelist = args[3] || ''; + const blacklist = args[4] || ''; + const dependences = args[5] || ''; + const branch = args[6] || ''; + const extensions = args[7] || ''; const _type = type === 'raw' ? 'file'