修复国际化文案

This commit is contained in:
whyour 2026-06-21 23:53:32 +08:00
parent 369dd13212
commit 3044f63f03
28 changed files with 335 additions and 187 deletions

View File

@ -312,8 +312,8 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.log(req.params.id); const result = await cronService.log(req.params.id);
return res.send({ code: 200, data }); return res.send({ code: 200, data: result.content, logStatus: result.status });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }

View File

@ -9,6 +9,8 @@ import {
} from '../data/runningInstance'; } from '../data/runningInstance';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import os from 'os'; import os from 'os';
import { isEmpty } from 'lodash';
import { t, tf } from '../shared/i18n';
const route = Router(); const route = Router();
@ -181,7 +183,7 @@ export default (app: Router) => {
const data = rows.map((r: any, i) => ({ const data = rows.map((r: any, i) => ({
rank: i + 1, 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)), avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
maxTime: Number(r.max_time), maxTime: Number(r.max_time),
})); }));
@ -223,7 +225,7 @@ export default (app: Router) => {
const data = rows.map((r: any, i) => ({ const data = rows.map((r: any, i) => ({
rank: i + 1, 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), runCount: Number(r.run_count),
avgTime: Math.round(Number(r.total_time) / Number(r.run_count)), avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
successRate: successRate:
@ -276,7 +278,7 @@ export default (app: Router) => {
return { return {
instanceId: inst.id, instanceId: inst.id,
id: inst.cron_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, pid: inst.pid,
elapsed: inst.started_at ? now - inst.started_at : 0, elapsed: inst.started_at ? now - inst.started_at : 0,
logPath: inst.log_path, logPath: inst.log_path,
@ -303,7 +305,7 @@ export default (app: Router) => {
running, running,
idleTasks: idleTasks.map((c: any) => ({ idleTasks: idleTasks.map((c: any) => ({
id: c.id, id: c.id,
name: c.name || c.command || `任务#${c.id}`, name: c.name || c.command || tf('任务#%s', c.id),
lastRun: c.last_execution_time lastRun: c.last_execution_time
? dayjs.unix(c.last_execution_time).format('MM-DD HH:mm') ? 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([ const [crons, stats] = (await Promise.all([
CrontabModel.findAll({ where: { isDisabled: 0 }, raw: true }), CrontabModel.findAll({ where: { isDisabled: 0 }, raw: true }),
CrontabStatModel.findAll({ where: { date: today }, raw: true }), CrontabStatModel.findAll({ where: { date: today }, raw: true }),
])) as any[]; ]));
const statMap: Record<number, any> = {}; const statMap: Record<number, any> = {};
stats.forEach((s: any) => { statMap[s.ref_id] = s; }); stats.forEach((s: any) => { statMap[s.ref_id] = s; });
const labelMap: Record<string, { count: number; runs: number; success: number; totalTime: number }> = {}; const labelMap: Record<string, { count: number; runs: number; success: number; totalTime: number }> = {};
crons.forEach((c: any) => { crons.forEach((c) => {
let rawLabels = c.labels; let rawLabels = c.labels;
if (typeof rawLabels === 'string') rawLabels = JSON.parse(rawLabels); if (typeof rawLabels === 'string') rawLabels = JSON.parse(rawLabels);
const labels: string[] = Array.isArray(rawLabels) && rawLabels.length > 0 ? rawLabels : ['未分类']; const labels: string[] = Array.isArray(rawLabels)
const st = statMap[c.id]; ? [...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) => { labels.forEach((label: string) => {
if (!labelMap[label]) labelMap[label] = { count: 0, runs: 0, success: 0, totalTime: 0 }; if (!labelMap[label]) labelMap[label] = { count: 0, runs: 0, success: 0, totalTime: 0 };
labelMap[label].count += 1; labelMap[label].count += 1;
@ -372,7 +379,7 @@ export default (app: Router) => {
code: 200, code: 200,
data: { data: {
platform: os.platform(), platform: os.platform(),
uptime: Math.floor(os.uptime()), uptime: Math.floor(process.uptime()),
memTotal: os.totalmem(), memTotal: os.totalmem(),
memFree: os.freemem(), memFree: os.freemem(),
memUsagePercent: ((1 - os.freemem() / os.totalmem()) * 100).toFixed(1), memUsagePercent: ((1 - os.freemem() / os.totalmem()) * 100).toFixed(1),

View File

@ -1,6 +1,5 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { createRandomString } from './share';
dotenv.config({ dotenv.config({
path: path.join(__dirname, '../../.env'), 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 sampleFile = path.join(samplePath, 'config.sample.sh');
const sqliteFile = path.join(samplePath, 'database.sqlite'); const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'version.yaml'); const versionFile = path.join(rootPath, 'version.yaml');
const dataTgzFile = path.join(tmpPath, 'data.tgz'); const dataTgzFile = path.join(tmpPath, 'data.tgz');
@ -142,8 +139,6 @@ export default {
shareShellFile, shareShellFile,
dependenceProxyFile, dependenceProxyFile,
configString, configString,
loginFaild,
authError,
logPath, logPath,
extraFile, extraFile,
authConfigFile, authConfigFile,

View File

@ -536,7 +536,7 @@ export function safeJSONParse(value?: string) {
try { try {
return JSON.parse(value); return JSON.parse(value);
} catch (error) { } catch (error) {
Logger.error('[safeJSONParse失败]', error); Logger.error('[safeJSONParse error]', error);
return {}; return {};
} }
} }
@ -548,7 +548,7 @@ export async function rmPath(path: string) {
await fs.rm(path, { force: true, recursive: true, maxRetries: 5 }); await fs.rm(path, { force: true, recursive: true, maxRetries: 5 });
} }
} catch (error) { } catch (error) {
Logger.error('[rmPath失败]', error); Logger.error('[rmPath error]', error);
} }
} }
@ -563,7 +563,7 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
return true; return true;
} catch (error) { } catch (error) {
Logger.error('[setSystemTimezone失败]', error); Logger.error('[setSystemTimezone error]', error);
return false; return false;
} }
} }

View File

@ -2,11 +2,13 @@ export class SockMessage {
message?: string; message?: string;
type?: SockMessageType; type?: SockMessageType;
references?: number[]; references?: number[];
status?: number | string;
constructor(options: SockMessage) { constructor(options: SockMessage) {
this.type = options.type; this.type = options.type;
this.message = options.message; this.message = options.message;
this.references = options.references; this.references = options.references;
this.status = options.status;
} }
} }

View File

@ -6,6 +6,7 @@ import SshKeyService from '../services/sshKey';
import config from '../config'; import config from '../config';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
import { join } from 'path'; import { join } from 'path';
import { t } from '../shared/i18n';
export default async () => { export default async () => {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
@ -25,7 +26,7 @@ export default async () => {
} }
const cron = { const cron = {
id: NaN, id: NaN,
name: '生成token', name: t('生成token'),
command: tokenCommand, command: tokenCommand,
runOrigin: 'system', runOrigin: 'system',
} as ScheduleTaskType; } as ScheduleTaskType;
@ -44,7 +45,7 @@ export default async () => {
if (data.info.logRemoveFrequency) { if (data.info.logRemoveFrequency) {
const rmlogCron = { const rmlogCron = {
id: data.id as number, id: data.id as number,
name: '删除日志', name: t('删除日志'),
command: `ql rmlog ${data.info.logRemoveFrequency}`, command: `ql rmlog ${data.info.logRemoveFrequency}`,
runOrigin: 'system' as const, runOrigin: 'system' as const,
}; };

View File

@ -712,23 +712,27 @@ export default class CronService {
await this.setCrontab(); await this.setCrontab();
} }
public async log(id: number) { public async log(id: number): Promise<{ content: string; status: string }> {
const doc = await this.getDb({ id }); const doc = await this.getDb({ id });
if (!doc) { if (!doc) {
return ''; return { content: '', status: 'empty' };
} }
if (doc.log_name === '/dev/null') { if (doc.log_name === '/dev/null') {
return '日志设置为忽略'; return { content: t('日志设置为忽略'), status: 'ignored' };
} }
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath)); const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) { 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 { } else {
return typeof doc.status === 'number' && return typeof doc.status === 'number' &&
[CrontabStatus.queued, CrontabStatus.running].includes(doc.status) [CrontabStatus.queued, CrontabStatus.running].includes(doc.status)
? '运行中...' ? { content: t('运行中...'), status: 'running' }
: '日志不存在...'; : { content: t('日志不存在...'), status: 'notFound' };
} }
} }

View File

@ -24,6 +24,7 @@ import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
import { detectOS } from '../config/util'; import { detectOS } from '../config/util';
import { LINUX_DEPENDENCE_COMMAND } from '../config/const'; import { LINUX_DEPENDENCE_COMMAND } from '../config/const';
import { t, tf } from '../shared/i18n';
@Service() @Service()
export default class DependenceService { export default class DependenceService {
@ -232,7 +233,7 @@ export default class DependenceService {
} }
const depIds = [dependency.id!]; const depIds = [dependency.id!];
let depName = dependency.name.trim(); let depName = dependency.name.trim();
const actionText = isInstall ? '安装' : '删除'; const actionText = isInstall ? t('安装') : t('删除');
const socketMessageType = isInstall const socketMessageType = isInstall
? 'installDependence' ? 'installDependence'
: 'uninstallDependence'; : 'uninstallDependence';
@ -249,15 +250,20 @@ export default class DependenceService {
{ where: { id: depIds } }, { where: { id: depIds } },
); );
const startTime = dayjs(); const startTime = dayjs();
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format( const message = tf(
'YYYY-MM-DD HH:mm:ss', '开始%s依赖 %s开始时间 %s\n\n当前系统不支持\n\n依赖%s失败结束时间 %s耗时 %s 秒',
)}\n\n当前系统不支持\n\n依赖${actionText} ${startTime.format( actionText,
'YYYY-MM-DD HH:mm:ss', depName,
)} ${startTime.diff(startTime, 'second')} `; 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({ this.sockService.sendMessage({
type: socketMessageType, type: socketMessageType,
message, message,
references: depIds, references: depIds,
status: DependenceStatus.installFailed,
}); });
this.updateLog(depIds, message); this.updateLog(depIds, message);
return resolve(null); return resolve(null);
@ -280,13 +286,17 @@ export default class DependenceService {
} }
const startTime = dayjs(); const startTime = dayjs();
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format( const message = tf(
'YYYY-MM-DD HH:mm:ss', '开始%s依赖 %s开始时间 %s\n\n',
)}\n\n`; actionText,
depName,
startTime.format('YYYY-MM-DD HH:mm:ss'),
);
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: socketMessageType,
message, message,
references: depIds, references: depIds,
status,
}); });
this.updateLog(depIds, message); this.updateLog(depIds, message);
@ -322,13 +332,19 @@ export default class DependenceService {
(!depVersion || depInfo.includes(depVersion)) (!depVersion || depInfo.includes(depVersion))
) { ) {
const endTime = dayjs(); const endTime = dayjs();
const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format( const _message = tf(
'YYYY-MM-DD HH:mm:ss', '检测到已经安装 %s\n\n%s\n\n跳过安装\n\n依赖%s成功结束时间 %s耗时 %s 秒',
)} ${endTime.diff(startTime, 'second')} `; depName,
depInfo,
actionText,
endTime.format('YYYY-MM-DD HH:mm:ss'),
String(endTime.diff(startTime, 'second')),
);
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: socketMessageType,
message: _message, message: _message,
references: depIds, references: depIds,
status: DependenceStatus.installed,
}); });
this.updateLog(depIds, _message); this.updateLog(depIds, _message);
await DependenceModel.update( await DependenceModel.update(
@ -353,6 +369,7 @@ export default class DependenceService {
type: socketMessageType, type: socketMessageType,
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
status,
}); });
this.updateLog(depIds, data.toString()); this.updateLog(depIds, data.toString());
}); });
@ -362,6 +379,7 @@ export default class DependenceService {
type: socketMessageType, type: socketMessageType,
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
status,
}); });
this.updateLog(depIds, data.toString()); this.updateLog(depIds, data.toString());
}); });
@ -371,6 +389,7 @@ export default class DependenceService {
type: socketMessageType, type: socketMessageType,
message: JSON.stringify(err), message: JSON.stringify(err),
references: depIds, references: depIds,
status,
}); });
this.updateLog(depIds, JSON.stringify(err)); this.updateLog(depIds, JSON.stringify(err));
}); });
@ -378,28 +397,27 @@ export default class DependenceService {
cp.on('exit', async (code) => { cp.on('exit', async (code) => {
const endTime = dayjs(); const endTime = dayjs();
const isSucceed = code === 0; const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败'; const resultText = isSucceed ? t('成功') : t('失败');
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format( const message =
'YYYY-MM-DD HH:mm:ss', '\n' +
)} ${endTime.diff(startTime, 'second')} `; 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({ this.sockService.sendMessage({
type: socketMessageType, type: socketMessageType,
message, message,
references: depIds, references: depIds,
status: exitStatus,
}); });
this.updateLog(depIds, message); 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 docs = await DependenceModel.findAll({ where: { id: depIds } });
const _docIds = docs const _docIds = docs
.filter((x) => x.status !== DependenceStatus.cancelled) .filter((x) => x.status !== DependenceStatus.cancelled)
@ -407,7 +425,7 @@ export default class DependenceService {
if (_docIds.length > 0) { if (_docIds.length > 0) {
await DependenceModel.update( await DependenceModel.update(
{ status }, { status: exitStatus },
{ where: { id: _docIds } }, { where: { id: _docIds } },
); );
} }

View File

@ -365,7 +365,7 @@ export default class NotificationService {
{ {
title: `${this.title}`, title: `${this.title}`,
thumb_media_id, thumb_media_id,
author: `智能助手`, author: t('智能助手'),
content_source_url: ``, content_source_url: ``,
content: `${this.content.replace(/\n/g, '<br/>')}`, content: `${this.content.replace(/\n/g, '<br/>')}`,
digest: `${this.content}`, digest: `${this.content}`,
@ -381,7 +381,7 @@ export default class NotificationService {
title: `${this.title}`, title: `${this.title}`,
description: `${this.content}`, description: `${this.content}`,
url: 'https://github.com/whyour/qinglong', url: 'https://github.com/whyour/qinglong',
btntxt: '更多', btntxt: t('更多'),
}, },
}; };
break; break;
@ -432,7 +432,7 @@ export default class NotificationService {
roomName: `${aibotkName}`, roomName: `${aibotkName}`,
message: { message: {
type: 1, type: 1,
content: `青龙快讯】\n\n${this.title}\n${this.content}`, content: `${t('青龙快讯')}\n\n${this.title}\n${this.content}`,
}, },
}; };
break; break;
@ -443,7 +443,7 @@ export default class NotificationService {
name: `${aibotkName}`, name: `${aibotkName}`,
message: { message: {
type: 1, type: 1,
content: `青龙快讯】\n\n${this.title}\n${this.content}`, content: `${t('青龙快讯')}\n\n${this.title}\n${this.content}`,
}, },
}; };
break; break;
@ -613,7 +613,7 @@ export default class NotificationService {
}); });
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: `"青龙快讯" <${emailUser}>`, from: `"${t('青龙快讯')}" <${emailUser}>`,
to: recipients, to: recipients,
subject: `${this.title}`, subject: `${this.title}`,
html: `${this.content.replace(/\n/g, '<br/>')}`, html: `${this.content.replace(/\n/g, '<br/>')}`,

View File

@ -24,7 +24,7 @@ import path, { join } from 'path';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler'; import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock'; import SockService from './sock';
import { t } from '../shared/i18n'; import { t, tf } from '../shared/i18n';
import SshKeyService from './sshKey'; import SshKeyService from './sshKey';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const'; import { LOG_END_SYMBOL } from '../config/const';
@ -131,14 +131,14 @@ export default class SubscriptionService {
); );
const absolutePath = await handleLogPath( const absolutePath = await handleLogPath(
logPath as string, 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 // 执行sub_before
let beforeStr = ''; let beforeStr = '';
try { try {
if (doc.sub_before) { 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); beforeStr = await promiseExec(doc.sub_before);
} }
} catch (error: any) { } catch (error: any) {
@ -165,7 +165,7 @@ export default class SubscriptionService {
let afterStr = ''; let afterStr = '';
try { try {
if (sub.sub_after) { 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); afterStr = await promiseExec(sub.sub_after);
} }
} catch (error: any) { } catch (error: any) {
@ -178,9 +178,13 @@ export default class SubscriptionService {
await logStreamManager.write( await logStreamManager.write(
absolutePath, absolutePath,
`\n## 执行结束... ${endTime.format( '\n' +
'YYYY-MM-DD HH:mm:ss', tf(
)} ${diff} ${LOG_END_SYMBOL}`, '## 执行结束... %s 耗时 %s 秒',
endTime.format('YYYY-MM-DD HH:mm:ss'),
String(diff),
) +
LOG_END_SYMBOL,
); );
// Close the stream after task completion // Close the stream after task completion

View File

@ -78,8 +78,8 @@ export default class SystemService {
const code = Math.random().toString().slice(-6); const code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify( const isSuccess = await this.notificationService.testNotify(
notificationInfo, notificationInfo,
'青龙', t('青龙'),
`【蛟龙】测试通知 https://t.me/jiao_long`, t('【蛟龙】测试通知 https://t.me/jiao_long'),
); );
if (isSuccess) { if (isSuccess) {
const result = await this.updateAuthDb({ const result = await this.updateAuthDb({
@ -100,7 +100,7 @@ export default class SystemService {
}); });
const cron = { const cron = {
id: result.id as number, id: result.id as number,
name: '删除日志', name: t('删除日志'),
command: `ql rmlog ${info.logRemoveFrequency}`, command: `ql rmlog ${info.logRemoveFrequency}`,
runOrigin: 'system' as const, runOrigin: 'system' as const,
}; };
@ -179,6 +179,7 @@ export default class SystemService {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'updateNodeMirror', type: 'updateNodeMirror',
message: 'update node mirror end', message: 'update node mirror end',
status: 'completed',
}); });
}, },
onError: async (message: string) => { onError: async (message: string) => {
@ -232,6 +233,7 @@ export default class SystemService {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'updateLinuxMirror', type: 'updateLinuxMirror',
message: 'update linux mirror end', message: 'update linux mirror end',
status: 'completed',
}); });
onEnd?.(); onEnd?.();
if (!hasError) { if (!hasError) {
@ -340,6 +342,15 @@ export default class SystemService {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'updateSystemVersion', type: 'updateSystemVersion',
message: JSON.stringify(err), message: JSON.stringify(err),
status: 'failed',
});
});
cp.on('exit', (code) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: '',
status: code === 0 ? 'success' : 'failed',
}); });
}); });

View File

@ -25,7 +25,7 @@ import uniq from 'lodash/uniq';
import pickBy from 'lodash/pickBy'; import pickBy from 'lodash/pickBy';
import isNil from 'lodash/isNil'; import isNil from 'lodash/isNil';
import { shareStore } from '../shared/store'; import { shareStore } from '../shared/store';
import { t } from '../shared/i18n'; import { t, tf } from '../shared/i18n';
@Service() @Service()
export default class UserService { export default class UserService {
@ -67,7 +67,7 @@ export default class UserService {
); );
return { return {
code: 410, code: 410,
message: `失败次数过多,请${waitTime}秒后重试`, message: tf('失败次数过多,请%s秒后重试', waitTime),
data: waitTime, data: waitTime,
}; };
} }
@ -128,10 +128,19 @@ export default class UserService {
isTwoFactorChecking: false, isTwoFactorChecking: false,
}); });
this.notificationService.notify( this.notificationService.notify(
'登录通知', t('登录通知'),
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ t('你于') +
req.platform dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss') +
} ip地址 ${ip}`, t('在') +
address +
' ' +
req.platform +
t('端') +
' ' +
t('登录成功') +
t('ip地址') +
' ' +
ip,
); );
await this.insertDb({ await this.insertDb({
type: AuthDataType.loginLog, type: AuthDataType.loginLog,
@ -164,10 +173,19 @@ export default class UserService {
platform: req.platform, platform: req.platform,
}); });
this.notificationService.notify( this.notificationService.notify(
'登录通知', t('登录通知'),
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ t('你于') +
req.platform dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss') +
} ip地址 ${ip}`, t('在') +
address +
' ' +
req.platform +
t('端') +
' ' +
t('登录失败') +
t('ip地址') +
' ' +
ip,
); );
await this.insertDb({ await this.insertDb({
type: AuthDataType.loginLog, type: AuthDataType.loginLog,
@ -184,11 +202,11 @@ export default class UserService {
const waitTime = Math.round(Math.pow(3, retries + 1)); const waitTime = Math.round(Math.pow(3, retries + 1));
return { return {
code: 410, code: 410,
message: `失败次数过多,请${waitTime}秒后重试`, message: tf('失败次数过多,请%s秒后重试', waitTime),
data: waitTime, data: waitTime,
}; };
} else { } 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 code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify( const isSuccess = await this.notificationService.testNotify(
notificationInfo, notificationInfo,
'青龙', t('青龙'),
`【蛟龙】测试通知 https://t.me/jiao_long`, t('【蛟龙】测试通知 https://t.me/jiao_long'),
); );
if (isSuccess) { if (isSuccess) {
const result = await this.updateAuthDb({ const result = await this.updateAuthDb({

View File

@ -80,6 +80,55 @@ const messages: Record<string, Record<string, string>> = {
'订阅执行完成': 'Subscription completed', '订阅执行完成': 'Subscription completed',
'wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行': 'wxPusher requires at least one of TopicIds or Uids', 'wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行': 'wxPusher requires at least one of TopicIds or Uids',
'Url 或者 Body 中必须包含 $title': 'Url or Body must contain $title', '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; return key;
} }
export function tf(key: string, ...args: (string | number)[]): string {
return args.reduce<string>(
(str, arg) => str.replace(/%s|%d/, String(arg)),
t(key),
);
}

View File

@ -4,6 +4,7 @@ import { AuthDataType, SystemModel } from '../data/system';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { Dependence } from '../data/dependence'; import { Dependence } from '../data/dependence';
import NotificationService from '../services/notify'; import NotificationService from '../services/notify';
import { t, tf } from '../shared/i18n';
import { import {
ICronFn, ICronFn,
IDependencyFn, IDependencyFn,
@ -152,8 +153,14 @@ class TaskLimit {
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1); this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
this.client.systemNotify( this.client.systemNotify(
{ {
title: '任务重复运行', title: t('任务重复运行'),
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`, content: tf(
'任务:%s命令%s定时%s处于运行中的超过 %d 个,请检查定时设置',
cron.name || '',
cron.command || '',
cron.schedule || '',
5,
),
}, },
(err, res) => { (err, res) => {
if (err) { if (err) {

View File

@ -594,5 +594,11 @@
"黑名单": "Blacklist", "黑名单": "Blacklist",
"默认为 CPU 个数": "Default is the number of CPUs", "默认为 CPU 个数": "Default is the number of CPUs",
",保存后不可恢复": ", it can't be recovered after saving.", ",保存后不可恢复": ", 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"
} }

View File

@ -594,5 +594,11 @@
"黑名单": "黑名单", "黑名单": "黑名单",
"默认为 CPU 个数": "默认为 CPU 个数", "默认为 CPU 个数": "默认为 CPU 个数",
",保存后不可恢复": ",保存后不可恢复", ",保存后不可恢复": ",保存后不可恢复",
",删除后不可恢复": ",删除后不可恢复" ",删除后不可恢复": ",删除后不可恢复",
"日志不存在": "日志不存在",
"日志设置为忽略": "日志设置为忽略",
"确认保存": "确认保存",
"上传失败": "上传失败",
"成功上传": "成功上传",
"个环境变量": "个环境变量"
} }

View File

@ -298,7 +298,7 @@ const CronDetailModal = ({
const saveFile = () => { const saveFile = () => {
Modal.confirm({ Modal.confirm({
title: `确认保存`, title: intl.get('确认保存'),
content: ( content: (
<> <>
{intl.get('确认保存文件')} {intl.get('确认保存文件')}
@ -323,7 +323,7 @@ const CronDetailModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setValue(content); setValue(content);
message.success(`保存成功`); message.success(intl.get('保存成功'));
} }
resolve(null); resolve(null);
}) })

View File

@ -46,17 +46,15 @@ const CronLogModal = ({
} }
request request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`) .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then(({ code, data }) => { .then(({ code, data, logStatus }) => {
if ( if (
code === 200 && code === 200 &&
localStorage.getItem("logCron") === uniqPath && localStorage.getItem("logCron") === uniqPath &&
data !== value data !== value
) { ) {
const log = data as string; const log = (data as string) || intl.get("暂无日志");
setValue(log || intl.get("暂无日志")); setValue(log);
const hasNext = Boolean( const hasNext = logStatus === 'running';
log && !logEnded(log) && !log.includes("日志不存在") && !log.includes("日志设置为忽略"),
);
if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) { if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) {
setTimeout(() => { setTimeout(() => {
autoScroll(); autoScroll();

View File

@ -465,49 +465,33 @@ const Dependence = () => {
}, [logDependence]); }, [logDependence]);
const handleMessage = useCallback((payload: any) => { const handleMessage = useCallback((payload: any) => {
const { message, references } = payload; const { references, status } = payload;
let status: number | undefined = undefined; if (typeof status !== 'number' || !references?.length) return;
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.删除失败;
}
if (status === Status.) { if (status === Status.) {
setTimeout(() => { setTimeout(() => {
setValue((p) => { setValue((p) => {
const _result = [...p]; const _result = [...p];
for (let i = 0; i < references.length; i++) { for (const refId of references) {
const index = p.findIndex((x) => x.id === references[i]); const index = p.findIndex((x) => x.id === refId);
if (index !== -1) { if (index !== -1) _result.splice(index, 1);
_result.splice(index, 1);
}
} }
return _result; return _result;
}); });
}, 300); }, 300);
return; return;
} }
}
if (typeof status === 'number') {
setValue((p) => { setValue((p) => {
const result = [...p]; const result = [...p];
for (let i = 0; i < references.length; i++) { for (const refId of references) {
const index = p.findIndex((x) => x.id === references[i]); const index = p.findIndex((x) => x.id === refId);
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, { ...p[index], status });
...p[index],
status,
});
} }
} }
return result; return result;
}); });
}
}, []); }, []);
useEffect(() => { useEffect(() => {

View File

@ -54,8 +54,11 @@ const DependenceLogModal = ({
) { ) {
const log = (data?.log || []).join('') as string; const log = (data?.log || []).join('') as string;
setValue(log); setValue(log);
setExecuting(!log.includes('结束时间')); const dbStatus = data?.status as number | undefined;
setIsRemoveFailed(log.includes('删除失败')); setExecuting(
dbStatus === Status. || dbStatus === Status.,
);
setIsRemoveFailed(dbStatus === Status.);
} }
}) })
.finally(() => { .finally(() => {
@ -94,16 +97,17 @@ const DependenceLogModal = ({
}, [dependence]); }, [dependence]);
const handleMessage = (payload: any) => { const handleMessage = (payload: any) => {
const { message, references } = payload; const { message, references, status } = payload;
if ( if (
references.length > 0 && !references?.length ||
references.includes(dependence.id) && !references.includes(dependence.id)
[Status., Status.].includes(dependence.status) ) return;
) {
if (message.includes('结束时间')) { if (typeof status === 'number') {
setExecuting(false); setExecuting(status === Status. || status === Status.);
setIsRemoveFailed(message.includes('删除失败')); setIsRemoveFailed(status === Status.);
} }
if (message) {
setValue((p) => `${p}${message}`); setValue((p) => `${p}${message}`);
} }
}; };

View File

@ -603,7 +603,7 @@ const Env = () => {
); );
if (code === 200) { if (code === 200) {
message.success(`成功上传${data.length}个环境变量`); message.success(`${intl.get('成功上传')}${data.length}${intl.get('个环境变量')}`);
getEnvs(); getEnvs();
} }
setImportLoading(false); setImportLoading(false);

View File

@ -131,7 +131,7 @@ const Log = () => {
const deleteFile = () => { const deleteFile = () => {
Modal.confirm({ Modal.confirm({
title: `确认删除`, title: intl.get('确认删除'),
content: ( content: (
<> <>
{intl.get('确认删除')} {intl.get('确认删除')}
@ -155,7 +155,7 @@ const Log = () => {
}) })
.then(({ code }) => { .then(({ code }) => {
if (code === 200) { if (code === 200) {
message.success(`删除成功`); message.success(intl.get('删除成功'));
let newData = [...data]; let newData = [...data];
if (currentNode.parent) { if (currentNode.parent) {
newData = depthFirstSearch( newData = depthFirstSearch(

View File

@ -249,7 +249,7 @@ const Script = () => {
const saveFile = () => { const saveFile = () => {
Modal.confirm({ Modal.confirm({
title: `确认保存`, title: intl.get('确认保存'),
content: ( content: (
<> <>
{intl.get('确认保存文件')} {intl.get('确认保存文件')}
@ -273,7 +273,7 @@ const Script = () => {
}) })
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
message.success(`保存成功`); message.success(intl.get('保存成功'));
setValue(content); setValue(content);
handleIsEditing(currentNode.title, false); handleIsEditing(currentNode.title, false);
} }
@ -287,7 +287,7 @@ const Script = () => {
const deleteFile = () => { const deleteFile = () => {
Modal.confirm({ Modal.confirm({
title: `确认删除`, title: intl.get('确认删除'),
content: ( content: (
<> <>
{intl.get('确认删除')} {intl.get('确认删除')}
@ -311,7 +311,7 @@ const Script = () => {
}) })
.then(({ code }) => { .then(({ code }) => {
if (code === 200) { if (code === 200) {
message.success(`删除成功`); message.success(intl.get('删除成功'));
let newData = [...data]; let newData = [...data];
if (currentNode.parent) { if (currentNode.parent) {
newData = depthFirstSearch( newData = depthFirstSearch(

View File

@ -13,6 +13,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const modalRef = useRef<any>(); const modalRef = useRef<any>();
const lastStatusRef = useRef<string | undefined>();
const checkUpgrade = () => { const checkUpgrade = () => {
if (updateLoading) return; if (updateLoading) return;
@ -166,7 +167,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
useEffect(() => { useEffect(() => {
if (!value) return; if (!value) return;
const updateFailed = value.includes("失败,请检查"); const updateFailed = lastStatusRef.current === 'failed';
modalRef.current.update({ modalRef.current.update({
maskClosable: updateFailed, maskClosable: updateFailed,
@ -184,26 +185,29 @@ const CheckUpdate = ({ systemInfo }: any) => {
}, [value]); }, [value]);
const handleMessage = useCallback((payload: any) => { const handleMessage = useCallback((payload: any) => {
let { message: _message } = payload; const { message: _message, status } = payload;
const updateFailed = _message.includes("失败,请检查");
if (updateFailed) { if (status === 'failed') {
lastStatusRef.current = 'failed';
message.error(intl.get("更新失败,请检查网络及日志或稍后再试")); message.error(intl.get("更新失败,请检查网络及日志或稍后再试"));
} }
if (status === 'success') {
lastStatusRef.current = 'success';
setTimeout(() => {
showReloadModal();
}, 1000);
}
setTimeout(() => { setTimeout(() => {
document document
.querySelector("#log-identifier") .querySelector("#log-identifier")
?.scrollIntoView({ behavior: "smooth" }); ?.scrollIntoView({ behavior: "smooth" });
}, 600); }, 600);
if (_message.includes("更新包下载成功")) { if (_message) {
setTimeout(() => {
showReloadModal();
}, 1000);
}
setValue((p) => `${p}${_message}`); setValue((p) => `${p}${_message}`);
}
}, []); }, []);
useEffect(() => { useEffect(() => {

View File

@ -75,12 +75,9 @@ const Dependence = () => {
}; };
const handleMessage = (payload: any) => { const handleMessage = (payload: any) => {
const { message } = payload; const { message, status } = payload;
setLog((p) => `${p}${message}`); if (message) setLog((p) => `${p}${message}`);
if ( if (status === 'completed') {
message.includes('update node mirror end') ||
message.includes('update linux mirror end')
) {
setLoading(false); setLoading(false);
} }
}; };
@ -109,7 +106,7 @@ const Dependence = () => {
ws.subscribe('updateLinuxMirror', handleMessage); ws.subscribe('updateLinuxMirror', handleMessage);
return () => { return () => {
ws.subscribe('updateNodeMirror', handleMessage); ws.unsubscribe('updateNodeMirror', handleMessage);
ws.unsubscribe('updateLinuxMirror', handleMessage); ws.unsubscribe('updateLinuxMirror', handleMessage);
}; };
}, []); }, []);

View File

@ -379,7 +379,7 @@ const Other = ({
showReloadModal(); showReloadModal();
} }
if (file.status === 'error') { if (file.status === 'error') {
message.error('上传失败'); message.error(intl.get('上传失败'));
} }
}} }}
name="data" name="data"

View File

@ -51,7 +51,7 @@ const SystemLog = ({ height, theme }: any) => {
const deleteLog = () => { const deleteLog = () => {
request.delete(`${config.apiPrefix}system/log`).then((x) => { request.delete(`${config.apiPrefix}system/log`).then((x) => {
message.success('删除成功'); message.success(intl.get('删除成功'));
refresh(); refresh();
}); });
}; };

View File

@ -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 onPaste = useCallback((e: any) => {
const text = e.clipboardData.getData('text') as string; const text = e.clipboardData.getData('text') as string;
if (text.startsWith('ql ')) { if (text.startsWith('ql ')) {
const [ const args = parseArgs(text);
, const type = args[1];
type, const url = args[2] || '';
url, const whitelist = args[3] || '';
whitelist, const blacklist = args[4] || '';
blacklist, const dependences = args[5] || '';
dependences, const branch = args[6] || '';
branch, const extensions = args[7] || '';
extensions,
] = text
.split(' ')
.map((x) => x.trim().replace(/\"/g, '').replace(/\'/, ''));
const _type = const _type =
type === 'raw' type === 'raw'
? 'file' ? 'file'