feat: 添加定时任务和聊天消息加载功能,增强调度管理
This commit is contained in:
parent
4d6d989247
commit
5f2bc950b1
96
src/command/handlers/list_scheduler_jobs.rs
Normal file
96
src/command/handlers/list_scheduler_jobs.rs
Normal file
@ -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<SessionStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListSchedulerJobsCommandHandler {
|
||||||
|
pub fn new(store: Arc<SessionStore>) -> Self {
|
||||||
|
Self { store }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandHandler for ListSchedulerJobsCommandHandler {
|
||||||
|
fn can_handle(&self, cmd: &Command) -> bool {
|
||||||
|
matches!(cmd, Command::ListSchedulerJobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "list_scheduler_jobs",
|
||||||
|
description: "列出所有定时任务",
|
||||||
|
usage: "/list_scheduler_jobs",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
_cmd: Command,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
let records = self
|
||||||
|
.store
|
||||||
|
.list_scheduler_jobs(false)
|
||||||
|
.map_err(|e| CommandError::new("LIST_JOBS_ERROR", e.to_string()))?;
|
||||||
|
|
||||||
|
let summaries: Vec<SchedulerJobSummary> = 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<SchedulerJobSessionLookup> {
|
||||||
|
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 无执行对话
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/command/handlers/load_chat_messages.rs
Normal file
65
src/command/handlers/load_chat_messages.rs
Normal file
@ -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<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "load_chat_messages",
|
||||||
|
description: "加载指定 channel 和 chat_id 的对话消息",
|
||||||
|
usage: "/load_chat_messages <channel> <chat_id>",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
cmd: Command,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
pub mod get_current;
|
pub mod get_current;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod list_channels;
|
pub mod list_channels;
|
||||||
|
pub mod list_scheduler_jobs;
|
||||||
pub mod list_sessions;
|
pub mod list_sessions;
|
||||||
pub mod list_sessions_by_channel;
|
pub mod list_sessions_by_channel;
|
||||||
pub mod list_topics;
|
pub mod list_topics;
|
||||||
|
pub mod load_chat_messages;
|
||||||
pub mod load_task_messages;
|
pub mod load_task_messages;
|
||||||
pub mod load_topic;
|
pub mod load_topic;
|
||||||
pub mod save_session;
|
pub mod save_session;
|
||||||
|
|||||||
@ -45,6 +45,13 @@ pub enum Command {
|
|||||||
ListTopics { session_id: String },
|
ListTopics { session_id: String },
|
||||||
/// 加载子智能体任务的消息历史
|
/// 加载子智能体任务的消息历史
|
||||||
LoadTaskMessages { task_id: String },
|
LoadTaskMessages { task_id: String },
|
||||||
|
/// 列出所有定时任务
|
||||||
|
ListSchedulerJobs,
|
||||||
|
/// 加载指定 channel + chat_id 的对话消息
|
||||||
|
LoadChatMessages {
|
||||||
|
channel: String,
|
||||||
|
chat_id: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
@ -63,6 +70,8 @@ impl Command {
|
|||||||
Command::ListSessionsByChannel { .. } => "list_sessions_by_channel",
|
Command::ListSessionsByChannel { .. } => "list_sessions_by_channel",
|
||||||
Command::ListTopics { .. } => "list_topics",
|
Command::ListTopics { .. } => "list_topics",
|
||||||
Command::LoadTaskMessages { .. } => "load_task_messages",
|
Command::LoadTaskMessages { .. } => "load_task_messages",
|
||||||
|
Command::ListSchedulerJobs => "list_scheduler_jobs",
|
||||||
|
Command::LoadChatMessages { .. } => "load_chat_messages",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,11 @@ use crate::command::handler::CommandRouter;
|
|||||||
use crate::command::handlers::get_current::GetCurrentSessionCommandHandler;
|
use crate::command::handlers::get_current::GetCurrentSessionCommandHandler;
|
||||||
use crate::command::handlers::help::HelpCommandHandler;
|
use crate::command::handlers::help::HelpCommandHandler;
|
||||||
use crate::command::handlers::list_channels::ListChannelsCommandHandler;
|
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::ListSessionsCommandHandler;
|
||||||
use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler;
|
use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler;
|
||||||
use crate::command::handlers::list_topics::ListTopicsCommandHandler;
|
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_task_messages::LoadTaskMessagesCommandHandler;
|
||||||
use crate::command::handlers::load_topic::LoadTopicCommandHandler;
|
use crate::command::handlers::load_topic::LoadTopicCommandHandler;
|
||||||
use crate::command::handlers::save_session::SaveSessionCommandHandler;
|
use crate::command::handlers::save_session::SaveSessionCommandHandler;
|
||||||
@ -399,6 +401,10 @@ async fn handle_inbound(
|
|||||||
// 注册 help 处理器
|
// 注册 help 处理器
|
||||||
let metadata = router.metadata_arc();
|
let metadata = router.metadata_arc();
|
||||||
router.register(Box::new(HelpCommandHandler::new(metadata)));
|
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!(
|
tracing::debug!(
|
||||||
@ -473,6 +479,28 @@ async fn handle_inbound(
|
|||||||
}).await;
|
}).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理定时任务列表
|
||||||
|
if let Some(jobs_json) = response.metadata.get("scheduler_jobs") {
|
||||||
|
if let Ok(jobs) = serde_json::from_str::<Vec<crate::protocol::SchedulerJobSummary>>(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 current_topic_id.is_none() {
|
||||||
if let Some(topics_json) = response.metadata.get("topics") {
|
if let Some(topics_json) = response.metadata.get("topics") {
|
||||||
match serde_json::from_str::<Vec<crate::protocol::TopicSummary>>(topics_json) {
|
match serde_json::from_str::<Vec<crate::protocol::TopicSummary>>(topics_json) {
|
||||||
|
|||||||
@ -48,6 +48,31 @@ pub struct MediaSummary {
|
|||||||
pub file_name: Option<String>,
|
pub file_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 定时任务会话查找键(用于前端加载执行对话)
|
||||||
|
#[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<String>,
|
||||||
|
pub last_error: Option<String>,
|
||||||
|
pub run_count: i64,
|
||||||
|
pub max_runs: Option<i64>,
|
||||||
|
pub last_fired_at: Option<i64>,
|
||||||
|
pub next_fire_at: Option<i64>,
|
||||||
|
pub created_at: i64,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub session_lookup: Option<SchedulerJobSessionLookup>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum WsInbound {
|
pub enum WsInbound {
|
||||||
@ -158,6 +183,10 @@ pub enum WsOutbound {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
summary: Option<String>,
|
summary: Option<String>,
|
||||||
},
|
},
|
||||||
|
#[serde(rename = "scheduler_job_list")]
|
||||||
|
SchedulerJobList {
|
||||||
|
jobs: Vec<SchedulerJobSummary>,
|
||||||
|
},
|
||||||
#[serde(rename = "pong")]
|
#[serde(rename = "pong")]
|
||||||
Pong,
|
Pong,
|
||||||
}
|
}
|
||||||
|
|||||||
128
web/src/App.tsx
128
web/src/App.tsx
@ -1,13 +1,14 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
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 { ChatContainer } from './components/Chat/ChatContainer'
|
||||||
import { TopicList } from './components/Sidebar/TopicList'
|
import { TopicList } from './components/Sidebar/TopicList'
|
||||||
import { SessionInfo } from './components/Sidebar/SessionInfo'
|
import { SessionInfo } from './components/Sidebar/SessionInfo'
|
||||||
|
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
|
||||||
import { ToolPanel } from './components/Panel/ToolPanel'
|
import { ToolPanel } from './components/Panel/ToolPanel'
|
||||||
import { ConnectionStatus } from './components/ConnectionStatus'
|
import { ConnectionStatus } from './components/ConnectionStatus'
|
||||||
import { useWebSocket } from './hooks/useWebSocket'
|
import { useWebSocket } from './hooks/useWebSocket'
|
||||||
import { useChat } from './hooks/useChat'
|
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'
|
const WS_URL = 'ws://127.0.0.1:19876/ws'
|
||||||
|
|
||||||
@ -31,6 +32,14 @@ function App() {
|
|||||||
isReadOnly,
|
isReadOnly,
|
||||||
// 子智能体视图
|
// 子智能体视图
|
||||||
subAgentView,
|
subAgentView,
|
||||||
|
// 定时任务
|
||||||
|
schedulerJobs,
|
||||||
|
sidebarTab,
|
||||||
|
setSidebarTab,
|
||||||
|
requestSchedulerJobList,
|
||||||
|
schedulerView,
|
||||||
|
enterSchedulerJobView,
|
||||||
|
exitSchedulerJobView,
|
||||||
// 方法
|
// 方法
|
||||||
handleMessage,
|
handleMessage,
|
||||||
handleCommand,
|
handleCommand,
|
||||||
@ -171,6 +180,34 @@ function App() {
|
|||||||
exitSubAgentView()
|
exitSubAgentView()
|
||||||
}, [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 chatMessages = useMemo(() => {
|
||||||
const result: ChatMessage[] = []
|
const result: ChatMessage[] = []
|
||||||
const toolCallIndex = new Map<string, number>()
|
const toolCallIndex = new Map<string, number>()
|
||||||
@ -247,27 +284,84 @@ function App() {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Left Sidebar */}
|
{/* Left Sidebar */}
|
||||||
<div className={`w-72 shrink-0 border-r border-white/8 bg-[#12121a]/50 flex flex-col ${subAgentView ? 'opacity-50 pointer-events-none' : ''}`}>
|
<div className={`w-72 shrink-0 border-r border-white/8 bg-[#12121a]/50 flex flex-col ${subAgentView || schedulerView ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
<SessionInfo
|
<SessionInfo
|
||||||
session={session}
|
session={session}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
/>
|
/>
|
||||||
<div className="border-b border-white/8" />
|
<div className="border-b border-white/8" />
|
||||||
|
|
||||||
|
{/* Tab 栏 */}
|
||||||
|
<div className="flex border-b border-white/8">
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarTab('topics')}
|
||||||
|
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
|
||||||
|
sidebarTab === 'topics'
|
||||||
|
? 'text-[#00f0ff] border-b-2 border-[#00f0ff]'
|
||||||
|
: 'text-zinc-500 hover:text-zinc-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
话题
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarTab('scheduler')}
|
||||||
|
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
|
||||||
|
sidebarTab === 'scheduler'
|
||||||
|
? 'text-[#00f0ff] border-b-2 border-[#00f0ff]'
|
||||||
|
: 'text-zinc-500 hover:text-zinc-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
定时任务
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<TopicList
|
{sidebarTab === 'topics' ? (
|
||||||
sessionId={sessionId}
|
<TopicList
|
||||||
sessionTitle={session?.title ?? ''}
|
sessionId={sessionId}
|
||||||
topics={topics}
|
sessionTitle={session?.title ?? ''}
|
||||||
currentTopicId={selectedTopic}
|
topics={topics}
|
||||||
isReadOnly={isReadOnly}
|
currentTopicId={selectedTopic}
|
||||||
onCreateTopic={handleCreateTopic}
|
isReadOnly={isReadOnly}
|
||||||
onSwitchTopic={handleSwitchTopic}
|
onCreateTopic={handleCreateTopic}
|
||||||
/>
|
onSwitchTopic={handleSwitchTopic}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SchedulerJobList
|
||||||
|
jobs={schedulerJobs}
|
||||||
|
onRefresh={handleRefreshSchedulerJobs}
|
||||||
|
onViewJob={handleViewSchedulerJob}
|
||||||
|
sessionId={sessionId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center - Chat */}
|
{/* Center - Chat */}
|
||||||
<div className="flex-1 min-w-0 bg-[#0a0a0f] flex flex-col">
|
<div className="flex-1 min-w-0 bg-[#0a0a0f] flex flex-col">
|
||||||
|
{/* Scheduler job view back bar */}
|
||||||
|
{schedulerView && (
|
||||||
|
<div className="shrink-0 border-b border-white/8 bg-[#12121a]/80 px-4 py-2 flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={handleExitSchedulerJobView}
|
||||||
|
className="flex items-center gap-1.5 text-sm text-[#00f0ff] hover:text-[#00f0ff]/80 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
<span>返回定时任务列表</span>
|
||||||
|
</button>
|
||||||
|
<div className="h-4 w-px bg-white/20" />
|
||||||
|
<div className="flex items-center gap-1.5 text-sm text-zinc-300">
|
||||||
|
<Clock className="h-4 w-4 text-amber-400" />
|
||||||
|
<span className="text-zinc-500">定时任务:</span>
|
||||||
|
<span className="text-white font-medium font-mono text-xs truncate max-w-[250px]">{schedulerView.description}</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-4 w-px bg-white/20" />
|
||||||
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<span className="text-zinc-500">通道:</span>
|
||||||
|
<span className="text-zinc-300">{schedulerView.channel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Sub-agent back bar */}
|
{/* Sub-agent back bar */}
|
||||||
{subAgentView && (
|
{subAgentView && (
|
||||||
<div className="shrink-0 border-b border-white/8 bg-[#12121a]/80 px-4 py-2 flex items-center gap-4">
|
<div className="shrink-0 border-b border-white/8 bg-[#12121a]/80 px-4 py-2 flex items-center gap-4">
|
||||||
@ -313,9 +407,13 @@ function App() {
|
|||||||
<ChatContainer
|
<ChatContainer
|
||||||
messages={chatMessages}
|
messages={chatMessages}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isReadOnly={subAgentView ? true : isReadOnly}
|
isReadOnly={subAgentView || schedulerView ? true : isReadOnly}
|
||||||
channelName={subAgentView ? `子智能体: ${subAgentView.description}` : (session?.title ?? 'PicoBot')}
|
channelName={
|
||||||
onSendMessage={subAgentView ? () => {} : handleSendMessage}
|
schedulerView ? `定时任务: ${schedulerView.description}` :
|
||||||
|
subAgentView ? `子智能体: ${subAgentView.description}` :
|
||||||
|
(session?.title ?? 'PicoBot')
|
||||||
|
}
|
||||||
|
onSendMessage={subAgentView || schedulerView ? () => {} : handleSendMessage}
|
||||||
onNavigateToSubAgent={handleNavigateToSubAgent}
|
onNavigateToSubAgent={handleNavigateToSubAgent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
193
web/src/components/Sidebar/SchedulerJobList.tsx
Normal file
193
web/src/components/Sidebar/SchedulerJobList.tsx
Normal file
@ -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<string, string> = {
|
||||||
|
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<string, unknown>
|
||||||
|
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 <Check className="h-3 w-3 text-emerald-400" />
|
||||||
|
case 'error':
|
||||||
|
return <X className="h-3 w-3 text-red-400" />
|
||||||
|
default:
|
||||||
|
return <Minus className="h-3 w-3 text-zinc-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: SchedulerJobListProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between border-b border-white/8 px-3 py-2.5">
|
||||||
|
<h2 className="font-semibold text-white flex items-center gap-2 text-sm">
|
||||||
|
<Clock className="h-4 w-4 text-[#00f0ff]" />
|
||||||
|
定时任务
|
||||||
|
{jobs.length > 0 && (
|
||||||
|
<span className="text-xs text-zinc-500">({jobs.length})</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onRefresh}
|
||||||
|
className="flex items-center gap-1 rounded-lg px-2 py-1 text-xs text-zinc-400 hover:text-white hover:bg-white/10 transition-all"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Job List */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-2">
|
||||||
|
{!sessionId ? (
|
||||||
|
<div className="p-4 text-center text-sm text-zinc-500">
|
||||||
|
<Clock className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>等待连接...</p>
|
||||||
|
</div>
|
||||||
|
) : jobs.length === 0 ? (
|
||||||
|
<div className="p-4 text-center text-sm text-zinc-500">
|
||||||
|
<Clock className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>暂无定时任务</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{jobs.map((job) => {
|
||||||
|
const badge = stateBadge(job.state)
|
||||||
|
const hasLookup = !!job.session_lookup
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={job.id}
|
||||||
|
onClick={() => {
|
||||||
|
if (hasLookup && job.session_lookup) {
|
||||||
|
onViewJob(job.session_lookup, job.id, job.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!hasLookup}
|
||||||
|
className={`w-full rounded-lg px-3 py-2.5 text-left transition-all border ${
|
||||||
|
hasLookup
|
||||||
|
? 'hover:bg-white/5 border-transparent hover:border-white/10 cursor-pointer'
|
||||||
|
: 'border-transparent cursor-default opacity-70'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Row 1: ID + kind + enabled */}
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="flex items-center gap-1.5 min-w-0 flex-1">
|
||||||
|
<span className="text-xs text-zinc-500 font-mono truncate max-w-[120px]">
|
||||||
|
{job.id}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs px-1.5 py-0.5 rounded bg-white/10 text-zinc-300 shrink-0">
|
||||||
|
{kindLabel(job.kind)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
<span className={`inline-block h-1.5 w-1.5 rounded-full ${job.enabled ? 'bg-emerald-400' : 'bg-zinc-600'}`} />
|
||||||
|
{hasLookup && <ChevronRight className="h-3.5 w-3.5 text-zinc-600" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 2: State badge + last status */}
|
||||||
|
<div className="flex items-center gap-2 mb-1.5">
|
||||||
|
<span className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded border ${badge.color}`}>
|
||||||
|
{badge.pulse && <span className="inline-block h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />}
|
||||||
|
{badge.label}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-0.5 text-xs text-zinc-500">
|
||||||
|
{lastStatusIcon(job.last_status)}
|
||||||
|
<span className={
|
||||||
|
job.last_status === 'error' ? 'text-red-400' :
|
||||||
|
job.last_status === 'ok' ? 'text-emerald-400' : 'text-zinc-500'
|
||||||
|
}>
|
||||||
|
{job.last_status === 'ok' ? '正常' :
|
||||||
|
job.last_status === 'error' ? '异常' :
|
||||||
|
job.last_status === 'skipped' ? '跳过' : '--'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 3: Error message (if any) */}
|
||||||
|
{job.last_error && (
|
||||||
|
<div className="mb-1.5 text-xs text-red-400/80 truncate bg-red-500/10 rounded px-1.5 py-0.5">
|
||||||
|
{job.last_error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Row 4: Run count + schedule */}
|
||||||
|
<div className="flex items-center justify-between text-xs text-zinc-500 mb-1">
|
||||||
|
<span>
|
||||||
|
执行: {job.run_count}{job.max_runs ? `/${job.max_runs}` : ''}
|
||||||
|
</span>
|
||||||
|
<span className="truncate max-w-[120px] text-zinc-600">
|
||||||
|
{scheduleDescription(job.schedule)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 5: Times */}
|
||||||
|
<div className="flex items-center justify-between text-xs text-zinc-600">
|
||||||
|
<span>上次: {formatTime(job.last_fired_at)}</span>
|
||||||
|
<span>下次: {formatTime(job.next_fire_at)}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -15,6 +15,9 @@ import type {
|
|||||||
Session,
|
Session,
|
||||||
TaskMessagesLoaded,
|
TaskMessagesLoaded,
|
||||||
Attachment,
|
Attachment,
|
||||||
|
SchedulerJobList,
|
||||||
|
SchedulerJobSummary,
|
||||||
|
SchedulerJobSessionLookup,
|
||||||
} from '../types/protocol'
|
} from '../types/protocol'
|
||||||
|
|
||||||
// 简化后的层级状态
|
// 简化后的层级状态
|
||||||
@ -58,6 +61,17 @@ interface UseChatReturn {
|
|||||||
// 子智能体导航方法
|
// 子智能体导航方法
|
||||||
enterSubAgentView: (taskId: string, description: string) => Command
|
enterSubAgentView: (taskId: string, description: string) => Command
|
||||||
exitSubAgentView: () => void
|
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 {
|
interface SubAgentView {
|
||||||
@ -69,6 +83,14 @@ interface SubAgentView {
|
|||||||
messages: ChatMessage[]
|
messages: ChatMessage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SchedulerJobView {
|
||||||
|
jobId: string
|
||||||
|
description: string
|
||||||
|
channel: string
|
||||||
|
chatId: string
|
||||||
|
messages: ChatMessage[]
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_CHANNEL = 'websocket'
|
const DEFAULT_CHANNEL = 'websocket'
|
||||||
const DEFAULT_CHAT_ID = 'default'
|
const DEFAULT_CHAT_ID = 'default'
|
||||||
|
|
||||||
@ -82,6 +104,9 @@ export function useChat(): UseChatReturn {
|
|||||||
const [topics, setTopics] = useState<Topic[]>([])
|
const [topics, setTopics] = useState<Topic[]>([])
|
||||||
const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
|
const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
|
||||||
const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null)
|
const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null)
|
||||||
|
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
|
||||||
|
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
|
||||||
|
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
|
||||||
|
|
||||||
// Message ID generator
|
// Message ID generator
|
||||||
const messageIdCounter = useRef(0)
|
const messageIdCounter = useRef(0)
|
||||||
@ -90,8 +115,9 @@ export function useChat(): UseChatReturn {
|
|||||||
return `msg_${Date.now()}_${messageIdCounter.current}`
|
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<SubAgentView | null>(null)
|
const subAgentViewRef = useRef<SubAgentView | null>(null)
|
||||||
|
const schedulerViewRef = useRef<SchedulerJobView | null>(null)
|
||||||
|
|
||||||
const isConnected = useMemo(() => connectionId !== null, [connectionId])
|
const isConnected = useMemo(() => connectionId !== null, [connectionId])
|
||||||
const sessionId = useMemo(() => session?.id ?? null, [session])
|
const sessionId = useMemo(() => session?.id ?? null, [session])
|
||||||
@ -192,6 +218,21 @@ export function useChat(): UseChatReturn {
|
|||||||
const handleServerMessage = useCallback((message: WsOutbound) => {
|
const handleServerMessage = useCallback((message: WsOutbound) => {
|
||||||
console.log('Received message:', message)
|
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
|
// Route to sub-agent view if active
|
||||||
const currentSubAgentView = subAgentViewRef.current
|
const currentSubAgentView = subAgentViewRef.current
|
||||||
if (currentSubAgentView) {
|
if (currentSubAgentView) {
|
||||||
@ -384,6 +425,12 @@ export function useChat(): UseChatReturn {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'scheduler_job_list': {
|
||||||
|
const msg = message as SchedulerJobList
|
||||||
|
setSchedulerJobs(msg.jobs)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'channel_list':
|
case 'channel_list':
|
||||||
case 'pong':
|
case 'pong':
|
||||||
// 忽略这些消息
|
// 忽略这些消息
|
||||||
@ -460,11 +507,15 @@ export function useChat(): UseChatReturn {
|
|||||||
}
|
}
|
||||||
}, [sessionId])
|
}, [sessionId])
|
||||||
|
|
||||||
// Keep ref in sync with state
|
// Keep refs in sync with state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
subAgentViewRef.current = subAgentView
|
subAgentViewRef.current = subAgentView
|
||||||
}, [subAgentView])
|
}, [subAgentView])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
schedulerViewRef.current = schedulerView
|
||||||
|
}, [schedulerView])
|
||||||
|
|
||||||
const enterSubAgentView = useCallback((taskId: string, description: string): Command => {
|
const enterSubAgentView = useCallback((taskId: string, description: string): Command => {
|
||||||
const newView: SubAgentView = {
|
const newView: SubAgentView = {
|
||||||
taskId,
|
taskId,
|
||||||
@ -487,13 +538,46 @@ export function useChat(): UseChatReturn {
|
|||||||
setSubAgentView(null)
|
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(() => {
|
const resolvedMessages = useMemo(() => {
|
||||||
if (subAgentView) {
|
if (subAgentView) {
|
||||||
return subAgentView.messages
|
return subAgentView.messages
|
||||||
}
|
}
|
||||||
|
if (schedulerView) {
|
||||||
|
return schedulerView.messages
|
||||||
|
}
|
||||||
return messages
|
return messages
|
||||||
}, [subAgentView, messages])
|
}, [subAgentView, schedulerView, messages])
|
||||||
|
|
||||||
// WebSocket 通道始终可写
|
// WebSocket 通道始终可写
|
||||||
const isReadOnly = false
|
const isReadOnly = false
|
||||||
@ -521,5 +605,12 @@ export function useChat(): UseChatReturn {
|
|||||||
requestTopicList,
|
requestTopicList,
|
||||||
enterSubAgentView,
|
enterSubAgentView,
|
||||||
exitSubAgentView,
|
exitSubAgentView,
|
||||||
|
schedulerJobs,
|
||||||
|
sidebarTab,
|
||||||
|
setSidebarTab,
|
||||||
|
requestSchedulerJobList,
|
||||||
|
schedulerView,
|
||||||
|
enterSchedulerJobView,
|
||||||
|
exitSchedulerJobView,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -157,6 +157,32 @@ export interface Pong {
|
|||||||
type: '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 {
|
export interface TaskMessagesLoaded {
|
||||||
type: 'task_messages_loaded'
|
type: 'task_messages_loaded'
|
||||||
task_id: string
|
task_id: string
|
||||||
@ -180,6 +206,7 @@ export type WsOutbound =
|
|||||||
| TopicList
|
| TopicList
|
||||||
| ChannelList
|
| ChannelList
|
||||||
| TaskMessagesLoaded
|
| TaskMessagesLoaded
|
||||||
|
| SchedulerJobList
|
||||||
| Pong
|
| Pong
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -247,6 +274,16 @@ export interface LoadTaskMessagesCommand {
|
|||||||
task_id: string
|
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 =
|
export type Command =
|
||||||
| CreateSessionCommand
|
| CreateSessionCommand
|
||||||
| ListSessionsCommand
|
| ListSessionsCommand
|
||||||
@ -260,6 +297,8 @@ export type Command =
|
|||||||
| ListSessionsByChannelCommand
|
| ListSessionsByChannelCommand
|
||||||
| ListTopicsCommand
|
| ListTopicsCommand
|
||||||
| LoadTaskMessagesCommand
|
| LoadTaskMessagesCommand
|
||||||
|
| ListSchedulerJobsCommand
|
||||||
|
| LoadChatMessagesCommand
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// UI Types
|
// UI Types
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user