mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 06:16:10 +00:00
feat(cron): show command payloads in UI (#1286)
This commit is contained in:
1 parent
226e33ddfb
commit
99e2b7f851
8 files changed
+192
-34
No files matched your search
@@ -128,7 +128,22 @@
|
||||
"deleteAfterRun": "Delete after run",
|
||||
"deleteAfterRunDesc": "Auto-delete this job after first successful run",
|
||||
"timezone": "Timezone",
|
||||
"timezoneDesc": "Timezone for cron expressions"
|
||||
"timezoneDesc": "Timezone for cron expressions",
|
||||
"commandSection": "Command",
|
||||
"commandArgv": "Command",
|
||||
"noCommand": "No command configured",
|
||||
"commandCwd": "Working directory",
|
||||
"defaultWorkingDirectory": "Gateway default",
|
||||
"commandTimeout": "Timeout",
|
||||
"commandOutputLimit": "Output limit",
|
||||
"commandEnv": "Environment",
|
||||
"commandInput": "Input",
|
||||
"commandNoOutputTimeout": "No-output timeout",
|
||||
"seconds": "{{count}}s",
|
||||
"bytes": "{{count}} bytes",
|
||||
"defaultValue": "Default",
|
||||
"none": "None",
|
||||
"envVars": "{{count}} vars"
|
||||
},
|
||||
"stateless": "Stateless",
|
||||
"statelessHelp": "Skip session persistence. Each run starts fresh without loading previous messages. Saves tokens for crons that don't need past context.",
|
||||
@@ -145,5 +160,9 @@
|
||||
"failedRun": "Failed to run cron job",
|
||||
"updated": "Job updated",
|
||||
"failedUpdate": "Failed to update job"
|
||||
},
|
||||
"payload": {
|
||||
"agent": "Agent",
|
||||
"command": "Command"
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,22 @@
|
||||
"deleteAfterRun": "실행 후 삭제",
|
||||
"deleteAfterRunDesc": "첫 번째 성공적인 실행 후 이 작업 자동 삭제",
|
||||
"timezone": "시간대",
|
||||
"timezoneDesc": "Cron 표현식의 시간대"
|
||||
"timezoneDesc": "Cron 표현식의 시간대",
|
||||
"commandSection": "명령",
|
||||
"commandArgv": "명령",
|
||||
"noCommand": "설정된 명령 없음",
|
||||
"commandCwd": "작업 디렉터리",
|
||||
"defaultWorkingDirectory": "게이트웨이 기본값",
|
||||
"commandTimeout": "타임아웃",
|
||||
"commandOutputLimit": "출력 제한",
|
||||
"commandEnv": "환경 변수",
|
||||
"commandInput": "입력",
|
||||
"commandNoOutputTimeout": "무출력 타임아웃",
|
||||
"seconds": "{{count}}초",
|
||||
"bytes": "{{count}} bytes",
|
||||
"defaultValue": "기본값",
|
||||
"none": "없음",
|
||||
"envVars": "{{count}}개"
|
||||
},
|
||||
"stateless": "상태 없음",
|
||||
"statelessHelp": "세션 지속성 건너뜁니다. 각 실행은 이전 메시지 없이 새로 시작됩니다. 이전 컨텍스트가 필요 없는 cron의 토큰을 절약합니다.",
|
||||
@@ -145,5 +160,9 @@
|
||||
"failedRun": "Cron 작업 실행 실패",
|
||||
"updated": "작업 업데이트됨",
|
||||
"failedUpdate": "작업 업데이트 실패"
|
||||
},
|
||||
"payload": {
|
||||
"agent": "에이전트",
|
||||
"command": "명령"
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,22 @@
|
||||
"deleteAfterRun": "Xóa sau khi chạy",
|
||||
"deleteAfterRunDesc": "Tự động xóa công việc sau lần chạy thành công đầu tiên",
|
||||
"timezone": "Múi giờ",
|
||||
"timezoneDesc": "Múi giờ cho biểu thức cron"
|
||||
"timezoneDesc": "Múi giờ cho biểu thức cron",
|
||||
"commandSection": "Lệnh",
|
||||
"commandArgv": "Lệnh",
|
||||
"noCommand": "Chưa cấu hình lệnh",
|
||||
"commandCwd": "Thư mục làm việc",
|
||||
"defaultWorkingDirectory": "Mặc định gateway",
|
||||
"commandTimeout": "Thời gian chờ",
|
||||
"commandOutputLimit": "Giới hạn output",
|
||||
"commandEnv": "Môi trường",
|
||||
"commandInput": "Input",
|
||||
"commandNoOutputTimeout": "Timeout khi không có output",
|
||||
"seconds": "{{count}}s",
|
||||
"bytes": "{{count}} bytes",
|
||||
"defaultValue": "Mặc định",
|
||||
"none": "Không có",
|
||||
"envVars": "{{count}} biến"
|
||||
},
|
||||
"stateless": "Không trạng thái",
|
||||
"statelessHelp": "Bỏ qua lưu session. Mỗi lần chạy bắt đầu mới. Tiết kiệm token cho các cron không cần context cũ.",
|
||||
@@ -145,5 +160,9 @@
|
||||
"failedRun": "Không thể chạy tác vụ định kỳ",
|
||||
"updated": "Đã cập nhật job",
|
||||
"failedUpdate": "Cập nhật job thất bại"
|
||||
},
|
||||
"payload": {
|
||||
"agent": "Agent",
|
||||
"command": "Lệnh"
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,22 @@
|
||||
"deleteAfterRun": "运行后删除",
|
||||
"deleteAfterRunDesc": "首次成功运行后自动删除此任务",
|
||||
"timezone": "时区",
|
||||
"timezoneDesc": "Cron表达式的时区"
|
||||
"timezoneDesc": "Cron表达式的时区",
|
||||
"commandSection": "命令",
|
||||
"commandArgv": "命令",
|
||||
"noCommand": "未配置命令",
|
||||
"commandCwd": "工作目录",
|
||||
"defaultWorkingDirectory": "网关默认值",
|
||||
"commandTimeout": "超时",
|
||||
"commandOutputLimit": "输出限制",
|
||||
"commandEnv": "环境变量",
|
||||
"commandInput": "输入",
|
||||
"commandNoOutputTimeout": "无输出超时",
|
||||
"seconds": "{{count}}秒",
|
||||
"bytes": "{{count}} bytes",
|
||||
"defaultValue": "默认值",
|
||||
"none": "无",
|
||||
"envVars": "{{count}}个变量"
|
||||
},
|
||||
"stateless": "无状态",
|
||||
"statelessHelp": "跳过会话持久化。每次运行从零开始。为不需要历史上下文的定时任务节省令牌。",
|
||||
@@ -145,5 +160,9 @@
|
||||
"failedRun": "运行定时任务失败",
|
||||
"updated": "任务已更新",
|
||||
"failedUpdate": "更新任务失败"
|
||||
},
|
||||
"payload": {
|
||||
"agent": "Agent",
|
||||
"command": "命令"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Calendar, Clock, AlertTriangle, Pencil } from "lucide-react";
|
||||
import { Calendar, Clock, AlertTriangle, Pencil, Terminal } from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,7 +15,7 @@ import { useChannels } from "@/pages/channels/hooks/use-channels";
|
||||
import { useWs } from "@/hooks/use-ws";
|
||||
import { Methods } from "@/api/protocol";
|
||||
import type { CronJob, CronJobPatch } from "../hooks/use-cron";
|
||||
import { CronStatusBadge } from "../cron-utils";
|
||||
import { CronStatusBadge, formatCommand, isCommandCron } from "../cron-utils";
|
||||
import { useAgents } from "@/pages/agents/hooks/use-agents";
|
||||
import { CronScheduleSection } from "./cron-schedule-section";
|
||||
import { CronDeliverySection } from "./cron-delivery-section";
|
||||
@@ -66,6 +66,8 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
const [stateless, setStateless] = useState(job.stateless ?? false);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const command = job.payload?.command;
|
||||
const commandEnvCount = command?.env ? Object.keys(command.env).length : 0;
|
||||
|
||||
// Fetch delivery targets on mount
|
||||
const fetchTargets = useCallback(async () => {
|
||||
@@ -98,7 +100,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
}
|
||||
const patch: import("../hooks/use-cron").CronJobPatch = {
|
||||
schedule,
|
||||
message: message.trim(),
|
||||
message: isCommandCron(job) ? undefined : message.trim(),
|
||||
agentId: agentId.trim() || "",
|
||||
deliver,
|
||||
deliverChannel: deliver ? channel.trim() || undefined : undefined,
|
||||
@@ -134,31 +136,88 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
readonly={readonly}
|
||||
/>
|
||||
|
||||
{/* Message section */}
|
||||
<section className="space-y-3 rounded-lg border p-3 sm:p-4 overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">{t("detail.messageSection")}</h3>
|
||||
{!readonly && (
|
||||
<Button variant="ghost" size="sm" className="h-7 gap-1 text-xs text-muted-foreground"
|
||||
onClick={() => setEditingMessage(!editingMessage)}>
|
||||
<Pencil className="h-3 w-3" />
|
||||
{editingMessage ? t("detail.preview") : t("detail.edit")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{editingMessage ? (
|
||||
<Textarea value={message} onChange={(e) => setMessage(e.target.value)}
|
||||
rows={6} placeholder={t("create.messagePlaceholder")} className="text-base md:text-sm resize-none" />
|
||||
) : (
|
||||
<div className="rounded-md border bg-muted/30 p-3 sm:p-4">
|
||||
{message ? (
|
||||
<MarkdownRenderer content={message} className="prose-sm max-w-none" />
|
||||
) : (
|
||||
<p className="text-sm italic text-muted-foreground">{t("detail.noMessage")}</p>
|
||||
{isCommandCron(job) ? (
|
||||
<section className="space-y-3 rounded-lg border border-amber-200 bg-amber-50/40 p-3 sm:p-4 overflow-hidden dark:border-amber-900/50 dark:bg-amber-950/20">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
<h3 className="text-sm font-medium">{t("detail.commandSection")}</h3>
|
||||
</div>
|
||||
<span className="rounded-full border border-amber-300 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-700 dark:border-amber-800 dark:text-amber-300">
|
||||
{t("payload.command")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3 rounded-md border bg-background/80 p-3 sm:p-4">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-medium text-muted-foreground">{t("detail.commandArgv")}</div>
|
||||
{formatCommand(job) ? (
|
||||
<pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 text-xs"><code>{formatCommand(job)}</code></pre>
|
||||
) : (
|
||||
<p className="text-sm italic text-muted-foreground">{t("detail.noCommand")}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium text-muted-foreground">{t("detail.commandCwd")}</div>
|
||||
<div className="truncate text-sm">{command?.cwd || t("detail.defaultWorkingDirectory")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">{t("detail.commandTimeout")}</div>
|
||||
<div className="text-sm">{command?.timeoutSeconds ? t("detail.seconds", { count: command.timeoutSeconds }) : t("detail.defaultValue")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">{t("detail.commandOutputLimit")}</div>
|
||||
<div className="text-sm">{command?.outputMaxBytes ? t("detail.bytes", { count: command.outputMaxBytes }) : t("detail.defaultValue")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">{t("detail.commandEnv")}</div>
|
||||
<div className="text-sm">{commandEnvCount ? t("detail.envVars", { count: commandEnvCount }) : t("detail.none")}</div>
|
||||
</div>
|
||||
</div>
|
||||
{(command?.input || command?.noOutputTimeoutSeconds) && (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{command.input && (
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">{t("detail.commandInput")}</div>
|
||||
<pre className="max-h-32 overflow-auto rounded-md bg-muted px-3 py-2 text-xs"><code>{command.input}</code></pre>
|
||||
</div>
|
||||
)}
|
||||
{command.noOutputTimeoutSeconds ? (
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">{t("detail.commandNoOutputTimeout")}</div>
|
||||
<div className="text-sm">{t("detail.seconds", { count: command.noOutputTimeoutSeconds })}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
) : (
|
||||
<section className="space-y-3 rounded-lg border p-3 sm:p-4 overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">{t("detail.messageSection")}</h3>
|
||||
{!readonly && (
|
||||
<Button variant="ghost" size="sm" className="h-7 gap-1 text-xs text-muted-foreground"
|
||||
onClick={() => setEditingMessage(!editingMessage)}>
|
||||
<Pencil className="h-3 w-3" />
|
||||
{editingMessage ? t("detail.preview") : t("detail.edit")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{editingMessage ? (
|
||||
<Textarea value={message} onChange={(e) => setMessage(e.target.value)}
|
||||
rows={6} placeholder={t("create.messagePlaceholder")} className="text-base md:text-sm resize-none" />
|
||||
) : (
|
||||
<div className="rounded-md border bg-muted/30 p-3 sm:p-4">
|
||||
{message ? (
|
||||
<MarkdownRenderer content={message} className="prose-sm max-w-none" />
|
||||
) : (
|
||||
<p className="text-sm italic text-muted-foreground">{t("detail.noMessage")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Delivery section */}
|
||||
<CronDeliverySection
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { CronJob } from "./hooks/use-cron";
|
||||
import { formatSchedule, CronStatusBadge } from "./cron-utils";
|
||||
import { formatSchedule, CronStatusBadge, formatCommand, isCommandCron } from "./cron-utils";
|
||||
import { useAgents } from "@/pages/agents/hooks/use-agents";
|
||||
|
||||
interface CronListRowProps {
|
||||
@@ -38,6 +38,9 @@ export function CronListRow({ job, onClick, onRun, onDelete }: CronListRowProps)
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Badge variant="outline" className="text-2xs px-1 py-0">{job.schedule.kind}</Badge>
|
||||
<Badge variant={isCommandCron(job) ? "secondary" : "outline"} className="text-2xs px-1 py-0">
|
||||
{isCommandCron(job) ? t("payload.command") : t("payload.agent")}
|
||||
</Badge>
|
||||
<span className="truncate">{formatSchedule(job)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,9 +55,9 @@ export function CronListRow({ job, onClick, onRun, onDelete }: CronListRowProps)
|
||||
{agent?.display_name || agent?.agent_key || job.agentId || t("card.defaultAgent")}
|
||||
</div>
|
||||
|
||||
{/* Message preview */}
|
||||
{/* Message / command preview */}
|
||||
<div className="hidden shrink-0 text-xs text-muted-foreground/60 lg:block lg:w-40 lg:truncate">
|
||||
{job.payload?.message}
|
||||
{isCommandCron(job) ? formatCommand(job) : job.payload?.message}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { CronJob } from "./hooks/use-cron";
|
||||
|
||||
export function isCommandCron(job: CronJob): boolean {
|
||||
return job.payload?.kind === "command";
|
||||
}
|
||||
|
||||
export function formatCommand(job: CronJob): string {
|
||||
const argv = job.payload?.command?.argv;
|
||||
if (argv && argv.length > 0) return argv.join(" ");
|
||||
return "";
|
||||
}
|
||||
|
||||
export function formatSchedule(job: CronJob): string {
|
||||
const s = job.schedule;
|
||||
if (s.kind === "every" && s.everyMs) {
|
||||
|
||||
@@ -15,10 +15,20 @@ export interface CronSchedule {
|
||||
tz?: string;
|
||||
}
|
||||
|
||||
export interface CronCommandSpec {
|
||||
argv?: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
input?: string;
|
||||
timeoutSeconds?: number;
|
||||
noOutputTimeoutSeconds?: number;
|
||||
outputMaxBytes?: number;
|
||||
}
|
||||
|
||||
export interface CronPayload {
|
||||
kind: string;
|
||||
message: string;
|
||||
command?: string;
|
||||
command?: CronCommandSpec;
|
||||
}
|
||||
|
||||
export interface CronJobPatch {
|
||||
|
||||
Reference in new issue
Block a user