mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
i18n: add zh-CN for cron page and validation errors (#29315)
* i18n: add zh-CN for cron page and validation errors * cron: treat unexpected delivery statuses as unknown * test(cron): align validation tests with i18n keys --------- Co-authored-by: 周鹤0668001310 <zhou.he3@xydigit.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Changes
|
||||
|
||||
- Web UI/i18n: add German (`de`) locale support and auto-render language options from supported locale constants in Overview settings. (#28495) thanks @dsantoreis.
|
||||
- Web UI/Cron i18n: localize cron page labels, filters, form help text, and validation/error messaging in English and zh-CN. (#29315)
|
||||
- Discord/Thread bindings: replace fixed TTL lifecycle with inactivity (`idleHours`, default 24h) plus optional hard `maxAgeHours` lifecycle controls, and add `/session idle` + `/session max-age` commands for focused thread-bound sessions. (#27845) Thanks @osolmaz.
|
||||
- Android/Nodes: add `camera.list`, `device.permissions`, `device.health`, and `notifications.actions` (`open`/`dismiss`/`reply`) on Android nodes, plus first-class node-tool actions for the new device/notification commands. (#28260) Thanks @obviyus.
|
||||
- Android/Nodes parity: add `system.notify`, `photos.latest`, `contacts.search`/`contacts.add`, `calendar.events`/`calendar.add`, and `motion.activity`/`motion.pedometer`, with motion sensor-aware command gating and improved activity sampling reliability. (#29398) Thanks @obviyus.
|
||||
|
||||
@@ -120,4 +120,212 @@ export const en: TranslationMap = {
|
||||
ptBR: "Português (Brazilian Portuguese)",
|
||||
de: "Deutsch (German)",
|
||||
},
|
||||
cron: {
|
||||
summary: {
|
||||
enabled: "Enabled",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
jobs: "Jobs",
|
||||
nextWake: "Next wake",
|
||||
refreshing: "Refreshing...",
|
||||
refresh: "Refresh",
|
||||
},
|
||||
jobs: {
|
||||
title: "Jobs",
|
||||
subtitle: "All scheduled jobs stored in the gateway.",
|
||||
shownOf: "{shown} shown of {total}",
|
||||
searchJobs: "Search jobs",
|
||||
searchPlaceholder: "Name, description, or agent",
|
||||
enabled: "Enabled",
|
||||
all: "All",
|
||||
sort: "Sort",
|
||||
nextRun: "Next run",
|
||||
recentlyUpdated: "Recently updated",
|
||||
name: "Name",
|
||||
direction: "Direction",
|
||||
ascending: "Ascending",
|
||||
descending: "Descending",
|
||||
noMatching: "No matching jobs.",
|
||||
loading: "Loading...",
|
||||
loadMore: "Load more jobs",
|
||||
},
|
||||
runs: {
|
||||
title: "Run history",
|
||||
subtitleAll: "Latest runs across all jobs.",
|
||||
subtitleJob: "Latest runs for {title}.",
|
||||
scope: "Scope",
|
||||
allJobs: "All jobs",
|
||||
selectedJob: "Selected job",
|
||||
searchRuns: "Search runs",
|
||||
searchPlaceholder: "Summary, error, or job",
|
||||
newestFirst: "Newest first",
|
||||
oldestFirst: "Oldest first",
|
||||
status: "Status",
|
||||
delivery: "Delivery",
|
||||
clear: "Clear",
|
||||
allStatuses: "All statuses",
|
||||
allDelivery: "All delivery",
|
||||
selectJobHint: "Select a job to inspect run history.",
|
||||
noMatching: "No matching runs.",
|
||||
loadMore: "Load more runs",
|
||||
runStatusOk: "OK",
|
||||
runStatusError: "Error",
|
||||
runStatusSkipped: "Skipped",
|
||||
runStatusUnknown: "Unknown",
|
||||
deliveryDelivered: "Delivered",
|
||||
deliveryNotDelivered: "Not delivered",
|
||||
deliveryUnknown: "Unknown",
|
||||
deliveryNotRequested: "Not requested",
|
||||
},
|
||||
form: {
|
||||
editJob: "Edit Job",
|
||||
newJob: "New Job",
|
||||
updateSubtitle: "Update the selected scheduled job.",
|
||||
createSubtitle: "Create a scheduled wakeup or agent run.",
|
||||
required: "Required",
|
||||
requiredSr: "required",
|
||||
basics: "Basics",
|
||||
basicsSub: "Name it, choose the assistant, and set enabled state.",
|
||||
fieldName: "Name",
|
||||
description: "Description",
|
||||
agentId: "Agent ID",
|
||||
namePlaceholder: "Morning brief",
|
||||
descriptionPlaceholder: "Optional context for this job",
|
||||
agentPlaceholder: "main or ops",
|
||||
agentHelp: "Start typing to pick a known agent, or enter a custom one.",
|
||||
schedule: "Schedule",
|
||||
scheduleSub: "Control when this job runs.",
|
||||
every: "Every",
|
||||
at: "At",
|
||||
cronOption: "Cron",
|
||||
runAt: "Run at",
|
||||
unit: "Unit",
|
||||
minutes: "Minutes",
|
||||
hours: "Hours",
|
||||
days: "Days",
|
||||
expression: "Expression",
|
||||
expressionPlaceholder: "0 7 * * *",
|
||||
everyAmountPlaceholder: "30",
|
||||
timezoneOptional: "Timezone (optional)",
|
||||
timezonePlaceholder: "America/Los_Angeles",
|
||||
timezoneHelp: "Pick a common timezone or enter any valid IANA timezone.",
|
||||
jitterHelp: "Need jitter? Use Advanced → Stagger window / Stagger unit.",
|
||||
execution: "Execution",
|
||||
executionSub: "Choose when to wake, and what this job should do.",
|
||||
session: "Session",
|
||||
main: "Main",
|
||||
isolated: "Isolated",
|
||||
sessionHelp: "Main posts a system event. Isolated runs a dedicated agent turn.",
|
||||
wakeMode: "Wake mode",
|
||||
now: "Now",
|
||||
nextHeartbeat: "Next heartbeat",
|
||||
wakeModeHelp: "Now triggers immediately. Next heartbeat waits for the next cycle.",
|
||||
payloadKind: "What should run?",
|
||||
systemEvent: "Post message to main timeline",
|
||||
agentTurn: "Run assistant task (isolated)",
|
||||
systemEventHelp:
|
||||
"Sends your text to the gateway main timeline (good for reminders/triggers).",
|
||||
agentTurnHelp: "Starts an assistant run in its own session using your prompt.",
|
||||
timeoutSeconds: "Timeout (seconds)",
|
||||
timeoutPlaceholder: "Optional, e.g. 90",
|
||||
timeoutHelp:
|
||||
"Optional. Leave blank to use the gateway default timeout behavior for this run.",
|
||||
mainTimelineMessage: "Main timeline message",
|
||||
assistantTaskPrompt: "Assistant task prompt",
|
||||
deliverySection: "Delivery",
|
||||
deliverySub: "Choose where run summaries are sent.",
|
||||
resultDelivery: "Result delivery",
|
||||
announceDefault: "Announce summary (default)",
|
||||
webhookPost: "Webhook POST",
|
||||
noneInternal: "None (internal)",
|
||||
deliveryHelp: "Announce posts a summary to chat. None keeps execution internal.",
|
||||
webhookUrl: "Webhook URL",
|
||||
channel: "Channel",
|
||||
webhookPlaceholder: "https://example.com/cron",
|
||||
channelHelp: "Choose which connected channel receives the summary.",
|
||||
webhookHelp: "Send run summaries to a webhook endpoint.",
|
||||
to: "To",
|
||||
toPlaceholder: "+1555... or chat id",
|
||||
toHelp: "Optional recipient override (chat id, phone, or user id).",
|
||||
advanced: "Advanced",
|
||||
advancedHelp:
|
||||
"Optional overrides for delivery guarantees, schedule jitter, and model controls.",
|
||||
deleteAfterRun: "Delete after run",
|
||||
deleteAfterRunHelp: "Best for one-shot reminders that should auto-clean up.",
|
||||
clearAgentOverride: "Clear agent override",
|
||||
clearAgentHelp: "Force this job to use the gateway default assistant.",
|
||||
exactTiming: "Exact timing (no stagger)",
|
||||
exactTimingHelp: "Run on exact cron boundaries with no spread.",
|
||||
staggerWindow: "Stagger window",
|
||||
staggerUnit: "Stagger unit",
|
||||
staggerPlaceholder: "30",
|
||||
seconds: "Seconds",
|
||||
model: "Model",
|
||||
modelPlaceholder: "openai/gpt-5.2",
|
||||
modelHelp: "Start typing to pick a known model, or enter a custom one.",
|
||||
thinking: "Thinking",
|
||||
thinkingPlaceholder: "low",
|
||||
thinkingHelp: "Use a suggested level or enter a provider-specific value.",
|
||||
bestEffortDelivery: "Best effort delivery",
|
||||
bestEffortHelp: "Do not fail the job if delivery itself fails.",
|
||||
cantAddYet: "Can't add job yet",
|
||||
fillRequired: "Fill the required fields below to enable submit.",
|
||||
fixFields: "Fix {count} field to continue.",
|
||||
fixFieldsPlural: "Fix {count} fields to continue.",
|
||||
saving: "Saving...",
|
||||
saveChanges: "Save changes",
|
||||
addJob: "Add job",
|
||||
cancel: "Cancel",
|
||||
},
|
||||
jobList: {
|
||||
allJobs: "all jobs",
|
||||
selectJob: "(select a job)",
|
||||
enabled: "enabled",
|
||||
disabled: "disabled",
|
||||
edit: "Edit",
|
||||
clone: "Clone",
|
||||
disable: "Disable",
|
||||
enable: "Enable",
|
||||
run: "Run",
|
||||
history: "History",
|
||||
remove: "Remove",
|
||||
},
|
||||
jobDetail: {
|
||||
system: "System",
|
||||
prompt: "Prompt",
|
||||
delivery: "Delivery",
|
||||
agent: "Agent",
|
||||
},
|
||||
jobState: {
|
||||
status: "Status",
|
||||
next: "Next",
|
||||
last: "Last",
|
||||
},
|
||||
runEntry: {
|
||||
noSummary: "No summary.",
|
||||
runAt: "Run at",
|
||||
openRunChat: "Open run chat",
|
||||
next: "Next {rel}",
|
||||
due: "Due {rel}",
|
||||
},
|
||||
errors: {
|
||||
nameRequired: "Name is required.",
|
||||
scheduleAtInvalid: "Enter a valid date/time.",
|
||||
everyAmountInvalid: "Interval must be greater than 0.",
|
||||
cronExprRequired: "Cron expression is required.",
|
||||
staggerAmountInvalid: "Stagger must be greater than 0.",
|
||||
systemTextRequired: "System text is required.",
|
||||
agentMessageRequired: "Agent message is required.",
|
||||
timeoutInvalid: "If set, timeout must be greater than 0 seconds.",
|
||||
webhookUrlRequired: "Webhook URL is required.",
|
||||
webhookUrlInvalid: "Webhook URL must start with http:// or https://.",
|
||||
invalidRunTime: "Invalid run time.",
|
||||
invalidIntervalAmount: "Invalid interval amount.",
|
||||
cronExprRequiredShort: "Cron expression required.",
|
||||
invalidStaggerAmount: "Invalid stagger amount.",
|
||||
systemEventTextRequired: "System event text required.",
|
||||
agentMessageRequiredShort: "Agent message required.",
|
||||
nameRequiredShort: "Name required.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -119,4 +119,209 @@ export const zh_CN: TranslationMap = {
|
||||
ptBR: "Português (巴西葡萄牙语)",
|
||||
de: "Deutsch (德语)",
|
||||
},
|
||||
cron: {
|
||||
summary: {
|
||||
enabled: "已启用",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
jobs: "任务数",
|
||||
nextWake: "下次唤醒",
|
||||
refreshing: "刷新中...",
|
||||
refresh: "刷新",
|
||||
},
|
||||
jobs: {
|
||||
title: "任务列表",
|
||||
subtitle: "网关中存储的所有定时任务。",
|
||||
shownOf: "显示 {shown} / 共 {total}",
|
||||
searchJobs: "搜索任务",
|
||||
searchPlaceholder: "名称、描述或代理",
|
||||
enabled: "启用状态",
|
||||
all: "全部",
|
||||
sort: "排序",
|
||||
nextRun: "下次运行",
|
||||
recentlyUpdated: "最近更新",
|
||||
name: "名称",
|
||||
direction: "方向",
|
||||
ascending: "升序",
|
||||
descending: "降序",
|
||||
noMatching: "没有匹配的任务。",
|
||||
loading: "加载中...",
|
||||
loadMore: "加载更多任务",
|
||||
},
|
||||
runs: {
|
||||
title: "运行历史",
|
||||
subtitleAll: "所有任务的最新运行记录。",
|
||||
subtitleJob: "{title} 的最新运行记录。",
|
||||
scope: "范围",
|
||||
allJobs: "所有任务",
|
||||
selectedJob: "已选任务",
|
||||
searchRuns: "搜索运行",
|
||||
searchPlaceholder: "摘要、错误或任务",
|
||||
newestFirst: "最新优先",
|
||||
oldestFirst: "最早优先",
|
||||
status: "状态",
|
||||
delivery: "投递",
|
||||
clear: "清除",
|
||||
allStatuses: "全部状态",
|
||||
allDelivery: "全部投递",
|
||||
selectJobHint: "请选择一个任务以查看运行历史。",
|
||||
noMatching: "没有匹配的运行记录。",
|
||||
loadMore: "加载更多运行",
|
||||
runStatusOk: "成功",
|
||||
runStatusError: "错误",
|
||||
runStatusSkipped: "已跳过",
|
||||
runStatusUnknown: "未知",
|
||||
deliveryDelivered: "已投递",
|
||||
deliveryNotDelivered: "未投递",
|
||||
deliveryUnknown: "未知",
|
||||
deliveryNotRequested: "未请求",
|
||||
},
|
||||
form: {
|
||||
editJob: "编辑任务",
|
||||
newJob: "新建任务",
|
||||
updateSubtitle: "更新所选定时任务。",
|
||||
createSubtitle: "创建定时唤醒或代理运行。",
|
||||
required: "必填",
|
||||
requiredSr: "必填",
|
||||
basics: "基本信息",
|
||||
basicsSub: "命名、选择助手并设置启用状态。",
|
||||
fieldName: "名称",
|
||||
description: "描述",
|
||||
agentId: "代理 ID",
|
||||
namePlaceholder: "晨间简报",
|
||||
descriptionPlaceholder: "此任务的可选说明",
|
||||
agentPlaceholder: "main 或 ops",
|
||||
agentHelp: "输入以选择已知代理,或输入自定义 ID。",
|
||||
schedule: "调度",
|
||||
scheduleSub: "控制任务运行时间。",
|
||||
every: "每隔",
|
||||
at: "指定时间",
|
||||
cronOption: "Cron",
|
||||
runAt: "运行时间",
|
||||
unit: "单位",
|
||||
minutes: "分钟",
|
||||
hours: "小时",
|
||||
days: "天",
|
||||
expression: "表达式",
|
||||
expressionPlaceholder: "0 7 * * *",
|
||||
everyAmountPlaceholder: "30",
|
||||
timezoneOptional: "时区(可选)",
|
||||
timezonePlaceholder: "America/Los_Angeles",
|
||||
timezoneHelp: "选择常用时区或输入有效的 IANA 时区。",
|
||||
jitterHelp: "需要抖动?使用高级 → 抖动窗口 / 抖动单位。",
|
||||
execution: "执行",
|
||||
executionSub: "选择唤醒时机和任务执行内容。",
|
||||
session: "会话",
|
||||
main: "主会话",
|
||||
isolated: "隔离会话",
|
||||
sessionHelp: "主会话发布系统事件。隔离会话运行独立的代理轮次。",
|
||||
wakeMode: "唤醒模式",
|
||||
now: "立即",
|
||||
nextHeartbeat: "下次心跳",
|
||||
wakeModeHelp: "立即模式立即触发。下次心跳等待下一个周期。",
|
||||
payloadKind: "执行内容",
|
||||
systemEvent: "发布消息到主时间线",
|
||||
agentTurn: "运行助手任务(隔离)",
|
||||
systemEventHelp: "将文本发送到网关主时间线(适用于提醒/触发)。",
|
||||
agentTurnHelp: "使用您的提示在独立会话中启动助手运行。",
|
||||
timeoutSeconds: "超时(秒)",
|
||||
timeoutPlaceholder: "可选,如 90",
|
||||
timeoutHelp: "可选。留空以使用网关默认超时行为。",
|
||||
mainTimelineMessage: "主时间线消息",
|
||||
assistantTaskPrompt: "助手任务提示",
|
||||
deliverySection: "投递",
|
||||
deliverySub: "选择运行摘要的发送位置。",
|
||||
resultDelivery: "结果投递",
|
||||
announceDefault: "发布摘要(默认)",
|
||||
webhookPost: "Webhook POST",
|
||||
noneInternal: "无(仅内部)",
|
||||
deliveryHelp: "发布将摘要发送到聊天。无保持执行仅内部。",
|
||||
webhookUrl: "Webhook URL",
|
||||
channel: "频道",
|
||||
webhookPlaceholder: "https://example.com/cron",
|
||||
channelHelp: "选择接收摘要的已连接频道。",
|
||||
webhookHelp: "将运行摘要发送到 Webhook 端点。",
|
||||
to: "收件人",
|
||||
toPlaceholder: "+1555... 或聊天 ID",
|
||||
toHelp: "可选收件人覆盖(聊天 ID、电话或用户 ID)。",
|
||||
advanced: "高级",
|
||||
advancedHelp: "投递保证、调度抖动和模型控制的可选覆盖。",
|
||||
deleteAfterRun: "运行后删除",
|
||||
deleteAfterRunHelp: "适用于应自动清理的一次性提醒。",
|
||||
clearAgentOverride: "清除代理覆盖",
|
||||
clearAgentHelp: "强制此任务使用网关默认助手。",
|
||||
exactTiming: "精确时间(无抖动)",
|
||||
exactTimingHelp: "在精确的 cron 边界运行,无分散。",
|
||||
staggerWindow: "抖动窗口",
|
||||
staggerUnit: "抖动单位",
|
||||
staggerPlaceholder: "30",
|
||||
seconds: "秒",
|
||||
model: "模型",
|
||||
modelPlaceholder: "openai/gpt-5.2",
|
||||
modelHelp: "输入以选择已知模型,或输入自定义模型。",
|
||||
thinking: "思考",
|
||||
thinkingPlaceholder: "low",
|
||||
thinkingHelp: "使用建议级别或输入提供商特定值。",
|
||||
bestEffortDelivery: "尽力投递",
|
||||
bestEffortHelp: "投递失败时不使任务失败。",
|
||||
cantAddYet: "暂无法添加任务",
|
||||
fillRequired: "填写下方必填项以启用提交。",
|
||||
fixFields: "修复 {count} 个字段以继续。",
|
||||
fixFieldsPlural: "修复 {count} 个字段以继续。",
|
||||
saving: "保存中...",
|
||||
saveChanges: "保存更改",
|
||||
addJob: "添加任务",
|
||||
cancel: "取消",
|
||||
},
|
||||
jobList: {
|
||||
allJobs: "所有任务",
|
||||
selectJob: "(选择任务)",
|
||||
enabled: "已启用",
|
||||
disabled: "已禁用",
|
||||
edit: "编辑",
|
||||
clone: "克隆",
|
||||
disable: "禁用",
|
||||
enable: "启用",
|
||||
run: "运行",
|
||||
history: "历史",
|
||||
remove: "删除",
|
||||
},
|
||||
jobDetail: {
|
||||
system: "系统",
|
||||
prompt: "提示",
|
||||
delivery: "投递",
|
||||
agent: "代理",
|
||||
},
|
||||
jobState: {
|
||||
status: "状态",
|
||||
next: "下次",
|
||||
last: "上次",
|
||||
},
|
||||
runEntry: {
|
||||
noSummary: "无摘要。",
|
||||
runAt: "运行于",
|
||||
openRunChat: "打开运行聊天",
|
||||
next: "下次 {rel}",
|
||||
due: "到期 {rel}",
|
||||
},
|
||||
errors: {
|
||||
nameRequired: "名称为必填项。",
|
||||
scheduleAtInvalid: "请输入有效的日期/时间。",
|
||||
everyAmountInvalid: "间隔必须大于 0。",
|
||||
cronExprRequired: "Cron 表达式为必填项。",
|
||||
staggerAmountInvalid: "抖动值必须大于 0。",
|
||||
systemTextRequired: "系统文本为必填项。",
|
||||
agentMessageRequired: "代理消息为必填项。",
|
||||
timeoutInvalid: "若设置超时,必须大于 0 秒。",
|
||||
webhookUrlRequired: "Webhook URL 为必填项。",
|
||||
webhookUrlInvalid: "Webhook URL 必须以 http:// 或 https:// 开头。",
|
||||
invalidRunTime: "无效的运行时间。",
|
||||
invalidIntervalAmount: "无效的间隔值。",
|
||||
cronExprRequiredShort: "Cron 表达式为必填。",
|
||||
invalidStaggerAmount: "无效的抖动值。",
|
||||
systemEventTextRequired: "系统事件文本为必填。",
|
||||
agentMessageRequiredShort: "代理消息为必填。",
|
||||
nameRequiredShort: "名称为必填。",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -343,11 +343,11 @@ describe("cron controller", () => {
|
||||
deliveryMode: "webhook",
|
||||
deliveryTo: "ftp://bad",
|
||||
});
|
||||
expect(errors.name).toBeDefined();
|
||||
expect(errors.cronExpr).toBeDefined();
|
||||
expect(errors.payloadText).toBeDefined();
|
||||
expect(errors.timeoutSeconds).toBe("If set, timeout must be greater than 0 seconds.");
|
||||
expect(errors.deliveryTo).toBeDefined();
|
||||
expect(errors.name).toBe("cron.errors.nameRequired");
|
||||
expect(errors.cronExpr).toBe("cron.errors.cronExprRequired");
|
||||
expect(errors.payloadText).toBe("cron.errors.agentMessageRequired");
|
||||
expect(errors.timeoutSeconds).toBe("cron.errors.timeoutInvalid");
|
||||
expect(errors.deliveryTo).toBe("cron.errors.webhookUrlInvalid");
|
||||
});
|
||||
|
||||
it("blocks add/update submit when validation errors exist", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { DEFAULT_CRON_FORM } from "../app-defaults.ts";
|
||||
import { toNumber } from "../format.ts";
|
||||
import type { GatewayBrowserClient } from "../gateway.ts";
|
||||
@@ -95,28 +96,28 @@ export function normalizeCronFormState(form: CronFormState): CronFormState {
|
||||
export function validateCronForm(form: CronFormState): CronFieldErrors {
|
||||
const errors: CronFieldErrors = {};
|
||||
if (!form.name.trim()) {
|
||||
errors.name = "Name is required.";
|
||||
errors.name = "cron.errors.nameRequired";
|
||||
}
|
||||
if (form.scheduleKind === "at") {
|
||||
const ms = Date.parse(form.scheduleAt);
|
||||
if (!Number.isFinite(ms)) {
|
||||
errors.scheduleAt = "Enter a valid date/time.";
|
||||
errors.scheduleAt = "cron.errors.scheduleAtInvalid";
|
||||
}
|
||||
} else if (form.scheduleKind === "every") {
|
||||
const amount = toNumber(form.everyAmount, 0);
|
||||
if (amount <= 0) {
|
||||
errors.everyAmount = "Interval must be greater than 0.";
|
||||
errors.everyAmount = "cron.errors.everyAmountInvalid";
|
||||
}
|
||||
} else {
|
||||
if (!form.cronExpr.trim()) {
|
||||
errors.cronExpr = "Cron expression is required.";
|
||||
errors.cronExpr = "cron.errors.cronExprRequired";
|
||||
}
|
||||
if (!form.scheduleExact) {
|
||||
const staggerAmount = form.staggerAmount.trim();
|
||||
if (staggerAmount) {
|
||||
const stagger = toNumber(staggerAmount, 0);
|
||||
if (stagger <= 0) {
|
||||
errors.staggerAmount = "Stagger must be greater than 0.";
|
||||
errors.staggerAmount = "cron.errors.staggerAmountInvalid";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,24 +125,24 @@ export function validateCronForm(form: CronFormState): CronFieldErrors {
|
||||
if (!form.payloadText.trim()) {
|
||||
errors.payloadText =
|
||||
form.payloadKind === "systemEvent"
|
||||
? "System text is required."
|
||||
: "Agent message is required.";
|
||||
? "cron.errors.systemTextRequired"
|
||||
: "cron.errors.agentMessageRequired";
|
||||
}
|
||||
if (form.payloadKind === "agentTurn") {
|
||||
const timeoutRaw = form.timeoutSeconds.trim();
|
||||
if (timeoutRaw) {
|
||||
const timeout = toNumber(timeoutRaw, 0);
|
||||
if (timeout <= 0) {
|
||||
errors.timeoutSeconds = "If set, timeout must be greater than 0 seconds.";
|
||||
errors.timeoutSeconds = "cron.errors.timeoutInvalid";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (form.deliveryMode === "webhook") {
|
||||
const target = form.deliveryTo.trim();
|
||||
if (!target) {
|
||||
errors.deliveryTo = "Webhook URL is required.";
|
||||
errors.deliveryTo = "cron.errors.webhookUrlRequired";
|
||||
} else if (!/^https?:\/\//i.test(target)) {
|
||||
errors.deliveryTo = "Webhook URL must start with http:// or https://.";
|
||||
errors.deliveryTo = "cron.errors.webhookUrlInvalid";
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
@@ -428,14 +429,14 @@ export function buildCronSchedule(form: CronFormState) {
|
||||
if (form.scheduleKind === "at") {
|
||||
const ms = Date.parse(form.scheduleAt);
|
||||
if (!Number.isFinite(ms)) {
|
||||
throw new Error("Invalid run time.");
|
||||
throw new Error(t("cron.errors.invalidRunTime"));
|
||||
}
|
||||
return { kind: "at" as const, at: new Date(ms).toISOString() };
|
||||
}
|
||||
if (form.scheduleKind === "every") {
|
||||
const amount = toNumber(form.everyAmount, 0);
|
||||
if (amount <= 0) {
|
||||
throw new Error("Invalid interval amount.");
|
||||
throw new Error(t("cron.errors.invalidIntervalAmount"));
|
||||
}
|
||||
const unit = form.everyUnit;
|
||||
const mult = unit === "minutes" ? 60_000 : unit === "hours" ? 3_600_000 : 86_400_000;
|
||||
@@ -443,7 +444,7 @@ export function buildCronSchedule(form: CronFormState) {
|
||||
}
|
||||
const expr = form.cronExpr.trim();
|
||||
if (!expr) {
|
||||
throw new Error("Cron expression required.");
|
||||
throw new Error(t("cron.errors.cronExprRequiredShort"));
|
||||
}
|
||||
if (form.scheduleExact) {
|
||||
return { kind: "cron" as const, expr, tz: form.cronTz.trim() || undefined, staggerMs: 0 };
|
||||
@@ -454,7 +455,7 @@ export function buildCronSchedule(form: CronFormState) {
|
||||
}
|
||||
const staggerValue = toNumber(staggerAmount, 0);
|
||||
if (staggerValue <= 0) {
|
||||
throw new Error("Invalid stagger amount.");
|
||||
throw new Error(t("cron.errors.invalidStaggerAmount"));
|
||||
}
|
||||
const staggerMs = form.staggerUnit === "minutes" ? staggerValue * 60_000 : staggerValue * 1_000;
|
||||
return { kind: "cron" as const, expr, tz: form.cronTz.trim() || undefined, staggerMs };
|
||||
@@ -464,13 +465,13 @@ export function buildCronPayload(form: CronFormState) {
|
||||
if (form.payloadKind === "systemEvent") {
|
||||
const text = form.payloadText.trim();
|
||||
if (!text) {
|
||||
throw new Error("System event text required.");
|
||||
throw new Error(t("cron.errors.systemEventTextRequired"));
|
||||
}
|
||||
return { kind: "systemEvent" as const, text };
|
||||
}
|
||||
const message = form.payloadText.trim();
|
||||
if (!message) {
|
||||
throw new Error("Agent message required.");
|
||||
throw new Error(t("cron.errors.agentMessageRequiredShort"));
|
||||
}
|
||||
const payload: {
|
||||
kind: "agentTurn";
|
||||
@@ -540,7 +541,7 @@ export async function addCronJob(state: CronState) {
|
||||
delivery,
|
||||
};
|
||||
if (!job.name) {
|
||||
throw new Error("Name required.");
|
||||
throw new Error(t("cron.errors.nameRequiredShort"));
|
||||
}
|
||||
if (state.cronEditingJobId) {
|
||||
await state.client.request("cron.update", {
|
||||
|
||||
@@ -491,9 +491,9 @@ describe("cron view", () => {
|
||||
payloadText: "",
|
||||
},
|
||||
fieldErrors: {
|
||||
name: "Name is required.",
|
||||
cronExpr: "Cron expression is required.",
|
||||
payloadText: "Agent message is required.",
|
||||
name: "cron.errors.nameRequired",
|
||||
cronExpr: "cron.errors.cronExprRequired",
|
||||
payloadText: "cron.errors.agentMessageRequired",
|
||||
},
|
||||
canSubmit: false,
|
||||
}),
|
||||
@@ -527,9 +527,9 @@ describe("cron view", () => {
|
||||
payloadText: "",
|
||||
},
|
||||
fieldErrors: {
|
||||
name: "Name is required.",
|
||||
everyAmount: "Interval must be greater than 0.",
|
||||
payloadText: "Agent message is required.",
|
||||
name: "cron.errors.nameRequired",
|
||||
everyAmount: "cron.errors.everyAmountInvalid",
|
||||
payloadText: "cron.errors.agentMessageRequired",
|
||||
},
|
||||
canSubmit: false,
|
||||
}),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user