mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 09:28:34 +00:00
fix(webui): localize automation runtime labels
This commit is contained in:
parent
e08462ca30
commit
43830b7162
@ -872,11 +872,27 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
|
|||||||
by_id = {job["id"]: job for job in disabled.json()["jobs"]}
|
by_id = {job["id"]: job for job in disabled.json()["jobs"]}
|
||||||
assert by_id[user_job.id]["enabled"] is False
|
assert by_id[user_job.id]["enabled"] is False
|
||||||
|
|
||||||
|
disabled_run = await _http_get(
|
||||||
|
f"http://127.0.0.1:29932/api/webui/automations/run?id={user_job.id}",
|
||||||
|
headers=auth,
|
||||||
|
)
|
||||||
|
assert disabled_run.status_code == 409
|
||||||
|
|
||||||
protected_delete = await _http_get(
|
protected_delete = await _http_get(
|
||||||
"http://127.0.0.1:29932/api/webui/automations/delete?id=heartbeat",
|
"http://127.0.0.1:29932/api/webui/automations/delete?id=heartbeat",
|
||||||
headers=auth,
|
headers=auth,
|
||||||
)
|
)
|
||||||
assert protected_delete.status_code == 403
|
assert protected_delete.status_code == 403
|
||||||
|
protected_disable = await _http_get(
|
||||||
|
"http://127.0.0.1:29932/api/webui/automations/disable?id=heartbeat",
|
||||||
|
headers=auth,
|
||||||
|
)
|
||||||
|
assert protected_disable.status_code == 403
|
||||||
|
protected_run = await _http_get(
|
||||||
|
"http://127.0.0.1:29932/api/webui/automations/run?id=heartbeat",
|
||||||
|
headers=auth,
|
||||||
|
)
|
||||||
|
assert protected_run.status_code == 403
|
||||||
|
|
||||||
enabled = await _http_get(
|
enabled = await _http_get(
|
||||||
f"http://127.0.0.1:29932/api/webui/automations/enable?id={user_job.id}",
|
f"http://127.0.0.1:29932/api/webui/automations/enable?id={user_job.id}",
|
||||||
|
|||||||
@ -3590,7 +3590,7 @@ function AutomationRow({
|
|||||||
)}
|
)}
|
||||||
title={record.error || fmtDateTime(record.run_at_ms, locale)}
|
title={record.error || fmtDateTime(record.run_at_ms, locale)}
|
||||||
>
|
>
|
||||||
{record.status} · {formatAutomationRunDuration(record.duration_ms)}
|
{automationRunStatusLabel(record.status, tx)} · {formatAutomationRunDuration(record.duration_ms, locale, tx)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -3767,7 +3767,7 @@ function formatAutomationSchedule(
|
|||||||
}
|
}
|
||||||
if (job.schedule.kind === "every" && job.schedule.every_ms) {
|
if (job.schedule.kind === "every" && job.schedule.every_ms) {
|
||||||
return tx("settings.automations.schedule.every", "Every {{duration}}", {
|
return tx("settings.automations.schedule.every", "Every {{duration}}", {
|
||||||
duration: formatAutomationInterval(job.schedule.every_ms),
|
duration: formatAutomationInterval(job.schedule.every_ms, locale),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (job.schedule.kind === "cron" && job.schedule.expr) {
|
if (job.schedule.kind === "cron" && job.schedule.expr) {
|
||||||
@ -3797,27 +3797,59 @@ function formatAutomationLast(
|
|||||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||||
): string {
|
): string {
|
||||||
if (!job.state.last_run_at_ms) return tx("settings.automations.last.never", "Never");
|
if (!job.state.last_run_at_ms) return tx("settings.automations.last.never", "Never");
|
||||||
const status = job.state.last_status || tx("settings.automations.last.unknown", "unknown");
|
const status = automationRunStatusLabel(job.state.last_status, tx);
|
||||||
return `${fmtDateTime(job.state.last_run_at_ms, locale)} · ${status}`;
|
return `${fmtDateTime(job.state.last_run_at_ms, locale)} · ${status}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAutomationInterval(ms: number): string {
|
function automationRunStatusLabel(
|
||||||
const units: Array<[string, number]> = [
|
status: string | null | undefined,
|
||||||
["d", 86_400_000],
|
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||||
["h", 3_600_000],
|
): string {
|
||||||
["m", 60_000],
|
if (status === "ok") return tx("settings.automations.history.ok", "Completed");
|
||||||
["s", 1000],
|
if (status === "error") return tx("settings.automations.history.error", "Error");
|
||||||
];
|
if (status === "skipped") return tx("settings.automations.history.skipped", "Skipped");
|
||||||
for (const [suffix, size] of units) {
|
return status || tx("settings.automations.last.unknown", "unknown");
|
||||||
if (ms >= size && ms % size === 0) return `${ms / size}${suffix}`;
|
|
||||||
}
|
|
||||||
return `${Math.round(ms / 1000)}s`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAutomationRunDuration(ms: number | undefined): string {
|
function formatAutomationUnit(
|
||||||
if (!ms || ms < 1000) return "<1s";
|
value: number,
|
||||||
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
|
unit: Intl.NumberFormatOptions["unit"],
|
||||||
return `${Math.round(ms / 60_000)}m`;
|
locale: string,
|
||||||
|
maximumFractionDigits = 0,
|
||||||
|
): string {
|
||||||
|
return new Intl.NumberFormat(locale, {
|
||||||
|
style: "unit",
|
||||||
|
unit,
|
||||||
|
unitDisplay: "long",
|
||||||
|
maximumFractionDigits,
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAutomationInterval(ms: number, locale: string): string {
|
||||||
|
const units: Array<[Intl.NumberFormatOptions["unit"], number]> = [
|
||||||
|
["day", 86_400_000],
|
||||||
|
["hour", 3_600_000],
|
||||||
|
["minute", 60_000],
|
||||||
|
["second", 1000],
|
||||||
|
];
|
||||||
|
for (const [unit, size] of units) {
|
||||||
|
if (ms >= size && ms % size === 0) return formatAutomationUnit(ms / size, unit, locale);
|
||||||
|
}
|
||||||
|
const fallbackUnit = ms < 60_000 ? "second" : "minute";
|
||||||
|
const fallbackSize = fallbackUnit === "second" ? 1000 : 60_000;
|
||||||
|
return formatAutomationUnit(ms / fallbackSize, fallbackUnit, locale, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAutomationRunDuration(
|
||||||
|
ms: number | undefined,
|
||||||
|
locale: string,
|
||||||
|
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||||
|
): string {
|
||||||
|
if (!ms || ms < 1000) {
|
||||||
|
return tx("settings.automations.duration.lessThanSecond", "< 1 second");
|
||||||
|
}
|
||||||
|
if (ms < 60_000) return formatAutomationUnit(ms / 1000, "second", locale, 1);
|
||||||
|
return formatAutomationUnit(ms / 60_000, "minute", locale, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppsCatalogSettings({
|
function AppsCatalogSettings({
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
"unknown": "unknown"
|
"unknown": "unknown"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "Completed",
|
||||||
|
"error": "Error",
|
||||||
|
"skipped": "Skipped"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "< 1 second"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "Nunca",
|
"never": "Nunca",
|
||||||
"unknown": "desconocido"
|
"unknown": "desconocido"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "Completada",
|
||||||
|
"error": "Error",
|
||||||
|
"skipped": "Omitida"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "menos de 1 segundo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "Jamais",
|
"never": "Jamais",
|
||||||
"unknown": "inconnu"
|
"unknown": "inconnu"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "Terminée",
|
||||||
|
"error": "Erreur",
|
||||||
|
"skipped": "Ignorée"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "moins de 1 seconde"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "Belum pernah",
|
"never": "Belum pernah",
|
||||||
"unknown": "tidak dikenal"
|
"unknown": "tidak dikenal"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "Selesai",
|
||||||
|
"error": "Error",
|
||||||
|
"skipped": "Dilewati"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "kurang dari 1 detik"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "未実行",
|
"never": "未実行",
|
||||||
"unknown": "不明"
|
"unknown": "不明"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "完了",
|
||||||
|
"error": "エラー",
|
||||||
|
"skipped": "スキップ"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "1 秒未満"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "실행된 적 없음",
|
"never": "실행된 적 없음",
|
||||||
"unknown": "알 수 없음"
|
"unknown": "알 수 없음"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "완료",
|
||||||
|
"error": "오류",
|
||||||
|
"skipped": "건너뜀"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "1초 미만"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "Chưa từng chạy",
|
"never": "Chưa từng chạy",
|
||||||
"unknown": "không xác định"
|
"unknown": "không xác định"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "Hoàn tất",
|
||||||
|
"error": "Lỗi",
|
||||||
|
"skipped": "Đã bỏ qua"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "dưới 1 giây"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "从未运行",
|
"never": "从未运行",
|
||||||
"unknown": "未知"
|
"unknown": "未知"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "完成",
|
||||||
|
"error": "错误",
|
||||||
|
"skipped": "已跳过"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "不到 1 秒"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -529,6 +529,14 @@
|
|||||||
"last": {
|
"last": {
|
||||||
"never": "從未執行",
|
"never": "從未執行",
|
||||||
"unknown": "未知"
|
"unknown": "未知"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"ok": "完成",
|
||||||
|
"error": "錯誤",
|
||||||
|
"skipped": "已略過"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"lessThanSecond": "不到 1 秒"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
|||||||
@ -405,7 +405,43 @@ describe("App layout", () => {
|
|||||||
await i18n.changeLanguage("zh-CN");
|
await i18n.changeLanguage("zh-CN");
|
||||||
mockFetchRoutes({
|
mockFetchRoutes({
|
||||||
"/api/settings": baseSettingsPayload(),
|
"/api/settings": baseSettingsPayload(),
|
||||||
"/api/webui/automations": { jobs: [] },
|
"/api/webui/automations": {
|
||||||
|
jobs: [
|
||||||
|
{
|
||||||
|
id: "job-zh",
|
||||||
|
name: "每日检查",
|
||||||
|
enabled: true,
|
||||||
|
protected: false,
|
||||||
|
delete_after_run: false,
|
||||||
|
schedule: { kind: "every", every_ms: 86_400_000 },
|
||||||
|
payload: {
|
||||||
|
message: "检查仓库状态",
|
||||||
|
kind: "agent_turn",
|
||||||
|
session_key: "websocket:chat-a",
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0),
|
||||||
|
last_run_at_ms: Date.UTC(2026, 3, 16, 10, 0, 0),
|
||||||
|
last_status: "ok",
|
||||||
|
pending: false,
|
||||||
|
run_history: [
|
||||||
|
{
|
||||||
|
run_at_ms: Date.UTC(2026, 3, 16, 10, 0, 0),
|
||||||
|
status: "ok",
|
||||||
|
duration_ms: 500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
origin: {
|
||||||
|
session_key: "websocket:chat-a",
|
||||||
|
channel: "websocket",
|
||||||
|
chat_id: "chat-a",
|
||||||
|
title: "发布准备",
|
||||||
|
preview: "检查发布阻塞项",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<App />);
|
render(<App />);
|
||||||
@ -419,7 +455,10 @@ describe("App layout", () => {
|
|||||||
expect(screen.getByText("统一查看 cron 提醒、周期性 agent 任务、一次性自动任务和受保护的系统自动任务。")).toBeInTheDocument();
|
expect(screen.getByText("统一查看 cron 提醒、周期性 agent 任务、一次性自动任务和受保护的系统自动任务。")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: "刷新" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "刷新" })).toBeInTheDocument();
|
||||||
expect(screen.getByText("任务队列")).toBeInTheDocument();
|
expect(screen.getByText("任务队列")).toBeInTheDocument();
|
||||||
expect(screen.getByText("暂无自动任务。")).toBeInTheDocument();
|
expect(screen.getByText("每日检查")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("检查仓库状态")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("每 1天")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("完成 · 不到 1 秒")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("Workspace automations")).not.toBeInTheDocument();
|
expect(screen.queryByText("Workspace automations")).not.toBeInTheDocument();
|
||||||
expect(document.title).toBe("自动任务 · nanobot");
|
expect(document.title).toBe("自动任务 · nanobot");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -58,6 +58,8 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [
|
|||||||
"settings.automations.systemTask",
|
"settings.automations.systemTask",
|
||||||
"settings.automations.labels.schedule",
|
"settings.automations.labels.schedule",
|
||||||
"settings.automations.status.active",
|
"settings.automations.status.active",
|
||||||
|
"settings.automations.history.ok",
|
||||||
|
"settings.automations.duration.lessThanSecond",
|
||||||
"settings.automations.deleteTitle",
|
"settings.automations.deleteTitle",
|
||||||
"settings.sections.interface",
|
"settings.sections.interface",
|
||||||
"settings.sections.localPreferences",
|
"settings.sections.localPreferences",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user