返回 DeepSeek-Reasonix
automationDrafts.ts
根目录 / desktop / frontend / src / store / automationDrafts.ts
1 import { create } from "zustand";
2 import type { HeartbeatTask } from "../custom/features/heartbeat/heartbeat.types";
3 import type { HeartbeatFrequencyType } from "../custom/features/heartbeat/heartbeat.presentation";
4 export const automationEditableFields = ["title", "prompt", "interval", "enabled", "scope", "workspaceRoot", "approvalMode", "newConversationEachRun", "notifyChannels", "timeWindowStart", "timeWindowEnd"] as const;
5 type Field = typeof automationEditableFields[number];
6 export type AutomationDraft = {
7 baseline: HeartbeatTask | null; draft: HeartbeatTask; conflicts: Field[];
8 missing: boolean; busy: boolean; error: boolean; version: number;
9 frequency: HeartbeatFrequencyType; tab: "configuration" | "history";
10 };
11 export function frequencyOf(interval: string): HeartbeatFrequencyType {
12 const cycle = interval.match(/\|(daily|weekly|biweekly|monthly|yearly)/)?.[1];
13 return cycle ? cycle as HeartbeatFrequencyType : interval.trim().split(/\s+/).length >= 5 ? "cron" : "interval";
14 }
15 export function automationDraftDirty(entry: AutomationDraft): boolean {
16 return !entry.baseline || automationEditableFields.some((key) => entry.draft[key] !== entry.baseline?.[key]);
17 }
18 function createEntry(task: HeartbeatTask, isNew: boolean): AutomationDraft {
19 return { baseline: isNew ? null : { ...task }, draft: { ...task }, conflicts: [], missing: false,
20 busy: false, error: false, version: 0, frequency: frequencyOf(task.interval), tab: "configuration" };
21 }
22 export function reconcileAutomationDraft(entry: AutomationDraft, current?: HeartbeatTask): AutomationDraft {
23 if (!entry.baseline) return current ? { ...entry, conflicts: [...automationEditableFields] } : entry;
24 if (!current) return { ...entry, missing: true };
25 const draft = { ...entry.draft };
26 const conflicts = new Set(entry.conflicts);
27 for (const key of automationEditableFields) {
28 const localChanged = draft[key] !== entry.baseline[key];
29 const remoteChanged = current[key] !== entry.baseline[key];
30 if (localChanged && remoteChanged && draft[key] !== current[key]) conflicts.add(key);
31 if (!localChanged) Object.assign(draft, { [key]: current[key] });
32 if (draft[key] === current[key]) conflicts.delete(key);
33 }
34 draft.topicId = current.topicId; draft.lastRunAt = current.lastRunAt; draft.runHistory = current.runHistory;
35 return { ...entry, baseline: { ...current }, draft, missing: false, conflicts: [...conflicts],
36 frequency: draft.interval === entry.draft.interval ? entry.frequency : frequencyOf(draft.interval) };
37 }
38 type Store = {
39 entries: Record<string, AutomationDraft>;
40 ensure: (task: HeartbeatTask, isNew?: boolean) => void;
41 edit: (id: string, update: (task: HeartbeatTask) => HeartbeatTask) => void;
42 ui: (id: string, patch: Partial<Pick<AutomationDraft, "frequency" | "tab">>) => void;
43 reconcile: (tasks: HeartbeatTask[]) => void;
44 begin: (id: string) => number | null;
45 finish: (id: string, version: number, saved?: HeartbeatTask) => void;
46 settle: (id: string, version: number, success: boolean) => void;
47 discard: (id: string) => void;
48 remove: (id: string) => void;
49 };
50 export const useAutomationDraftStore = create<Store>((set, get) => ({
51 entries: {},
52 ensure: (task, isNew = false) => { if (!get().entries[task.id]) set((s) => ({ entries: { ...s.entries, [task.id]: createEntry(task, isNew) } })); },
53 edit: (id, update) => set((s) => {
54 const entry = s.entries[id]; if (!entry || entry.busy) return s;
55 return { entries: { ...s.entries, [id]: { ...entry, draft: update(entry.draft), version: entry.version + 1 } } };
56 }),
57 ui: (id, patch) => set((s) => { const entry = s.entries[id]; return !entry || entry.busy ? s : { entries: { ...s.entries, [id]: { ...entry, ...patch } } }; }),
58 reconcile: (tasks) => set((s) => ({ entries: Object.fromEntries(Object.entries(s.entries).map(([id, entry]) => [id, reconcileAutomationDraft(entry, tasks.find((task) => task.id === id))])) })),
59 begin: (id) => {
60 const entry = get().entries[id]; if (!entry || entry.busy) return null;
61 const version = entry.version + 1;
62 set((s) => ({ entries: { ...s.entries, [id]: { ...entry, version, busy: true, error: false } } })); return version;
63 },
64 finish: (id, version, saved) => set((s) => {
65 const entry = s.entries[id]; if (!entry || entry.version !== version) return s;
66 return { entries: { ...s.entries, [id]: saved ? { ...createEntry(saved, false), version, tab: entry.tab } : { ...entry, busy: false, error: true } } };
67 }),
68 settle: (id, version, success) => set((s) => { const entry = s.entries[id]; return !entry || entry.version !== version ? s : { entries: { ...s.entries, [id]: { ...entry, busy: false, error: !success } } }; }),
69 discard: (id) => set((s) => {
70 const entry = s.entries[id]; if (!entry || entry.busy) return s;
71 const entries = { ...s.entries };
72 if (entry.baseline && !entry.missing) entries[id] = createEntry(entry.baseline, false); else delete entries[id];
73 return { entries };
74 }),
75 remove: (id) => set((s) => { const entries = { ...s.entries }; delete entries[id]; return { entries }; }),
76 }));
77
77 lines TYPESCRIPT