support reasoning agentid by accountId or session for cron (#847)

This commit is contained in:
Tao Yiping
2026-04-14 14:52:47 +08:00
committed by GitHub
Unverified
parent 54ec784545
commit 758a8f8c94
9 changed files with 276 additions and 389 deletions

View File

@@ -251,7 +251,6 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
const [name, setName] = useState(job?.name || '');
const [message, setMessage] = useState(job?.message || '');
const [selectedAgentId, setSelectedAgentId] = useState(job?.agentId || useChatStore.getState().currentAgentId);
const [agentIdChanged, setAgentIdChanged] = useState(false);
// Extract cron expression string from CronSchedule object or use as-is if string
const initialSchedule = (() => {
const s = job?.schedule;
@@ -411,7 +410,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
schedule: finalSchedule,
delivery: finalDelivery,
enabled,
...(agentIdChanged ? { agentId: selectedAgentId } : {}),
agentId: selectedAgentId,
});
onClose();
toast.success(job ? t('toast.updated') : t('toast.created'));
@@ -468,7 +467,6 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
value={selectedAgentId}
onChange={(e) => {
setSelectedAgentId(e.target.value);
setAgentIdChanged(true);
}}
className="h-[44px] rounded-xl border-black/10 dark:border-white/10 bg-[#eeece3] dark:bg-muted text-[13px]"
>

View File

@@ -11,7 +11,7 @@ interface CronState {
jobs: CronJob[];
loading: boolean;
error: string | null;
// Actions
fetchJobs: () => Promise<void>;
createJob: (input: CronJobCreateInput) => Promise<CronJob>;
@@ -26,7 +26,7 @@ export const useCronStore = create<CronState>((set) => ({
jobs: [],
loading: false,
error: null,
fetchJobs: async () => {
const currentJobs = useCronStore.getState().jobs;
// Only show loading spinner when there's no data yet (stale-while-revalidate).
@@ -39,49 +39,20 @@ export const useCronStore = create<CronState>((set) => ({
try {
const result = await hostApiFetch<CronJob[]>('/api/cron/jobs');
// If Gateway returned fewer jobs than we have, something might be wrong - preserve all known jobs
// and just update agentIds from localStorage for the ones Gateway returned.
// Priority: API agentId (if non-'main') > currentJobs > localStorage > 'main'
const resultIds = new Set(result.map(j => j.id));
const savedAgentIdMap = JSON.parse(localStorage.getItem('cronAgentIdMap') || '{}') as Record<string, string>;
// Gateway now correctly returns agentId for all jobs.
// If Gateway returned fewer jobs than we have (e.g. race condition), preserve
// the extra ones from current state to avoid losing data.
const resultIds = new Set(result.map((j) => j.id));
const extraJobs = currentJobs.filter((j) => !resultIds.has(j.id));
const allJobs = [...result, ...extraJobs];
// Update localStorage agentId map with current data
const newAgentIdMap: Record<string, string> = {};
// For jobs returned by Gateway, restore agentId
const jobsWithAgentId = result.map((job) => {
// Priority: API response (if non-'main') > currentJobs > localStorage > default 'main'
const existingJob = currentJobs.find((j) => j.id === job.id);
const savedAgentId = savedAgentIdMap[job.id];
let agentId = job.agentId;
if (!agentId || agentId === 'main') {
// API returned 'main' or nothing — use cached value
if (existingJob && existingJob.agentId !== 'main') {
agentId = existingJob.agentId;
} else if (savedAgentId && savedAgentId !== 'main') {
agentId = savedAgentId;
} else {
agentId = 'main';
}
}
if (agentId !== 'main') {
newAgentIdMap[job.id] = agentId;
}
return { ...job, agentId };
});
// If Gateway returned fewer jobs, preserve extra jobs from current state
const extraJobs = currentJobs.filter(j => !resultIds.has(j.id));
const allJobs = [...jobsWithAgentId, ...extraJobs];
localStorage.setItem('cronAgentIdMap', JSON.stringify(newAgentIdMap));
set({ jobs: allJobs, loading: false });
} catch (error) {
// Preserve previous jobs on error so the user sees stale data instead of nothing.
set({ error: String(error), loading: false });
}
},
createJob: async (input) => {
try {
// Auto-capture currentAgentId if not provided
@@ -90,59 +61,23 @@ export const useCronStore = create<CronState>((set) => ({
method: 'POST',
body: JSON.stringify({ ...input, agentId }),
});
const jobWithAgentId = { ...job, agentId };
// Persist agentId to localStorage (since Gateway doesn't return it)
const savedMap = JSON.parse(localStorage.getItem('cronAgentIdMap') || '{}') as Record<string, string>;
savedMap[jobWithAgentId.id] = agentId;
localStorage.setItem('cronAgentIdMap', JSON.stringify(savedMap));
set((state) => ({ jobs: [...state.jobs, jobWithAgentId] }));
return jobWithAgentId;
set((state) => ({ jobs: [...state.jobs, job] }));
return job;
} catch (error) {
console.error('Failed to create cron job:', error);
throw error;
}
},
updateJob: async (id, input) => {
try {
const currentJob = useCronStore.getState().jobs.find((j) => j.id === id);
const newAgentId = input.agentId;
// If agentId changed, recreate with new agentId first then delete old one (Gateway doesn't support updating sessionTarget)
if (newAgentId && currentJob && newAgentId !== currentJob.agentId) {
// Create new job with new agentId first (preserves schedule on failure)
const { agentId: _agentId, ...restInput } = input;
const newJob = await hostApiFetch<CronJob>('/api/cron/jobs', {
method: 'POST',
body: JSON.stringify({ ...restInput, agentId: newAgentId }),
});
const jobWithAgentId = { ...currentJob, ...newJob, agentId: newAgentId };
// Update localStorage: add new id first, then remove old id
const savedMap = JSON.parse(localStorage.getItem('cronAgentIdMap') || '{}') as Record<string, string>;
savedMap[jobWithAgentId.id] = newAgentId;
localStorage.setItem('cronAgentIdMap', JSON.stringify(savedMap));
// Delete old job after new one is created successfully
await hostApiFetch(`/api/cron/jobs/${encodeURIComponent(id)}`, {
method: 'DELETE',
});
delete savedMap[id];
localStorage.setItem('cronAgentIdMap', JSON.stringify(savedMap));
set((state) => ({
jobs: state.jobs.map((j) => (j.id === id ? jobWithAgentId : j)),
}));
return;
}
// Normal update for other fields - use currentJob as base, overlay updatedJob to preserve fields
const updatedJob = await hostApiFetch<CronJob>(`/api/cron/jobs/${encodeURIComponent(id)}`, {
method: 'PUT',
body: JSON.stringify(input),
});
// Merge: updatedJob fields override currentJob, but preserve currentJob fields not in updatedJob
const jobWithAgentId = { ...currentJob, ...updatedJob, agentId: currentJob?.agentId ?? updatedJob.agentId };
set((state) => ({
jobs: state.jobs.map((job) =>
job.id === id ? jobWithAgentId : job
job.id === id ? updatedJob : job
),
}));
} catch (error) {
@@ -150,16 +85,12 @@ export const useCronStore = create<CronState>((set) => ({
throw error;
}
},
deleteJob: async (id) => {
try {
await hostApiFetch(`/api/cron/jobs/${encodeURIComponent(id)}`, {
method: 'DELETE',
});
// Remove from localStorage
const savedMap = JSON.parse(localStorage.getItem('cronAgentIdMap') || '{}') as Record<string, string>;
delete savedMap[id];
localStorage.setItem('cronAgentIdMap', JSON.stringify(savedMap));
set((state) => ({
jobs: state.jobs.filter((job) => job.id !== id),
}));
@@ -168,7 +99,7 @@ export const useCronStore = create<CronState>((set) => ({
throw error;
}
},
toggleJob: async (id, enabled) => {
try {
await hostApiFetch('/api/cron/toggle', {
@@ -185,7 +116,7 @@ export const useCronStore = create<CronState>((set) => ({
throw error;
}
},
triggerJob: async (id) => {
try {
await hostApiFetch('/api/cron/trigger', {
@@ -194,14 +125,8 @@ export const useCronStore = create<CronState>((set) => ({
});
// Refresh jobs after trigger to update lastRun/nextRun state
try {
const currentJobs = useCronStore.getState().jobs;
const resultJobs = await hostApiFetch<CronJob[]>('/api/cron/jobs');
// Preserve agentId from existing jobs
const jobsWithAgentId = resultJobs.map((job) => {
const existing = currentJobs.find((j) => j.id === job.id);
return existing ? { ...job, agentId: existing.agentId } : job;
});
set({ jobs: jobsWithAgentId });
const result = await hostApiFetch<CronJob[]>('/api/cron/jobs');
set({ jobs: result });
} catch {
// Ignore refresh error
}
@@ -210,6 +135,6 @@ export const useCronStore = create<CronState>((set) => ({
throw error;
}
},
setJobs: (jobs) => set({ jobs }),
}));

View File

@@ -17,6 +17,7 @@ const LOAD_SESSIONS_MIN_INTERVAL_MS = 1_200;
const LOAD_HISTORY_MIN_INTERVAL_MS = 800;
let lastLoadSessionsAt = 0;
let lastLoadHistoryAt = 0;
let cronRepairTriggeredThisSession = false;
interface GatewayHealth {
ok: boolean;
@@ -262,6 +263,17 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
const unsubscribers: Array<() => void> = [];
unsubscribers.push(subscribeHostEvent<GatewayStatus>('gateway:status', (payload) => {
set({ status: payload });
// Trigger cron repair when gateway becomes ready
if (!cronRepairTriggeredThisSession && payload.state === 'running') {
cronRepairTriggeredThisSession = true;
// Fire-and-forget: fetch cron jobs to trigger repair logic in background
import('./cron')
.then(({ useCronStore }) => {
useCronStore.getState().fetchJobs();
})
.catch(() => {});
}
}));
unsubscribers.push(subscribeHostEvent<{ message?: string }>('gateway:error', (payload) => {
set({ lastError: payload.message || 'Gateway error' });