From 5f2bc950b179ae4b542006e753665baa3a2223fa Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Tue, 2 Jun 2026 17:04:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=92=8C=E8=81=8A=E5=A4=A9=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/command/handlers/list_scheduler_jobs.rs | 96 +++++++++ src/command/handlers/load_chat_messages.rs | 65 ++++++ src/command/handlers/mod.rs | 2 + src/command/mod.rs | 9 + src/gateway/ws.rs | 28 +++ src/protocol/mod.rs | 29 +++ web/src/App.tsx | 128 ++++++++++-- .../components/Sidebar/SchedulerJobList.tsx | 193 ++++++++++++++++++ web/src/hooks/useChat.ts | 99 ++++++++- web/src/types/protocol.ts | 39 ++++ 10 files changed, 669 insertions(+), 19 deletions(-) create mode 100644 src/command/handlers/list_scheduler_jobs.rs create mode 100644 src/command/handlers/load_chat_messages.rs create mode 100644 web/src/components/Sidebar/SchedulerJobList.tsx diff --git a/src/command/handlers/list_scheduler_jobs.rs b/src/command/handlers/list_scheduler_jobs.rs new file mode 100644 index 0000000..02332e3 --- /dev/null +++ b/src/command/handlers/list_scheduler_jobs.rs @@ -0,0 +1,96 @@ +use crate::command::context::CommandContext; +use crate::command::handler::{CommandHandler, CommandMetadata}; +use crate::command::response::{CommandError, CommandResponse}; +use crate::command::Command; +use crate::protocol::{SchedulerJobSessionLookup, SchedulerJobSummary}; +use crate::storage::SessionStore; +use async_trait::async_trait; +use std::sync::Arc; + +pub struct ListSchedulerJobsCommandHandler { + store: Arc, +} + +impl ListSchedulerJobsCommandHandler { + pub fn new(store: Arc) -> Self { + Self { store } + } +} + +#[async_trait] +impl CommandHandler for ListSchedulerJobsCommandHandler { + fn can_handle(&self, cmd: &Command) -> bool { + matches!(cmd, Command::ListSchedulerJobs) + } + + fn metadata(&self) -> Option { + Some(CommandMetadata { + name: "list_scheduler_jobs", + description: "列出所有定时任务", + usage: "/list_scheduler_jobs", + }) + } + + async fn handle( + &self, + _cmd: Command, + ctx: CommandContext, + ) -> Result { + let records = self + .store + .list_scheduler_jobs(false) + .map_err(|e| CommandError::new("LIST_JOBS_ERROR", e.to_string()))?; + + let summaries: Vec = records + .into_iter() + .map(|r| { + let session_lookup = build_session_lookup(&r); + + SchedulerJobSummary { + id: r.id, + kind: r.kind, + schedule: r.schedule, + enabled: r.enabled, + state: r.state.as_str().to_string(), + last_status: r.last_status.map(|s| s.as_str().to_string()), + last_error: r.last_error, + run_count: r.run_count, + max_runs: r.max_runs, + last_fired_at: r.last_fired_at, + next_fire_at: r.next_fire_at, + created_at: r.created_at, + session_lookup, + } + }) + .collect(); + + let jobs_json = serde_json::to_string(&summaries) + .map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?; + + Ok(CommandResponse::success(ctx.request_id) + .with_metadata("scheduler_jobs", &jobs_json)) + } +} + +/// 从 job 的 target_json 推导 session_lookup。 +/// 只有 agent_task / silent_agent_task 才有执行对话可查看。 +fn build_session_lookup( + r: &crate::storage::SchedulerJobRecord, +) -> Option { + let target: serde_json::Value = r.target.clone(); + + match r.kind.as_str() { + "agent_task" => { + let channel = target.get("channel")?.as_str()?.to_string(); + let chat_id = target.get("chat_id")?.as_str()?.to_string(); + Some(SchedulerJobSessionLookup { channel, chat_id }) + } + "silent_agent_task" => { + let channel = target.get("channel")?.as_str()?.to_string(); + // silent_agent_task 使用虚拟 chat_id: scheduler/{job_id} + let chat_id = format!("scheduler/{}", r.id); + Some(SchedulerJobSessionLookup { channel, chat_id }) + } + _ => None, // internal_event / outbound_message 无执行对话 + } +} diff --git a/src/command/handlers/load_chat_messages.rs b/src/command/handlers/load_chat_messages.rs new file mode 100644 index 0000000..e312a50 --- /dev/null +++ b/src/command/handlers/load_chat_messages.rs @@ -0,0 +1,65 @@ +use crate::command::context::CommandContext; +use crate::command::handler::{CommandHandler, CommandMetadata}; +use crate::command::response::{CommandError, CommandResponse}; +use crate::command::Command; +use async_trait::async_trait; + +/// 加载指定 channel + chat_id 的对话消息。 +/// 实际的消息加载和发送在 ws.rs 中处理(类似 send_task_messages)。 +/// 此 handler 仅验证参数并通过 metadata 传递 chat_id。 +pub struct LoadChatMessagesCommandHandler; + +impl LoadChatMessagesCommandHandler { + pub fn new() -> Self { + Self + } +} + +impl Default for LoadChatMessagesCommandHandler { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl CommandHandler for LoadChatMessagesCommandHandler { + fn can_handle(&self, cmd: &Command) -> bool { + matches!(cmd, Command::LoadChatMessages { .. }) + } + + fn metadata(&self) -> Option { + Some(CommandMetadata { + name: "load_chat_messages", + description: "加载指定 channel 和 chat_id 的对话消息", + usage: "/load_chat_messages ", + }) + } + + async fn handle( + &self, + cmd: Command, + ctx: CommandContext, + ) -> Result { + match cmd { + Command::LoadChatMessages { channel, chat_id } => { + if channel.is_empty() { + return Err(CommandError::new( + "INVALID_ARGUMENT", + "channel must not be empty".to_string(), + )); + } + if chat_id.is_empty() { + return Err(CommandError::new( + "INVALID_ARGUMENT", + "chat_id must not be empty".to_string(), + )); + } + + Ok(CommandResponse::success(ctx.request_id) + .with_metadata("load_chat_channel", &channel) + .with_metadata("load_chat_id", &chat_id)) + } + _ => unreachable!(), + } + } +} diff --git a/src/command/handlers/mod.rs b/src/command/handlers/mod.rs index 5b89bb2..9696970 100644 --- a/src/command/handlers/mod.rs +++ b/src/command/handlers/mod.rs @@ -1,9 +1,11 @@ pub mod get_current; pub mod help; pub mod list_channels; +pub mod list_scheduler_jobs; pub mod list_sessions; pub mod list_sessions_by_channel; pub mod list_topics; +pub mod load_chat_messages; pub mod load_task_messages; pub mod load_topic; pub mod save_session; diff --git a/src/command/mod.rs b/src/command/mod.rs index bf55c9b..1c2b0b2 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -45,6 +45,13 @@ pub enum Command { ListTopics { session_id: String }, /// 加载子智能体任务的消息历史 LoadTaskMessages { task_id: String }, + /// 列出所有定时任务 + ListSchedulerJobs, + /// 加载指定 channel + chat_id 的对话消息 + LoadChatMessages { + channel: String, + chat_id: String, + }, } impl Command { @@ -63,6 +70,8 @@ impl Command { Command::ListSessionsByChannel { .. } => "list_sessions_by_channel", Command::ListTopics { .. } => "list_topics", Command::LoadTaskMessages { .. } => "load_task_messages", + Command::ListSchedulerJobs => "list_scheduler_jobs", + Command::LoadChatMessages { .. } => "load_chat_messages", } } } diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 71c49d0..fcc4465 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -8,9 +8,11 @@ use crate::command::handler::CommandRouter; use crate::command::handlers::get_current::GetCurrentSessionCommandHandler; use crate::command::handlers::help::HelpCommandHandler; use crate::command::handlers::list_channels::ListChannelsCommandHandler; +use crate::command::handlers::list_scheduler_jobs::ListSchedulerJobsCommandHandler; use crate::command::handlers::list_sessions::ListSessionsCommandHandler; use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler; use crate::command::handlers::list_topics::ListTopicsCommandHandler; +use crate::command::handlers::load_chat_messages::LoadChatMessagesCommandHandler; use crate::command::handlers::load_task_messages::LoadTaskMessagesCommandHandler; use crate::command::handlers::load_topic::LoadTopicCommandHandler; use crate::command::handlers::save_session::SaveSessionCommandHandler; @@ -399,6 +401,10 @@ async fn handle_inbound( // 注册 help 处理器 let metadata = router.metadata_arc(); router.register(Box::new(HelpCommandHandler::new(metadata))); + // 注册 list_scheduler_jobs 处理器 + router.register(Box::new(ListSchedulerJobsCommandHandler::new(store.clone()))); + // 注册 load_chat_messages 处理器 + router.register(Box::new(LoadChatMessagesCommandHandler::new())); // 构建命令上下文 tracing::debug!( @@ -473,6 +479,28 @@ async fn handle_inbound( }).await; } + // 处理定时任务列表 + if let Some(jobs_json) = response.metadata.get("scheduler_jobs") { + if let Ok(jobs) = serde_json::from_str::>(jobs_json) { + let _ = sender.send(WsOutbound::SchedulerJobList { jobs }).await; + } + } + + // 处理加载聊天消息请求 + if let Some(load_chat_id) = response.metadata.get("load_chat_id") { + let load_chat_channel = response.metadata.get("load_chat_channel") + .cloned() + .unwrap_or_default(); + if let Err(e) = send_task_messages(&store, load_chat_id, sender).await { + tracing::warn!( + error = %e, + channel = %load_chat_channel, + chat_id = %load_chat_id, + "Failed to send chat messages" + ); + } + } + if current_topic_id.is_none() { if let Some(topics_json) = response.metadata.get("topics") { match serde_json::from_str::>(topics_json) { diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 135fcfb..54b0e3d 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -48,6 +48,31 @@ pub struct MediaSummary { pub file_name: Option, } +/// 定时任务会话查找键(用于前端加载执行对话) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SchedulerJobSessionLookup { + pub channel: String, + pub chat_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SchedulerJobSummary { + pub id: String, + pub kind: String, + pub schedule: serde_json::Value, + pub enabled: bool, + pub state: String, + pub last_status: Option, + pub last_error: Option, + pub run_count: i64, + pub max_runs: Option, + pub last_fired_at: Option, + pub next_fire_at: Option, + pub created_at: i64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub session_lookup: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum WsInbound { @@ -158,6 +183,10 @@ pub enum WsOutbound { #[serde(default, skip_serializing_if = "Option::is_none")] summary: Option, }, + #[serde(rename = "scheduler_job_list")] + SchedulerJobList { + jobs: Vec, + }, #[serde(rename = "pong")] Pong, } diff --git a/web/src/App.tsx b/web/src/App.tsx index c8bea0f..0cad1e9 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,13 +1,14 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' -import { Zap, Cpu, MessageSquare, ArrowLeft, Bot } from 'lucide-react' +import { Zap, Cpu, MessageSquare, ArrowLeft, Bot, Clock } from 'lucide-react' import { ChatContainer } from './components/Chat/ChatContainer' import { TopicList } from './components/Sidebar/TopicList' import { SessionInfo } from './components/Sidebar/SessionInfo' +import { SchedulerJobList } from './components/Sidebar/SchedulerJobList' import { ToolPanel } from './components/Panel/ToolPanel' import { ConnectionStatus } from './components/ConnectionStatus' import { useWebSocket } from './hooks/useWebSocket' import { useChat } from './hooks/useChat' -import type { ChatMessage, Command, Attachment } from './types/protocol' +import type { ChatMessage, Command, Attachment, SchedulerJobSessionLookup } from './types/protocol' const WS_URL = 'ws://127.0.0.1:19876/ws' @@ -31,6 +32,14 @@ function App() { isReadOnly, // 子智能体视图 subAgentView, + // 定时任务 + schedulerJobs, + sidebarTab, + setSidebarTab, + requestSchedulerJobList, + schedulerView, + enterSchedulerJobView, + exitSchedulerJobView, // 方法 handleMessage, handleCommand, @@ -171,6 +180,34 @@ function App() { exitSubAgentView() }, [exitSubAgentView]) + // 切换到定时任务 tab 时自动获取列表 + useEffect(() => { + if (sidebarTab === 'scheduler' && status === 'connected') { + const cmd = requestSchedulerJobList() + handleCommand(cmd) + sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) + } + }, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList]) + + const handleRefreshSchedulerJobs = useCallback(() => { + const cmd = requestSchedulerJobList() + handleCommand(cmd) + sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) + }, [handleCommand, sendMessage, requestSchedulerJobList]) + + const handleViewSchedulerJob = useCallback( + (lookup: SchedulerJobSessionLookup, jobId: string, description: string) => { + const cmd = enterSchedulerJobView(lookup, jobId, description) + handleCommand(cmd) + sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) + }, + [enterSchedulerJobView, handleCommand, sendMessage] + ) + + const handleExitSchedulerJobView = useCallback(() => { + exitSchedulerJobView() + }, [exitSchedulerJobView]) + const chatMessages = useMemo(() => { const result: ChatMessage[] = [] const toolCallIndex = new Map() @@ -247,27 +284,84 @@ function App() { {/* Main Content */}
{/* Left Sidebar */} -
+
+ + {/* Tab 栏 */} +
+ + +
+
- + {sidebarTab === 'topics' ? ( + + ) : ( + + )}
{/* Center - Chat */}
+ {/* Scheduler job view back bar */} + {schedulerView && ( +
+ +
+
+ + 定时任务: + {schedulerView.description} +
+
+
+ 通道: + {schedulerView.channel} +
+
+ )} {/* Sub-agent back bar */} {subAgentView && (
@@ -313,9 +407,13 @@ function App() { {} : handleSendMessage} + isReadOnly={subAgentView || schedulerView ? true : isReadOnly} + channelName={ + schedulerView ? `定时任务: ${schedulerView.description}` : + subAgentView ? `子智能体: ${subAgentView.description}` : + (session?.title ?? 'PicoBot') + } + onSendMessage={subAgentView || schedulerView ? () => {} : handleSendMessage} onNavigateToSubAgent={handleNavigateToSubAgent} />
diff --git a/web/src/components/Sidebar/SchedulerJobList.tsx b/web/src/components/Sidebar/SchedulerJobList.tsx new file mode 100644 index 0000000..9c80e1f --- /dev/null +++ b/web/src/components/Sidebar/SchedulerJobList.tsx @@ -0,0 +1,193 @@ +import { Clock, RefreshCw, ChevronRight, Check, X, Minus } from 'lucide-react' +import type { SchedulerJobSummary, SchedulerJobSessionLookup } from '../../types/protocol' + +interface SchedulerJobListProps { + jobs: SchedulerJobSummary[] + onRefresh: () => void + onViewJob: (lookup: SchedulerJobSessionLookup, jobId: string, description: string) => void + sessionId: string | null +} + +function kindLabel(kind: string): string { + const map: Record = { + internal_event: '内部事件', + outbound_message: '外发消息', + agent_task: '智能体', + silent_agent_task: '静默智能体', + } + return map[kind] ?? kind +} + +function stateBadge(state: string): { label: string; color: string; pulse: boolean } { + switch (state) { + case 'running': + return { label: '执行中', color: 'bg-amber-500/20 text-amber-400 border-amber-500/30', pulse: true } + case 'scheduled': + return { label: '已调度', color: 'bg-amber-500/15 text-amber-300 border-amber-500/20', pulse: false } + case 'paused': + return { label: '已暂停', color: 'bg-zinc-500/20 text-zinc-400 border-zinc-500/30', pulse: false } + case 'completed': + return { label: '已完成', color: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', pulse: false } + default: + return { label: state, color: 'bg-zinc-500/20 text-zinc-400 border-zinc-500/30', pulse: false } + } +} + +function formatTime(tsMillis: number | undefined): string { + if (tsMillis == null) return '--' + const d = new Date(tsMillis) + return d.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) +} + +function scheduleDescription(schedule: unknown): string { + if (!schedule || typeof schedule !== 'object') return '--' + const s = schedule as Record + switch (s.type) { + case 'cron': + return `Cron: ${s.expression}` + case 'interval': + return `每 ${s.seconds} 秒` + case 'delay': + return `延迟 ${s.seconds} 秒` + case 'at': + return `定时: ${s.timestamp}` + default: + return JSON.stringify(s) + } +} + +function lastStatusIcon(lastStatus: string | undefined) { + switch (lastStatus) { + case 'ok': + return + case 'error': + return + default: + return + } +} + +export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: SchedulerJobListProps) { + return ( +
+ {/* Header */} +
+

+ + 定时任务 + {jobs.length > 0 && ( + ({jobs.length}) + )} +

+ +
+ + {/* Job List */} +
+ {!sessionId ? ( +
+ +

等待连接...

+
+ ) : jobs.length === 0 ? ( +
+ +

暂无定时任务

+
+ ) : ( +
+ {jobs.map((job) => { + const badge = stateBadge(job.state) + const hasLookup = !!job.session_lookup + return ( + + ) + })} +
+ )} +
+
+ ) +} diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index 72c910e..d75fb9a 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -15,6 +15,9 @@ import type { Session, TaskMessagesLoaded, Attachment, + SchedulerJobList, + SchedulerJobSummary, + SchedulerJobSessionLookup, } from '../types/protocol' // 简化后的层级状态 @@ -58,6 +61,17 @@ interface UseChatReturn { // 子智能体导航方法 enterSubAgentView: (taskId: string, description: string) => Command exitSubAgentView: () => void + + // 定时任务状态 + schedulerJobs: SchedulerJobSummary[] + sidebarTab: 'topics' | 'scheduler' + setSidebarTab: (tab: 'topics' | 'scheduler') => void + requestSchedulerJobList: () => Command + + // 定时任务执行对话查看 + schedulerView: SchedulerJobView | null + enterSchedulerJobView: (lookup: SchedulerJobSessionLookup, jobId: string, description: string) => Command + exitSchedulerJobView: () => void } interface SubAgentView { @@ -69,6 +83,14 @@ interface SubAgentView { messages: ChatMessage[] } +interface SchedulerJobView { + jobId: string + description: string + channel: string + chatId: string + messages: ChatMessage[] +} + const DEFAULT_CHANNEL = 'websocket' const DEFAULT_CHAT_ID = 'default' @@ -82,6 +104,9 @@ export function useChat(): UseChatReturn { const [topics, setTopics] = useState([]) const [selectedTopic, setSelectedTopic] = useState(null) const [subAgentView, setSubAgentView] = useState(null) + const [schedulerJobs, setSchedulerJobs] = useState([]) + const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics') + const [schedulerView, setSchedulerView] = useState(null) // Message ID generator const messageIdCounter = useRef(0) @@ -90,8 +115,9 @@ export function useChat(): UseChatReturn { return `msg_${Date.now()}_${messageIdCounter.current}` } - // Ref to track subAgentView for use in callbacks + // Ref to track subAgentView and schedulerView for use in callbacks const subAgentViewRef = useRef(null) + const schedulerViewRef = useRef(null) const isConnected = useMemo(() => connectionId !== null, [connectionId]) const sessionId = useMemo(() => session?.id ?? null, [session]) @@ -192,6 +218,21 @@ export function useChat(): UseChatReturn { const handleServerMessage = useCallback((message: WsOutbound) => { console.log('Received message:', message) + // Route to scheduler job view if active + const currentSchedulerView = schedulerViewRef.current + if (currentSchedulerView) { + // Route all chat messages to the scheduler view + const chatMsg = serverMessageToChatMessage(message) + if (chatMsg) { + setSchedulerView((prev) => + prev + ? { ...prev, messages: [...prev.messages, chatMsg] } + : prev + ) + } + return + } + // Route to sub-agent view if active const currentSubAgentView = subAgentViewRef.current if (currentSubAgentView) { @@ -384,6 +425,12 @@ export function useChat(): UseChatReturn { break } + case 'scheduler_job_list': { + const msg = message as SchedulerJobList + setSchedulerJobs(msg.jobs) + break + } + case 'channel_list': case 'pong': // 忽略这些消息 @@ -460,11 +507,15 @@ export function useChat(): UseChatReturn { } }, [sessionId]) - // Keep ref in sync with state + // Keep refs in sync with state useEffect(() => { subAgentViewRef.current = subAgentView }, [subAgentView]) + useEffect(() => { + schedulerViewRef.current = schedulerView + }, [schedulerView]) + const enterSubAgentView = useCallback((taskId: string, description: string): Command => { const newView: SubAgentView = { taskId, @@ -487,13 +538,46 @@ export function useChat(): UseChatReturn { setSubAgentView(null) }, []) - // Memoize messages: when in sub-agent view, return sub-agent messages + // 定时任务方法 + const requestSchedulerJobList = useCallback((): Command => { + return { type: 'list_scheduler_jobs' } + }, []) + + const enterSchedulerJobView = useCallback( + (lookup: SchedulerJobSessionLookup, jobId: string, description: string): Command => { + const newView: SchedulerJobView = { + jobId, + description, + channel: lookup.channel, + chatId: lookup.chat_id, + messages: [], + } + schedulerViewRef.current = newView + setSchedulerView(newView) + return { + type: 'load_chat_messages', + channel: lookup.channel, + chat_id: lookup.chat_id, + } + }, + [] + ) + + const exitSchedulerJobView = useCallback(() => { + schedulerViewRef.current = null + setSchedulerView(null) + }, []) + + // Memoize messages: sub-agent view > scheduler view > main const resolvedMessages = useMemo(() => { if (subAgentView) { return subAgentView.messages } + if (schedulerView) { + return schedulerView.messages + } return messages - }, [subAgentView, messages]) + }, [subAgentView, schedulerView, messages]) // WebSocket 通道始终可写 const isReadOnly = false @@ -521,5 +605,12 @@ export function useChat(): UseChatReturn { requestTopicList, enterSubAgentView, exitSubAgentView, + schedulerJobs, + sidebarTab, + setSidebarTab, + requestSchedulerJobList, + schedulerView, + enterSchedulerJobView, + exitSchedulerJobView, } } diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index 7e2e77a..9543d8a 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -157,6 +157,32 @@ export interface Pong { type: 'pong' } +export interface SchedulerJobSessionLookup { + channel: string + chat_id: string +} + +export interface SchedulerJobSummary { + id: string + kind: string + schedule: unknown + enabled: boolean + state: string + last_status?: string + last_error?: string + run_count: number + max_runs?: number + last_fired_at?: number + next_fire_at?: number + created_at: number + session_lookup?: SchedulerJobSessionLookup +} + +export interface SchedulerJobList { + type: 'scheduler_job_list' + jobs: SchedulerJobSummary[] +} + export interface TaskMessagesLoaded { type: 'task_messages_loaded' task_id: string @@ -180,6 +206,7 @@ export type WsOutbound = | TopicList | ChannelList | TaskMessagesLoaded + | SchedulerJobList | Pong // ============================================================================ @@ -247,6 +274,16 @@ export interface LoadTaskMessagesCommand { task_id: string } +export interface ListSchedulerJobsCommand { + type: 'list_scheduler_jobs' +} + +export interface LoadChatMessagesCommand { + type: 'load_chat_messages' + channel: string + chat_id: string +} + export type Command = | CreateSessionCommand | ListSessionsCommand @@ -260,6 +297,8 @@ export type Command = | ListSessionsByChannelCommand | ListTopicsCommand | LoadTaskMessagesCommand + | ListSchedulerJobsCommand + | LoadChatMessagesCommand // ============================================================================ // UI Types