还没有选中调度方案
-
点击“新建”创建一个调度,然后拖入稳定版本脚本。
+
点击"新建"创建一个调度,然后拖入稳定版本脚本。
@@ -1558,381 +1347,3 @@ export default function SchedulePage({
);
}
-
-
-function RunHistory({
- runs,
- loading,
- disabled,
- onRefresh,
-}: {
- runs: ScheduleRunSummary[];
- loading: boolean;
- disabled: boolean;
- onRefresh: () => void;
-}) {
- return (
-
-
-
- 运行记录
- {runs.length}
-
-
-
-
- {loading && runs.length === 0 ? (
-
正在加载运行记录…
- ) : runs.length === 0 ? (
-
点击右上角“立即运行”后,这里会显示状态和耗时。
- ) : (
- runs.map((run) => (
-
-
-
-
{RUN_STATUS_LABELS[run.run_status]}
-
- {formatTime(run.queued_at)} · {formatDuration(run.duration_ms)}
-
- {run.error_message &&
{run.error_message}
}
-
- {run.run_id.slice(-8)}
-
- ))
- )}
-
-
- );
-}
-
-
-function ScheduleInspector({
- schedule,
- form,
- cronResult,
- busy,
- onChange,
- onPreview,
- onSave,
-}: {
- schedule: Schedule | null;
- form: ScheduleForm;
- cronResult: CronPreview | null;
- busy: boolean;
- onChange: (form: ScheduleForm) => void;
- onPreview: () => void;
- onSave: () => void;
-}) {
- if (!schedule) {
- return (
-
-
-
调度属性
-
选中调度方案后可配置触发方式、Cron 和执行策略。
-
- );
- }
- return (
-
-
-
-
-
-
-
-
-
- );
-}
-
-
-function NodeInspector({
- node,
- form,
- busy,
- onChange,
- onSave,
- onDelete,
-}: {
- node: ScheduleNode;
- form: NodeForm;
- busy: boolean;
- onChange: (form: NodeForm) => void;
- onSave: () => void;
- onDelete: () => void;
-}) {
- return (
-
-
-
-
-
-
- 节点配置
- {node.node_key}
-
-
-
- 稳定版本
-
- {node.version.script_name}
- {node.version.version_label}
- versions_id: {node.versions_id}
- SHA-256: {shortHash(node.version.content_hash)}
-
-
-
-
- 运行参数
-
-
-
-
-
-
-
-
- );
-}
-
-
-function withSuppressedError(action: () => Promise
): void {
- void action().catch(() => undefined);
-}
diff --git a/frontend/app/features/schedules/constants.ts b/frontend/app/features/schedules/constants.ts
new file mode 100644
index 0000000..a272801
--- /dev/null
+++ b/frontend/app/features/schedules/constants.ts
@@ -0,0 +1,36 @@
+// 调度页面常量
+
+export const CANVAS_WIDTH = 1400;
+export const CANVAS_HEIGHT = 860;
+export const NODE_WIDTH = 218;
+export const NODE_HEIGHT = 104;
+export const ARTIFACT_MIME = "application/x-model-platform-version";
+
+export const EMPTY_SCHEDULE_FORM = {
+ scheduleName: "",
+ description: "",
+ triggerType: "manual" as "manual" | "cron" | "api",
+ cronExpression: "0 9 * * *",
+ timezone: "Asia/Shanghai",
+ enabled: false,
+ maxConcurrency: "1",
+ failurePolicy: "stop" as "stop" | "continue",
+};
+
+export const EMPTY_NODE_FORM = {
+ nodeName: "",
+ timeoutSeconds: "600",
+ retryCount: "0",
+ retryIntervalSec: "5",
+ argumentsJson: "{}",
+ envRefsJson: "{}",
+};
+
+export const RUN_STATUS_LABELS: Record = {
+ queued: "排队中",
+ running: "运行中",
+ succeeded: "成功",
+ failed: "失败",
+ cancelled: "已取消",
+ timed_out: "已超时",
+};
diff --git a/frontend/app/features/schedules/utils.ts b/frontend/app/features/schedules/utils.ts
new file mode 100644
index 0000000..49a1092
--- /dev/null
+++ b/frontend/app/features/schedules/utils.ts
@@ -0,0 +1,95 @@
+import { type Schedule, type ScheduleArtifact, type ScheduleNode } from "../../services/api";
+import { EMPTY_SCHEDULE_FORM, EMPTY_NODE_FORM } from "./constants";
+
+export function formatTime(value: string | null): string {
+ if (!value) return "尚未执行";
+ return new Intl.DateTimeFormat("zh-CN", {
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ }).format(new Date(value));
+}
+
+export function shortHash(value: string): string {
+ return value ? `${value.slice(0, 7)}…${value.slice(-5)}` : "—";
+}
+
+export function formatDuration(value: number | null): string {
+ if (value === null) return "—";
+ if (value < 1000) return `${value} ms`;
+ if (value < 60_000) return `${(value / 1000).toFixed(1)} s`;
+ return `${Math.floor(value / 60_000)}m ${Math.round((value % 60_000) / 1000)}s`;
+}
+
+export function parseObject(text: string, label: string): Record {
+ let value: unknown;
+ try {
+ value = JSON.parse(text);
+ } catch {
+ throw new Error(`${label}必须是合法 JSON`);
+ }
+ if (!value || Array.isArray(value) || typeof value !== "object") {
+ throw new Error(`${label}必须是 JSON 对象`);
+ }
+ return value as Record;
+}
+
+export function scheduleToForm(schedule: Schedule) {
+ return {
+ scheduleName: schedule.schedule_name,
+ description: schedule.description ?? "",
+ triggerType: schedule.trigger_type,
+ cronExpression: schedule.cron_expression ?? "0 9 * * *",
+ timezone: schedule.timezone,
+ enabled: schedule.enabled,
+ maxConcurrency: String(schedule.max_concurrency),
+ failurePolicy: schedule.failure_policy,
+ };
+}
+
+export function nodeToForm(node: ScheduleNode) {
+ return {
+ nodeName: node.node_name,
+ timeoutSeconds: String(node.timeout_seconds),
+ retryCount: String(node.retry_count),
+ retryIntervalSec: String(node.retry_interval_sec),
+ argumentsJson: JSON.stringify(node.arguments_json ?? {}, null, 2),
+ envRefsJson: JSON.stringify(node.env_refs_json ?? {}, null, 2),
+ };
+}
+
+export function artifactNodeKey(
+ artifact: ScheduleArtifact,
+ schedule: Schedule,
+): string {
+ const ascii = artifact.script_name
+ .replace(/\.[^.]+$/, "")
+ .replace(/[^A-Za-z0-9_-]+/g, "_")
+ .replace(/^([^A-Za-z])/, "n_$1")
+ .replace(/^_+|_+$/g, "")
+ .slice(0, 48);
+ const base = ascii || `node_${artifact.versions_id.slice(-6).toLowerCase()}`;
+ const existing = new Set(schedule.nodes.map((item) => item.node_key));
+ if (!existing.has(base)) return base;
+ let index = 2;
+ while (existing.has(`${base}_${index}`)) index += 1;
+ return `${base}_${index}`.slice(0, 64);
+}
+
+export function edgePath(
+ source: ScheduleNode,
+ target: ScheduleNode,
+): string {
+ const x1 = source.position_x + 218;
+ const y1 = source.position_y + 52;
+ const x2 = target.position_x;
+ const y2 = target.position_y + 52;
+ const curve = Math.max(70, Math.abs(x2 - x1) * 0.45);
+ return `M ${x1} ${y1} C ${x1 + curve} ${y1}, ${x2 - curve} ${y2}, ${x2} ${y2}`;
+}
+
+export function withSuppressedError(action: () => Promise): void {
+ void action().catch(() => undefined);
+}