Compare commits
No commits in common. "c4d10c6413195c20622c526828ec16e4bc980d23" and "f808bd09ea1dc12c39ef919a679da4f6412c7f60" have entirely different histories.
c4d10c6413
...
f808bd09ea
@ -1,74 +0,0 @@
|
|||||||
use crate::command::context::CommandContext;
|
|
||||||
use crate::command::handler::{CommandHandler, CommandMetadata};
|
|
||||||
use crate::command::response::{CommandError, CommandResponse};
|
|
||||||
use crate::command::Command;
|
|
||||||
use crate::protocol::TodoItemSummary;
|
|
||||||
use crate::storage::SessionStore;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub struct ListTodosCommandHandler {
|
|
||||||
store: Arc<SessionStore>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ListTodosCommandHandler {
|
|
||||||
pub fn new(store: Arc<SessionStore>) -> Self {
|
|
||||||
Self { store }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl CommandHandler for ListTodosCommandHandler {
|
|
||||||
fn can_handle(&self, cmd: &Command) -> bool {
|
|
||||||
matches!(cmd, Command::ListTodos)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn metadata(&self) -> Option<CommandMetadata> {
|
|
||||||
Some(CommandMetadata {
|
|
||||||
name: "list_todos",
|
|
||||||
description: "列出当前 Todo 列表",
|
|
||||||
usage: "/list_todos",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle(
|
|
||||||
&self,
|
|
||||||
_cmd: Command,
|
|
||||||
ctx: CommandContext,
|
|
||||||
) -> Result<CommandResponse, CommandError> {
|
|
||||||
// scope_key = topic_id.unwrap_or(session_id)
|
|
||||||
let scope_key = ctx
|
|
||||||
.topic_id
|
|
||||||
.as_deref()
|
|
||||||
.filter(|t| !t.is_empty())
|
|
||||||
.or(ctx.session_id.as_deref())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
CommandError::new(
|
|
||||||
"MISSING_CONTEXT",
|
|
||||||
"Cannot list todos: no session_id or topic_id in command context",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let records = self
|
|
||||||
.store
|
|
||||||
.list_todos(scope_key)
|
|
||||||
.map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?;
|
|
||||||
|
|
||||||
let summaries: Vec<TodoItemSummary> = records
|
|
||||||
.into_iter()
|
|
||||||
.map(|r| TodoItemSummary {
|
|
||||||
id: r.id,
|
|
||||||
content: r.content,
|
|
||||||
status: r.status,
|
|
||||||
priority: r.priority,
|
|
||||||
created_at: r.created_at,
|
|
||||||
updated_at: r.updated_at,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let todos_json = serde_json::to_string(&summaries)
|
|
||||||
.map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(CommandResponse::success(ctx.request_id).with_metadata("todos", &todos_json))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,7 +5,6 @@ pub mod list_channels;
|
|||||||
pub mod list_memories;
|
pub mod list_memories;
|
||||||
pub mod list_scheduler_jobs;
|
pub mod list_scheduler_jobs;
|
||||||
pub mod list_skills;
|
pub mod list_skills;
|
||||||
pub mod list_todos;
|
|
||||||
pub mod memory_crud;
|
pub mod memory_crud;
|
||||||
pub mod list_sessions;
|
pub mod list_sessions;
|
||||||
pub mod list_sessions_by_channel;
|
pub mod list_sessions_by_channel;
|
||||||
|
|||||||
@ -73,8 +73,6 @@ pub enum Command {
|
|||||||
DeleteMemory { id: String },
|
DeleteMemory { id: String },
|
||||||
/// 列出所有技能
|
/// 列出所有技能
|
||||||
ListSkills,
|
ListSkills,
|
||||||
/// 列出当前 Todo 列表
|
|
||||||
ListTodos,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
@ -102,7 +100,6 @@ impl Command {
|
|||||||
Command::UpdateMemory { .. } => "update_memory",
|
Command::UpdateMemory { .. } => "update_memory",
|
||||||
Command::DeleteMemory { .. } => "delete_memory",
|
Command::DeleteMemory { .. } => "delete_memory",
|
||||||
Command::ListSkills => "list_skills",
|
Command::ListSkills => "list_skills",
|
||||||
Command::ListTodos => "list_todos",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ use std::sync::Arc;
|
|||||||
use crate::agent::{AgentError, AgentLoop, CompositeSystemPromptProvider};
|
use crate::agent::{AgentError, AgentLoop, CompositeSystemPromptProvider};
|
||||||
use crate::config::LLMProviderConfig;
|
use crate::config::LLMProviderConfig;
|
||||||
use crate::gateway::agent_prompt_provider::AgentPromptProvider;
|
use crate::gateway::agent_prompt_provider::AgentPromptProvider;
|
||||||
use crate::gateway::todo_prompt_provider::TodoPromptProvider;
|
|
||||||
use crate::skills::{SkillPromptProvider, SkillRuntime};
|
use crate::skills::{SkillPromptProvider, SkillRuntime};
|
||||||
use crate::storage::persistent_session_id;
|
use crate::storage::persistent_session_id;
|
||||||
use crate::storage::PromptInjectionRepository;
|
use crate::storage::PromptInjectionRepository;
|
||||||
@ -54,7 +53,6 @@ impl AgentFactory {
|
|||||||
self.prompt_repository.clone(),
|
self.prompt_repository.clone(),
|
||||||
)),
|
)),
|
||||||
Box::new(SkillPromptProvider::new(self.skills.clone())),
|
Box::new(SkillPromptProvider::new(self.skills.clone())),
|
||||||
Box::new(TodoPromptProvider::new()),
|
|
||||||
]));
|
]));
|
||||||
|
|
||||||
AgentLoop::with_tools_and_system_prompt_provider(
|
AgentLoop::with_tools_and_system_prompt_provider(
|
||||||
|
|||||||
@ -105,14 +105,9 @@
|
|||||||
- 调用工具的时候必须同时用简短的话告诉用户你调用工具是做什么
|
- 调用工具的时候必须同时用简短的话告诉用户你调用工具是做什么
|
||||||
- 无需担心创建子智能体过多的问题,请按用户或者skill的要求创建对应数量的子智能体,这样可以隔离上下文,更好完成工作
|
- 无需担心创建子智能体过多的问题,请按用户或者skill的要求创建对应数量的子智能体,这样可以隔离上下文,更好完成工作
|
||||||
- 思考的时候建议用中文思考
|
- 思考的时候建议用中文思考
|
||||||
- 涉及到时间的都用get_time工具获取,避免时间不准确
|
|
||||||
|
|
||||||
## 定时任务
|
## 定时任务
|
||||||
|
|
||||||
- 默认创建静默任务(silent_agent_task),在独立后台会话中执行,不干扰主对话
|
- 默认创建静默任务(silent_agent_task),在独立后台会话中执行,不干扰主对话
|
||||||
- 静默模式下如需发送消息给用户,prompt中需显式使用 send_session_message 工具
|
- 静默模式下如需发送消息给用户,prompt中需显式使用 send_session_message 工具
|
||||||
|
|
||||||
## todo工具使用规范
|
|
||||||
|
|
||||||
- 严格按照既定的未完成的todo工作项执行任务,如果工作项不在适用就更新,不得随意遗漏工作项
|
|
||||||
- 禁止将未完成的工作项标记为已完成
|
|
||||||
@ -190,14 +190,6 @@ impl AgentExecutionService {
|
|||||||
// 只有当是最新回合时才触发历史压缩
|
// 只有当是最新回合时才触发历史压缩
|
||||||
let should_schedule_compaction = is_current_turn;
|
let should_schedule_compaction = is_current_turn;
|
||||||
|
|
||||||
// 拦截 todo_write 结果:持久化 + 前端推送
|
|
||||||
if is_current_turn {
|
|
||||||
session.intercept_todo_write_results(
|
|
||||||
&request.result.emitted_messages,
|
|
||||||
request.chat_id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(FinalizedAgentResult {
|
Ok(FinalizedAgentResult {
|
||||||
outbound_messages,
|
outbound_messages,
|
||||||
should_schedule_compaction,
|
should_schedule_compaction,
|
||||||
|
|||||||
@ -25,7 +25,6 @@ pub mod session_message_service;
|
|||||||
pub mod session_pool;
|
pub mod session_pool;
|
||||||
pub mod static_files;
|
pub mod static_files;
|
||||||
pub mod tool_registry_factory;
|
pub mod tool_registry_factory;
|
||||||
pub mod todo_prompt_provider;
|
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
use axum::{Router, routing};
|
use axum::{Router, routing};
|
||||||
|
|||||||
@ -3,8 +3,6 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
use crate::agent::AgentError;
|
use crate::agent::AgentError;
|
||||||
use crate::bus::MessageBus;
|
use crate::bus::MessageBus;
|
||||||
use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, SubagentsConfig, TaskConfig};
|
use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, SubagentsConfig, TaskConfig};
|
||||||
@ -20,7 +18,6 @@ use crate::tools::{
|
|||||||
SessionMessageSender, SubAgentRuntimeConfig, SubagentCatalog, ToolRegistry,
|
SessionMessageSender, SubAgentRuntimeConfig, SubagentCatalog, ToolRegistry,
|
||||||
};
|
};
|
||||||
use crate::tools::task::repository::TaskRepository;
|
use crate::tools::task::repository::TaskRepository;
|
||||||
use crate::tools::todo_write::TodoItem;
|
|
||||||
|
|
||||||
use super::agent_factory::AgentFactory;
|
use super::agent_factory::AgentFactory;
|
||||||
use super::cli_session::CliSessionService;
|
use super::cli_session::CliSessionService;
|
||||||
@ -120,11 +117,6 @@ pub(crate) fn build_session_manager_with_sender(
|
|||||||
task_config.clone(),
|
task_config.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create shared todo state for TodoWriteTool
|
|
||||||
let todo_state: Arc<RwLock<HashMap<String, Vec<TodoItem>>>> =
|
|
||||||
Arc::new(RwLock::new(HashMap::new()));
|
|
||||||
let factory = factory.with_todo_state(todo_state);
|
|
||||||
|
|
||||||
// Create MCP Initializer (async, non-blocking)
|
// Create MCP Initializer (async, non-blocking)
|
||||||
// MCP servers connect in background task
|
// MCP servers connect in background task
|
||||||
let mut mcp_initializer = McpInitializer::with_config(mcp_config);
|
let mut mcp_initializer = McpInitializer::with_config(mcp_config);
|
||||||
|
|||||||
@ -385,84 +385,6 @@ impl Session {
|
|||||||
let _ = self.user_tx.send(msg).await;
|
let _ = self.user_tx.send(msg).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 扫描 agent 结果中的 todo_write 工具消息,
|
|
||||||
/// 提取 todos 并做持久化 + 前端推送(同步版本)。
|
|
||||||
pub(crate) fn intercept_todo_write_results(
|
|
||||||
&self,
|
|
||||||
emitted_messages: &[ChatMessage],
|
|
||||||
chat_id: &str,
|
|
||||||
) {
|
|
||||||
for msg in emitted_messages {
|
|
||||||
if msg.role != "tool" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if msg.tool_name.as_deref() != Some("todo_write") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析工具返回的 JSON
|
|
||||||
let parsed: serde_json::Value = match serde_json::from_str(&msg.content) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(todos_array) = parsed
|
|
||||||
.get("current_todos")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 计算持久化所需的 key
|
|
||||||
let session_id = crate::storage::persistent_session_id(&self.channel_name, chat_id);
|
|
||||||
let topic_id = self.current_topic(chat_id);
|
|
||||||
let scope_key = topic_id.map(|t| t.to_string()).unwrap_or_else(|| session_id.clone());
|
|
||||||
|
|
||||||
// 转换为 TodoRecord 并持久化
|
|
||||||
let records: Vec<crate::storage::TodoRecord> = todos_array
|
|
||||||
.iter()
|
|
||||||
.filter_map(|item| {
|
|
||||||
Some(crate::storage::TodoRecord {
|
|
||||||
id: item.get("id")?.as_str()?.to_string(),
|
|
||||||
scope_key: scope_key.clone(),
|
|
||||||
session_id: session_id.clone(),
|
|
||||||
topic_id: topic_id.map(|t| t.to_string()),
|
|
||||||
content: item.get("content")?.as_str()?.to_string(),
|
|
||||||
status: item.get("status")?.as_str()?.to_string(),
|
|
||||||
priority: item.get("priority")?.as_str()?.to_string(),
|
|
||||||
created_at: item.get("created_at")?.as_i64()?,
|
|
||||||
updated_at: item.get("updated_at")?.as_i64()?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// 持久化到 SQLite
|
|
||||||
if let Err(e) = self.store.replace_todos(&scope_key, &records) {
|
|
||||||
tracing::warn!(error = %e, scope_key = %scope_key, "Failed to persist todo list");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 推送到前端(使用 try_send 避免异步)
|
|
||||||
let summaries: Vec<crate::protocol::TodoItemSummary> = records
|
|
||||||
.iter()
|
|
||||||
.map(|r| crate::protocol::TodoItemSummary {
|
|
||||||
id: r.id.clone(),
|
|
||||||
content: r.content.clone(),
|
|
||||||
status: r.status.clone(),
|
|
||||||
priority: r.priority.clone(),
|
|
||||||
created_at: r.created_at,
|
|
||||||
updated_at: r.updated_at,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let _ = self.user_tx.try_send(crate::protocol::WsOutbound::TodoList {
|
|
||||||
todos: summaries,
|
|
||||||
scope_key: scope_key.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
break; // 只处理第一个成功的 todo_write
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取 provider_config 引用
|
/// 获取 provider_config 引用
|
||||||
pub fn provider_config(&self) -> &LLMProviderConfig {
|
pub fn provider_config(&self) -> &LLMProviderConfig {
|
||||||
&self.provider_config
|
&self.provider_config
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
|
||||||
|
|
||||||
pub struct TodoPromptProvider;
|
|
||||||
|
|
||||||
impl TodoPromptProvider {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SystemPromptProvider for TodoPromptProvider {
|
|
||||||
fn build(&self, _context: &SystemPromptContext) -> Option<SystemPrompt> {
|
|
||||||
Some(SystemPrompt {
|
|
||||||
content: TODO_WRITE_INSTRUCTIONS.to_string(),
|
|
||||||
context: Some("todo_write".to_string()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TODO_WRITE_INSTRUCTIONS: &str = r#"
|
|
||||||
## TodoWrite 工具
|
|
||||||
|
|
||||||
你可以使用 `todo_write` 工具在对话中维护结构化的任务列表。
|
|
||||||
|
|
||||||
### 何时使用
|
|
||||||
- 当任务有 3 个或以上明确步骤时,应该使用 todo_write 追踪进度
|
|
||||||
- 不需要为简单的单步操作(如回答一个问题、读取一个文件)创建 todo
|
|
||||||
|
|
||||||
### merge 参数
|
|
||||||
- `merge: false`(默认):全量替换 — 只传入需要追踪的 todo,不在列表中的项将被移除
|
|
||||||
- `merge: true`(推荐):增量更新 — 只传入需要添加或更新的项,未提及的项保持不变。**绝大多数情况应该使用 merge=true,这样你不需要记住所有 id**
|
|
||||||
|
|
||||||
### 状态语义
|
|
||||||
- `pending` — 尚未开始
|
|
||||||
- `in_progress` — 当前正在执行(同一时间只能有一个)
|
|
||||||
- `completed` — 已完成
|
|
||||||
- `cancelled` — 不再需要
|
|
||||||
|
|
||||||
### 核心规则
|
|
||||||
1. 同一时间只能有一个任务处于 `in_progress` 状态
|
|
||||||
2. 必须先完成当前 `in_progress` 的任务,再开始下一个
|
|
||||||
3. `completed` 和 `cancelled` 是终端状态,已完成的项不能被重新激活
|
|
||||||
4. 不要先标记 completed 再去实际执行 — 先完成工作,再标记
|
|
||||||
5. `content` 字段保持简洁、可执行
|
|
||||||
6. **更新已有任务必须传 `id`**。新任务不传 id(工具会自动生成),更新状态时必须传 id。id 可以从之前 todo_write 返回的 `current_todos` 中获取
|
|
||||||
|
|
||||||
### 使用范例
|
|
||||||
|
|
||||||
创建新任务(新任务必须 pending 或 in_progress,不传 id):
|
|
||||||
```json
|
|
||||||
{"merge": true, "todos": [{"content": "修复登录 bug", "status": "in_progress"}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
追加新任务:
|
|
||||||
```json
|
|
||||||
{"merge": true, "todos": [{"content": "补充测试", "status": "pending"}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
更新已有任务(**必须传 id**,从上次返回的 current_todos 中取得):
|
|
||||||
```json
|
|
||||||
{"merge": true, "todos": [{"id": "abc-123", "content": "修复登录 bug", "status": "completed"}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
同时更新多项:
|
|
||||||
```json
|
|
||||||
{"merge": true, "todos": [
|
|
||||||
{"id": "abc-123", "content": "修复登录 bug", "status": "completed"},
|
|
||||||
{"content": "代码审查", "status": "in_progress"}
|
|
||||||
]}
|
|
||||||
```
|
|
||||||
"#;
|
|
||||||
@ -1,19 +1,16 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
use crate::config::TaskConfig;
|
use crate::config::TaskConfig;
|
||||||
use crate::mcp::McpClientManager;
|
use crate::mcp::McpClientManager;
|
||||||
use crate::skills::SkillRuntime;
|
use crate::skills::SkillRuntime;
|
||||||
use crate::storage::{MemoryRepository, SchedulerJobRepository, SkillEventRepository};
|
use crate::storage::{MemoryRepository, SchedulerJobRepository, SkillEventRepository};
|
||||||
use crate::tools::todo_write::TodoItem;
|
|
||||||
use crate::tools::{
|
use crate::tools::{
|
||||||
BashTool, CalculatorTool, FileEditTool, FileReadTool, FileWriteTool,
|
BashTool, CalculatorTool, FileEditTool, FileReadTool, FileWriteTool,
|
||||||
HttpRequestTool, MemoryManageTool, MemorySearchTool,
|
HttpRequestTool, MemoryManageTool, MemorySearchTool,
|
||||||
SchedulerManageTool, SessionMessageSender, SessionSendTool, SkillActivateTool,
|
SchedulerManageTool, SessionMessageSender, SessionSendTool, SkillActivateTool,
|
||||||
SkillManageTool, SubAgentRuntime, TaskTool, TimeTool,
|
SkillManageTool, SubAgentRuntime, TaskTool, TimeTool,
|
||||||
TodoWriteTool, ToolRegistry, WebFetchTool,
|
ToolRegistry, WebFetchTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct ToolRegistryFactory {
|
pub(crate) struct ToolRegistryFactory {
|
||||||
@ -28,7 +25,6 @@ pub(crate) struct ToolRegistryFactory {
|
|||||||
task_config: TaskConfig,
|
task_config: TaskConfig,
|
||||||
subagent_runtime: Option<Arc<dyn SubAgentRuntime>>,
|
subagent_runtime: Option<Arc<dyn SubAgentRuntime>>,
|
||||||
mcp_manager: Option<Arc<McpClientManager>>,
|
mcp_manager: Option<Arc<McpClientManager>>,
|
||||||
todo_state: Option<Arc<RwLock<HashMap<String, Vec<TodoItem>>>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolRegistryFactory {
|
impl ToolRegistryFactory {
|
||||||
@ -55,18 +51,9 @@ impl ToolRegistryFactory {
|
|||||||
task_config,
|
task_config,
|
||||||
subagent_runtime: None,
|
subagent_runtime: None,
|
||||||
mcp_manager: None,
|
mcp_manager: None,
|
||||||
todo_state: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_todo_state(
|
|
||||||
mut self,
|
|
||||||
state: Arc<RwLock<HashMap<String, Vec<TodoItem>>>>,
|
|
||||||
) -> Self {
|
|
||||||
self.todo_state = Some(state);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn with_subagent_runtime(
|
pub(crate) fn with_subagent_runtime(
|
||||||
mut self,
|
mut self,
|
||||||
runtime: Arc<dyn SubAgentRuntime>,
|
runtime: Arc<dyn SubAgentRuntime>,
|
||||||
@ -111,11 +98,6 @@ impl ToolRegistryFactory {
|
|||||||
if self.is_enabled("memory_manage") {
|
if self.is_enabled("memory_manage") {
|
||||||
registry.register(MemoryManageTool::new(self.memories.clone()));
|
registry.register(MemoryManageTool::new(self.memories.clone()));
|
||||||
}
|
}
|
||||||
if self.is_enabled("todo_write") {
|
|
||||||
if let Some(ref state) = self.todo_state {
|
|
||||||
registry.register(TodoWriteTool::new(state.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.is_enabled("session_send") {
|
if self.is_enabled("session_send") {
|
||||||
registry.register(SessionSendTool::new(self.session_message_sender.clone()));
|
registry.register(SessionSendTool::new(self.session_message_sender.clone()));
|
||||||
}
|
}
|
||||||
@ -216,13 +198,6 @@ impl ToolRegistryFactory {
|
|||||||
registry.register(SessionSendTool::new(self.session_message_sender.clone()));
|
registry.register(SessionSendTool::new(self.session_message_sender.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo 追踪工具
|
|
||||||
if self.is_enabled("todo_write") {
|
|
||||||
if let Some(ref state) = self.todo_state {
|
|
||||||
registry.register(TodoWriteTool::new(state.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册 MCP 工具(如果提供)
|
// 注册 MCP 工具(如果提供)
|
||||||
if let Some(mcp_tools) = mcp_tools {
|
if let Some(mcp_tools) = mcp_tools {
|
||||||
for tool in mcp_tools {
|
for tool in mcp_tools {
|
||||||
|
|||||||
@ -12,7 +12,6 @@ use crate::command::handlers::list_channels::ListChannelsCommandHandler;
|
|||||||
use crate::command::handlers::list_memories::ListMemoriesCommandHandler;
|
use crate::command::handlers::list_memories::ListMemoriesCommandHandler;
|
||||||
use crate::command::handlers::list_skills::ListSkillsCommandHandler;
|
use crate::command::handlers::list_skills::ListSkillsCommandHandler;
|
||||||
use crate::command::handlers::list_scheduler_jobs::ListSchedulerJobsCommandHandler;
|
use crate::command::handlers::list_scheduler_jobs::ListSchedulerJobsCommandHandler;
|
||||||
use crate::command::handlers::list_todos::ListTodosCommandHandler;
|
|
||||||
use crate::command::handlers::memory_crud::MemoryCrudCommandHandler;
|
use crate::command::handlers::memory_crud::MemoryCrudCommandHandler;
|
||||||
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;
|
||||||
@ -423,8 +422,6 @@ async fn handle_inbound(
|
|||||||
router.register(Box::new(ListMemoriesCommandHandler::new(store.clone())));
|
router.register(Box::new(ListMemoriesCommandHandler::new(store.clone())));
|
||||||
// 注册 list_skills 处理器
|
// 注册 list_skills 处理器
|
||||||
router.register(Box::new(ListSkillsCommandHandler::new(skills_for_handler)));
|
router.register(Box::new(ListSkillsCommandHandler::new(skills_for_handler)));
|
||||||
// 注册 list_todos 处理器
|
|
||||||
router.register(Box::new(ListTodosCommandHandler::new(store.clone())));
|
|
||||||
// 注册 memory_crud 处理器
|
// 注册 memory_crud 处理器
|
||||||
router.register(Box::new(MemoryCrudCommandHandler::new(store.clone())));
|
router.register(Box::new(MemoryCrudCommandHandler::new(store.clone())));
|
||||||
// 注册 load_chat_messages 处理器
|
// 注册 load_chat_messages 处理器
|
||||||
@ -522,13 +519,6 @@ async fn handle_inbound(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 Todo 列表
|
|
||||||
if let Some(todos_json) = response.metadata.get("todos") {
|
|
||||||
if let Ok(todos) = serde_json::from_str::<Vec<crate::protocol::TodoItemSummary>>(todos_json) {
|
|
||||||
let _ = sender.send(WsOutbound::TodoList { todos, scope_key: String::new() }).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理记忆列表
|
// 处理记忆列表
|
||||||
if let Some(memories_json) = response.metadata.get("memories") {
|
if let Some(memories_json) = response.metadata.get("memories") {
|
||||||
if let Ok(memories) = serde_json::from_str::<Vec<crate::protocol::MemorySummary>>(memories_json) {
|
if let Ok(memories) = serde_json::from_str::<Vec<crate::protocol::MemorySummary>>(memories_json) {
|
||||||
@ -777,7 +767,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
|
|||||||
let tc_reasoning = if has_content_or_reasoning { None } else { msg.reasoning_content.clone() };
|
let tc_reasoning = if has_content_or_reasoning { None } else { msg.reasoning_content.clone() };
|
||||||
for tool_call in tool_calls {
|
for tool_call in tool_calls {
|
||||||
outbound.push(WsOutbound::ToolCall {
|
outbound.push(WsOutbound::ToolCall {
|
||||||
id: tool_call.id.clone(),
|
id: msg.id.clone(),
|
||||||
tool_call_id: tool_call.id.clone(),
|
tool_call_id: tool_call.id.clone(),
|
||||||
tool_name: tool_call.name.clone(),
|
tool_name: tool_call.name.clone(),
|
||||||
arguments: tool_call.arguments.clone(),
|
arguments: tool_call.arguments.clone(),
|
||||||
@ -808,7 +798,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
|
|||||||
let tool_state = msg.tool_state.as_ref().unwrap_or(&ToolMessageState::Completed);
|
let tool_state = msg.tool_state.as_ref().unwrap_or(&ToolMessageState::Completed);
|
||||||
match tool_state {
|
match tool_state {
|
||||||
ToolMessageState::Completed => vec![WsOutbound::ToolResult {
|
ToolMessageState::Completed => vec![WsOutbound::ToolResult {
|
||||||
id: msg.tool_call_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
|
id: msg.id.clone(),
|
||||||
tool_call_id: msg.tool_call_id.clone().unwrap_or_default(),
|
tool_call_id: msg.tool_call_id.clone().unwrap_or_default(),
|
||||||
tool_name: msg.tool_name.clone().unwrap_or_default(),
|
tool_name: msg.tool_name.clone().unwrap_or_default(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
@ -819,7 +809,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
|
|||||||
timestamp: Some(msg.timestamp / 1000),
|
timestamp: Some(msg.timestamp / 1000),
|
||||||
}],
|
}],
|
||||||
ToolMessageState::PendingUserAction => vec![WsOutbound::ToolPending {
|
ToolMessageState::PendingUserAction => vec![WsOutbound::ToolPending {
|
||||||
id: msg.tool_call_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
|
id: msg.id.clone(),
|
||||||
tool_call_id: msg.tool_call_id.clone().unwrap_or_default(),
|
tool_call_id: msg.tool_call_id.clone().unwrap_or_default(),
|
||||||
tool_name: msg.tool_name.clone().unwrap_or_default(),
|
tool_name: msg.tool_name.clone().unwrap_or_default(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
|
|||||||
@ -82,17 +82,6 @@ pub struct SkillSummary {
|
|||||||
pub source: String,
|
pub source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Todo item 摘要(发送给前端)
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct TodoItemSummary {
|
|
||||||
pub id: String,
|
|
||||||
pub content: String,
|
|
||||||
pub status: String,
|
|
||||||
pub priority: String,
|
|
||||||
pub created_at: i64,
|
|
||||||
pub updated_at: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SchedulerJobSummary {
|
pub struct SchedulerJobSummary {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -213,8 +202,6 @@ pub enum WsOutbound {
|
|||||||
task_id: String,
|
task_id: String,
|
||||||
description: String,
|
description: String,
|
||||||
subagent_type: String,
|
subagent_type: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
topic_id: Option<String>,
|
|
||||||
},
|
},
|
||||||
#[serde(rename = "session_established")]
|
#[serde(rename = "session_established")]
|
||||||
SessionEstablished { session_id: String },
|
SessionEstablished { session_id: String },
|
||||||
@ -268,11 +255,6 @@ pub enum WsOutbound {
|
|||||||
},
|
},
|
||||||
#[serde(rename = "execution_cancelled")]
|
#[serde(rename = "execution_cancelled")]
|
||||||
ExecutionCancelled { message: String },
|
ExecutionCancelled { message: String },
|
||||||
#[serde(rename = "todo_list")]
|
|
||||||
TodoList {
|
|
||||||
todos: Vec<TodoItemSummary>,
|
|
||||||
scope_key: String,
|
|
||||||
},
|
|
||||||
#[serde(rename = "pong")]
|
#[serde(rename = "pong")]
|
||||||
Pong,
|
Pong,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
|||||||
// AssistantResponse 已携带 reasoning 时,ToolCall 不再重复
|
// AssistantResponse 已携带 reasoning 时,ToolCall 不再重复
|
||||||
let tc_reasoning = if has_content_or_reasoning { None } else { message.reasoning_content.clone() };
|
let tc_reasoning = if has_content_or_reasoning { None } else { message.reasoning_content.clone() };
|
||||||
outbound.extend(tool_calls.iter().map(|tool_call| WsOutbound::ToolCall {
|
outbound.extend(tool_calls.iter().map(|tool_call| WsOutbound::ToolCall {
|
||||||
id: tool_call.id.clone(),
|
id: message.id.clone(),
|
||||||
tool_call_id: tool_call.id.clone(),
|
tool_call_id: tool_call.id.clone(),
|
||||||
tool_name: tool_call.name.clone(),
|
tool_name: tool_call.name.clone(),
|
||||||
arguments: tool_call.arguments.clone(),
|
arguments: tool_call.arguments.clone(),
|
||||||
@ -63,7 +63,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
|||||||
.unwrap_or(&ToolMessageState::Completed)
|
.unwrap_or(&ToolMessageState::Completed)
|
||||||
{
|
{
|
||||||
ToolMessageState::Completed => vec![WsOutbound::ToolResult {
|
ToolMessageState::Completed => vec![WsOutbound::ToolResult {
|
||||||
id: message.tool_call_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
|
id: message.id.clone(),
|
||||||
tool_call_id: message.tool_call_id.clone().unwrap_or_default(),
|
tool_call_id: message.tool_call_id.clone().unwrap_or_default(),
|
||||||
tool_name: message.tool_name.clone().unwrap_or_default(),
|
tool_name: message.tool_name.clone().unwrap_or_default(),
|
||||||
content: message.content.clone(),
|
content: message.content.clone(),
|
||||||
@ -74,7 +74,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
|||||||
timestamp: None,
|
timestamp: None,
|
||||||
}],
|
}],
|
||||||
ToolMessageState::PendingUserAction => vec![WsOutbound::ToolPending {
|
ToolMessageState::PendingUserAction => vec![WsOutbound::ToolPending {
|
||||||
id: message.tool_call_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
|
id: message.id.clone(),
|
||||||
tool_call_id: message.tool_call_id.clone().unwrap_or_default(),
|
tool_call_id: message.tool_call_id.clone().unwrap_or_default(),
|
||||||
tool_name: message.tool_name.clone().unwrap_or_default(),
|
tool_name: message.tool_name.clone().unwrap_or_default(),
|
||||||
content: message.content.clone(),
|
content: message.content.clone(),
|
||||||
@ -172,7 +172,6 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
|
|||||||
task_id: message.metadata.get("task_id").cloned().unwrap_or_default(),
|
task_id: message.metadata.get("task_id").cloned().unwrap_or_default(),
|
||||||
description: message.metadata.get("task_description").cloned().unwrap_or_default(),
|
description: message.metadata.get("task_description").cloned().unwrap_or_default(),
|
||||||
subagent_type: message.metadata.get("task_subagent_type").cloned().unwrap_or_default(),
|
subagent_type: message.metadata.get("task_subagent_type").cloned().unwrap_or_default(),
|
||||||
topic_id: message.metadata.get("topic_id").cloned(),
|
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,13 +14,13 @@ pub mod records;
|
|||||||
pub use error::StorageError;
|
pub use error::StorageError;
|
||||||
pub use ports::{
|
pub use ports::{
|
||||||
ConversationRepository, MemoryRepository, PromptInjectionRepository, SchedulerJobRepository,
|
ConversationRepository, MemoryRepository, PromptInjectionRepository, SchedulerJobRepository,
|
||||||
SkillEventRepository, TodoRepository,
|
SkillEventRepository,
|
||||||
};
|
};
|
||||||
pub use records::{
|
pub use records::{
|
||||||
allowed_namespace_names, get_namespace_description, is_valid_namespace,
|
allowed_namespace_names, get_namespace_description, is_valid_namespace,
|
||||||
ALLOWED_MEMORY_NAMESPACES, GLOBAL_SCOPE_KEY, MemoryRecord, MemoryUpsert, SchedulerJobRecord,
|
ALLOWED_MEMORY_NAMESPACES, GLOBAL_SCOPE_KEY, MemoryRecord, MemoryUpsert, SchedulerJobRecord,
|
||||||
SchedulerJobState, SchedulerJobStatus, SchedulerJobUpsert, SessionRecord, SkillEventRecord,
|
SchedulerJobState, SchedulerJobStatus, SchedulerJobUpsert, SessionRecord, SkillEventRecord,
|
||||||
TodoRecord, TopicRecord,
|
TopicRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -217,7 +217,6 @@ impl SessionStore {
|
|||||||
ensure_messages_schema(&conn)?;
|
ensure_messages_schema(&conn)?;
|
||||||
ensure_scheduler_schema(&conn)?;
|
ensure_scheduler_schema(&conn)?;
|
||||||
ensure_memory_scope_key_migration(&conn)?;
|
ensure_memory_scope_key_migration(&conn)?;
|
||||||
ensure_todos_schema(&conn)?;
|
|
||||||
|
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
@ -1492,74 +1491,6 @@ impl SessionStore {
|
|||||||
)
|
)
|
||||||
.map_err(StorageError::from)
|
.map_err(StorageError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace_todos(
|
|
||||||
&self,
|
|
||||||
scope_key: &str,
|
|
||||||
items: &[TodoRecord],
|
|
||||||
) -> Result<Vec<TodoRecord>, StorageError> {
|
|
||||||
let conn = self.pool.get()?;
|
|
||||||
let now = current_timestamp();
|
|
||||||
|
|
||||||
// Delete existing todos for this scope_key
|
|
||||||
conn.execute(
|
|
||||||
"DELETE FROM todos WHERE scope_key = ?1",
|
|
||||||
params![scope_key],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Insert new todos
|
|
||||||
for item in items {
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO todos (id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
|
||||||
params![
|
|
||||||
item.id,
|
|
||||||
scope_key,
|
|
||||||
item.session_id,
|
|
||||||
item.topic_id,
|
|
||||||
item.content,
|
|
||||||
item.status,
|
|
||||||
item.priority,
|
|
||||||
item.created_at,
|
|
||||||
now,
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(conn);
|
|
||||||
|
|
||||||
self.list_todos(scope_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_todos(&self, scope_key: &str) -> Result<Vec<TodoRecord>, StorageError> {
|
|
||||||
let conn = self.pool.get()?;
|
|
||||||
let mut stmt = conn.prepare(
|
|
||||||
"SELECT id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at
|
|
||||||
FROM todos
|
|
||||||
WHERE scope_key = ?1
|
|
||||||
ORDER BY created_at ASC",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let rows = stmt.query_map(params![scope_key], |row| {
|
|
||||||
Ok(TodoRecord {
|
|
||||||
id: row.get(0)?,
|
|
||||||
scope_key: row.get(1)?,
|
|
||||||
session_id: row.get(2)?,
|
|
||||||
topic_id: row.get(3)?,
|
|
||||||
content: row.get(4)?,
|
|
||||||
status: row.get(5)?,
|
|
||||||
priority: row.get(6)?,
|
|
||||||
created_at: row.get(7)?,
|
|
||||||
updated_at: row.get(8)?,
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut todos = Vec::new();
|
|
||||||
for row in rows {
|
|
||||||
todos.push(row?);
|
|
||||||
}
|
|
||||||
Ok(todos)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persistent_session_id(channel_name: &str, chat_id: &str) -> String {
|
pub fn persistent_session_id(channel_name: &str, chat_id: &str) -> String {
|
||||||
@ -1869,42 +1800,6 @@ fn ensure_memory_scope_key_migration(conn: &Connection) -> Result<(), StorageErr
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
|
||||||
let table_exists: bool = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='todos'",
|
|
||||||
[],
|
|
||||||
|row| row.get::<_, i64>(0),
|
|
||||||
)
|
|
||||||
.map(|count| count > 0)?;
|
|
||||||
|
|
||||||
if !table_exists {
|
|
||||||
conn.execute_batch(
|
|
||||||
"
|
|
||||||
CREATE TABLE IF NOT EXISTS todos (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
scope_key TEXT NOT NULL,
|
|
||||||
session_id TEXT NOT NULL,
|
|
||||||
topic_id TEXT,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'pending',
|
|
||||||
priority TEXT NOT NULL DEFAULT 'medium',
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_todos_scope
|
|
||||||
ON todos(scope_key, created_at ASC);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_todos_session
|
|
||||||
ON todos(session_id);
|
|
||||||
",
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_column(
|
fn has_column(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
@ -2114,7 +2009,7 @@ fn load_messages_after(
|
|||||||
messages.push(row?);
|
messages.push(row?);
|
||||||
}
|
}
|
||||||
Ok(messages)
|
Ok(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_timestamp() -> i64 {
|
fn current_timestamp() -> i64 {
|
||||||
std::time::SystemTime::now()
|
std::time::SystemTime::now()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use super::{
|
use super::{
|
||||||
MemoryRecord, MemoryUpsert, SchedulerJobRecord, SchedulerJobState, SchedulerJobStatus,
|
MemoryRecord, MemoryUpsert, SchedulerJobRecord, SchedulerJobState, SchedulerJobStatus,
|
||||||
SchedulerJobUpsert, SessionRecord, SkillEventRecord, StorageError, TodoRecord,
|
SchedulerJobUpsert, SessionRecord, SkillEventRecord, StorageError,
|
||||||
};
|
};
|
||||||
use crate::bus::ChatMessage;
|
use crate::bus::ChatMessage;
|
||||||
|
|
||||||
@ -145,18 +145,6 @@ pub trait SkillEventRepository: Send + Sync + 'static {
|
|||||||
) -> Result<Vec<SkillEventRecord>, StorageError>;
|
) -> Result<Vec<SkillEventRecord>, StorageError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait TodoRepository: Send + Sync + 'static {
|
|
||||||
/// Replace all todos for a scope (full replacement pattern).
|
|
||||||
fn replace_todos(
|
|
||||||
&self,
|
|
||||||
scope_key: &str,
|
|
||||||
todo_records: &[TodoRecord],
|
|
||||||
) -> Result<Vec<TodoRecord>, StorageError>;
|
|
||||||
|
|
||||||
/// Load all todos for a scope, ordered by created_at.
|
|
||||||
fn list_todos(&self, scope_key: &str) -> Result<Vec<TodoRecord>, StorageError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConversationRepository for super::SessionStore {
|
impl ConversationRepository for super::SessionStore {
|
||||||
fn ensure_channel_session(
|
fn ensure_channel_session(
|
||||||
&self,
|
&self,
|
||||||
@ -368,17 +356,3 @@ impl SkillEventRepository for super::SessionStore {
|
|||||||
super::SessionStore::list_skill_events(self, session_id)
|
super::SessionStore::list_skill_events(self, session_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TodoRepository for super::SessionStore {
|
|
||||||
fn replace_todos(
|
|
||||||
&self,
|
|
||||||
scope_key: &str,
|
|
||||||
todo_records: &[TodoRecord],
|
|
||||||
) -> Result<Vec<TodoRecord>, StorageError> {
|
|
||||||
super::SessionStore::replace_todos(self, scope_key, todo_records)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_todos(&self, scope_key: &str) -> Result<Vec<TodoRecord>, StorageError> {
|
|
||||||
super::SessionStore::list_todos(self, scope_key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -35,19 +35,6 @@ pub fn allowed_namespace_names() -> Vec<&'static str> {
|
|||||||
ALLOWED_MEMORY_NAMESPACES.iter().map(|(name, _)| *name).collect()
|
ALLOWED_MEMORY_NAMESPACES.iter().map(|(name, _)| *name).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TodoRecord {
|
|
||||||
pub id: String,
|
|
||||||
pub scope_key: String,
|
|
||||||
pub session_id: String,
|
|
||||||
pub topic_id: Option<String>,
|
|
||||||
pub content: String,
|
|
||||||
pub status: String,
|
|
||||||
pub priority: String,
|
|
||||||
pub created_at: i64,
|
|
||||||
pub updated_at: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SkillEventRecord {
|
pub struct SkillEventRecord {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|||||||
@ -14,7 +14,6 @@ pub mod skill_activate;
|
|||||||
pub mod skill_manage;
|
pub mod skill_manage;
|
||||||
pub mod task;
|
pub mod task;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod todo_write;
|
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
pub mod web_fetch;
|
pub mod web_fetch;
|
||||||
|
|
||||||
@ -40,7 +39,6 @@ pub use task::{
|
|||||||
SubagentCatalog, TaskError, TaskRepository, TaskTool,
|
SubagentCatalog, TaskError, TaskRepository, TaskTool,
|
||||||
};
|
};
|
||||||
pub use time::TimeTool;
|
pub use time::TimeTool;
|
||||||
pub use todo_write::TodoWriteTool;
|
|
||||||
pub use traits::{Tool, ToolContext, ToolResult};
|
pub use traits::{Tool, ToolContext, ToolResult};
|
||||||
pub use web_fetch::WebFetchTool;
|
pub use web_fetch::WebFetchTool;
|
||||||
|
|
||||||
|
|||||||
@ -52,8 +52,6 @@ impl SubagentPromptBuilder {
|
|||||||
2. 使用可用的工具进行必要操作\n\
|
2. 使用可用的工具进行必要操作\n\
|
||||||
3. 完成后给出简洁的总结\n\
|
3. 完成后给出简洁的总结\n\
|
||||||
4. 不要尝试创建新的子代理任务\n\n\
|
4. 不要尝试创建新的子代理任务\n\n\
|
||||||
任务追踪:\n\
|
|
||||||
你可以使用 `todo_write` 工具追踪子任务进度。规则:同一时间只有一个 in_progress,完成后再标记下一个,3步以上才使用。\n\n\
|
|
||||||
注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。"
|
注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。"
|
||||||
} else {
|
} else {
|
||||||
&def.prompt_template
|
&def.prompt_template
|
||||||
|
|||||||
@ -250,7 +250,6 @@ impl DefaultSubAgentRuntime {
|
|||||||
let mut metadata = HashMap::new();
|
let mut metadata = HashMap::new();
|
||||||
metadata.insert("subagent_task_id".to_string(), session.id.clone());
|
metadata.insert("subagent_task_id".to_string(), session.id.clone());
|
||||||
metadata.insert("is_subagent_event".to_string(), "true".to_string());
|
metadata.insert("is_subagent_event".to_string(), "true".to_string());
|
||||||
metadata.insert("topic_id".to_string(), session.parent_topic_id.clone().unwrap_or_default());
|
|
||||||
|
|
||||||
let emitter = Arc::new(PersistingEmittedMessageHandler::new(
|
let emitter = Arc::new(PersistingEmittedMessageHandler::new(
|
||||||
SubAgentEmitter {
|
SubAgentEmitter {
|
||||||
@ -425,7 +424,6 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
metadata.insert("task_id".to_string(), session.id.clone());
|
metadata.insert("task_id".to_string(), session.id.clone());
|
||||||
metadata.insert("task_description".to_string(), session.description.clone());
|
metadata.insert("task_description".to_string(), session.description.clone());
|
||||||
metadata.insert("task_subagent_type".to_string(), session.subagent_type.clone());
|
metadata.insert("task_subagent_type".to_string(), session.subagent_type.clone());
|
||||||
metadata.insert("topic_id".to_string(), session.parent_topic_id.clone().unwrap_or_default());
|
|
||||||
|
|
||||||
let event = OutboundMessage {
|
let event = OutboundMessage {
|
||||||
channel: session.parent_channel_name.clone(),
|
channel: session.parent_channel_name.clone(),
|
||||||
|
|||||||
@ -61,7 +61,7 @@ impl SubagentDef {
|
|||||||
Self {
|
Self {
|
||||||
name: "general".to_string(),
|
name: "general".to_string(),
|
||||||
description: "通用型子代理 - 处理复杂多步骤任务".to_string(),
|
description: "通用型子代理 - 处理复杂多步骤任务".to_string(),
|
||||||
prompt_template: "你是一个专注的子代理,正在执行一个独立任务。\n\n任务描述: {{description}}\n\n你应该:\n1. 专注于完成任务,不要偏离目标\n2. 使用可用的工具进行必要操作\n3. 完成后给出简洁的总结\n4. 不要尝试创建新的子代理任务\n\n任务追踪:\n你可以使用 `todo_write` 工具追踪子任务进度。规则:同一时间只有一个 in_progress,完成后再标记下一个,3步以上才使用。\n\n注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。".to_string(),
|
prompt_template: "你是一个专注的子代理,正在执行一个独立任务。\n\n任务描述: {{description}}\n\n你应该:\n1. 专注于完成任务,不要偏离目标\n2. 使用可用的工具进行必要操作\n3. 完成后给出简洁的总结\n4. 不要尝试创建新的子代理任务\n\n注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。".to_string(),
|
||||||
body: None,
|
body: None,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
max_execution_secs: None,
|
max_execution_secs: None,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ import { TopicList } from './components/Sidebar/TopicList'
|
|||||||
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
|
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
|
||||||
import { MemoryPanel } from './components/Panel/MemoryPanel'
|
import { MemoryPanel } from './components/Panel/MemoryPanel'
|
||||||
import { SkillList } from './components/Panel/SkillList'
|
import { SkillList } from './components/Panel/SkillList'
|
||||||
import { TodoPanel } from './components/Panel/TodoPanel'
|
|
||||||
import { ConnectionStatus } from './components/ConnectionStatus'
|
import { ConnectionStatus } from './components/ConnectionStatus'
|
||||||
import { ChannelSelector } from './components/Header/ChannelSelector'
|
import { ChannelSelector } from './components/Header/ChannelSelector'
|
||||||
import { SessionSelector } from './components/Header/SessionSelector'
|
import { SessionSelector } from './components/Header/SessionSelector'
|
||||||
@ -43,8 +42,6 @@ function App() {
|
|||||||
// 技能
|
// 技能
|
||||||
skills,
|
skills,
|
||||||
requestSkillList,
|
requestSkillList,
|
||||||
todos,
|
|
||||||
requestTodoList,
|
|
||||||
// 定时任务
|
// 定时任务
|
||||||
schedulerJobs,
|
schedulerJobs,
|
||||||
sidebarTab,
|
sidebarTab,
|
||||||
@ -323,7 +320,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList])
|
}, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList])
|
||||||
|
|
||||||
// 连接就绪时自动拉取记忆、技能和待办列表
|
// 连接就绪时自动拉取记忆和技能列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'connected') {
|
if (status === 'connected') {
|
||||||
const memCmd = requestMemoryList()
|
const memCmd = requestMemoryList()
|
||||||
@ -332,11 +329,8 @@ function App() {
|
|||||||
const skillCmd = requestSkillList()
|
const skillCmd = requestSkillList()
|
||||||
handleCommand(skillCmd)
|
handleCommand(skillCmd)
|
||||||
sendMessage({ type: 'command', payload: JSON.stringify(skillCmd) })
|
sendMessage({ type: 'command', payload: JSON.stringify(skillCmd) })
|
||||||
const todoCmd = requestTodoList()
|
|
||||||
handleCommand(todoCmd)
|
|
||||||
sendMessage({ type: 'command', payload: JSON.stringify(todoCmd) })
|
|
||||||
}
|
}
|
||||||
}, [status, handleCommand, sendMessage, requestMemoryList, requestSkillList, requestTodoList])
|
}, [status, handleCommand, sendMessage, requestMemoryList, requestSkillList])
|
||||||
|
|
||||||
const handleRefreshMemories = useCallback(() => {
|
const handleRefreshMemories = useCallback(() => {
|
||||||
const cmd = requestMemoryList()
|
const cmd = requestMemoryList()
|
||||||
@ -683,13 +677,6 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 悬浮 Todo 面板 */}
|
|
||||||
<TodoPanel
|
|
||||||
todos={todos}
|
|
||||||
requestTodoList={requestTodoList}
|
|
||||||
sendCommand={sendMemoryCommand}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,223 +0,0 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
||||||
import { ClipboardList, ChevronUp, ChevronDown, Circle } from 'lucide-react'
|
|
||||||
import type { TodoItemSummary, Command } from '../../types/protocol'
|
|
||||||
|
|
||||||
interface TodoPanelProps {
|
|
||||||
todos: TodoItemSummary[]
|
|
||||||
requestTodoList: () => Command
|
|
||||||
sendCommand: (cmd: Command) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── status helpers ───────────────────────────────────── */
|
|
||||||
|
|
||||||
interface StatusStyle { label: string; border: string; dot: string; text: string; icon: string }
|
|
||||||
|
|
||||||
const STATUS: Record<string, StatusStyle> = {
|
|
||||||
in_progress: { label: '进行中', border: 'border-amber-400/60', dot: 'bg-amber-400 shadow-[0_0_6px_#fbbf24]', text: 'text-amber-300', icon: '●' },
|
|
||||||
pending: { label: '待处理', border: 'border-slate-500/50', dot: 'bg-slate-500', text: 'text-slate-400', icon: '○' },
|
|
||||||
completed: { label: '已完成', border: 'border-emerald-400/40', dot: 'bg-emerald-400', text: 'text-emerald-400', icon: '✓' },
|
|
||||||
cancelled: { label: '已取消', border: 'border-red-400/40', dot: 'bg-red-400', text: 'text-red-400', icon: '✕' },
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusStyle(s: string): StatusStyle {
|
|
||||||
return STATUS[s] ?? { label: s, border: 'border-[var(--border-color)]', dot: 'bg-[var(--text-muted)]', text: 'text-[var(--text-muted)]', icon: '?' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRIORITY: Record<string, string> = {
|
|
||||||
high: 'text-rose-400',
|
|
||||||
medium: 'text-amber-400',
|
|
||||||
low: 'text-slate-400',
|
|
||||||
}
|
|
||||||
|
|
||||||
function priorityDot(p: string) { return PRIORITY[p] ?? 'text-slate-400' }
|
|
||||||
|
|
||||||
/* ── group helpers ────────────────────────────────────── */
|
|
||||||
|
|
||||||
const GROUP_ORDER = ['in_progress', 'pending', 'completed', 'cancelled']
|
|
||||||
const COLLAPSED_DEFAULT = new Set(['completed', 'cancelled'])
|
|
||||||
|
|
||||||
function groupTodos(todos: TodoItemSummary[]): Map<string, TodoItemSummary[]> {
|
|
||||||
const map = new Map<string, TodoItemSummary[]>()
|
|
||||||
for (const t of todos) {
|
|
||||||
const list = map.get(t.status) ?? []
|
|
||||||
list.push(t)
|
|
||||||
map.set(t.status, list)
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── pulse dot ────────────────────────────────────────── */
|
|
||||||
|
|
||||||
function PulseDot() {
|
|
||||||
return (
|
|
||||||
<span className="relative flex h-2 w-2">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75" />
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-amber-400 shadow-[0_0_6px_#fbbf24]" />
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── TodoPanel ────────────────────────────────────────── */
|
|
||||||
|
|
||||||
export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProps) {
|
|
||||||
const [expanded, setExpanded] = useState(() => {
|
|
||||||
try { return localStorage.getItem('picobot-todo-panel-open') === 'true' } catch { return false }
|
|
||||||
})
|
|
||||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => new Set(COLLAPSED_DEFAULT))
|
|
||||||
const prevTodoIdsRef = useRef<Set<string>>(new Set())
|
|
||||||
|
|
||||||
// 持久化展开状态
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem('picobot-todo-panel-open', String(expanded))
|
|
||||||
}, [expanded])
|
|
||||||
|
|
||||||
// 当有新 todo 出现时自动展开
|
|
||||||
useEffect(() => {
|
|
||||||
const newIds = new Set(todos.map(t => t.id))
|
|
||||||
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
|
|
||||||
if (hasNewItems && todos.length > 0) {
|
|
||||||
setExpanded(true)
|
|
||||||
}
|
|
||||||
prevTodoIdsRef.current = newIds
|
|
||||||
}, [todos])
|
|
||||||
|
|
||||||
const grouped = groupTodos(todos)
|
|
||||||
const inProgressCount = grouped.get('in_progress')?.length ?? 0
|
|
||||||
const totalCount = todos.length
|
|
||||||
|
|
||||||
const toggleGroup = useCallback((status: string) => {
|
|
||||||
setCollapsedGroups(prev => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
if (next.has(status)) next.delete(status); else next.add(status)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// ── 空状态 ──
|
|
||||||
if (totalCount === 0) {
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-4 right-4 z-50">
|
|
||||||
<button
|
|
||||||
onClick={() => sendCommand(requestTodoList())}
|
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-xl bg-[var(--bg-tertiary)]/70 backdrop-blur-sm border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:border-[var(--border-accent)] transition-all duration-300 shadow-lg text-xs"
|
|
||||||
title="刷新待办"
|
|
||||||
>
|
|
||||||
<ClipboardList className="h-3.5 w-3.5" />
|
|
||||||
<span>待办</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 折叠态 ──
|
|
||||||
if (!expanded) {
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-4 right-4 z-50">
|
|
||||||
<button
|
|
||||||
onClick={() => setExpanded(true)}
|
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-xl bg-[var(--bg-tertiary)]/80 backdrop-blur-sm border border-[var(--border-color)] hover:border-[var(--accent-cyan)]/30 transition-all duration-300 shadow-lg group"
|
|
||||||
>
|
|
||||||
{inProgressCount > 0 ? <PulseDot /> : <ClipboardList className="h-3.5 w-3.5 text-[var(--text-muted)]" />}
|
|
||||||
<span className="text-sm font-medium text-[var(--text-secondary)] group-hover:text-[var(--accent-cyan)] transition-colors">
|
|
||||||
待办 ({totalCount})
|
|
||||||
</span>
|
|
||||||
<ChevronUp className="h-3.5 w-3.5 text-[var(--text-muted)]" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 展开态 ──
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-4 right-4 z-50 w-72 max-h-[60vh] flex flex-col rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)]/95 backdrop-blur-md shadow-2xl overflow-hidden transition-all duration-300">
|
|
||||||
{/* 标题栏 */}
|
|
||||||
<div className="shrink-0 flex items-center justify-between px-3 py-2.5 border-b border-[var(--border-color)]">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ClipboardList className="h-4 w-4 text-[var(--accent-cyan)]" />
|
|
||||||
<span className="text-sm font-semibold text-[var(--text-primary)]">待办事项</span>
|
|
||||||
<span className="text-[11px] text-[var(--text-muted)] tabular-nums">共 {totalCount} 项</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setExpanded(false)}
|
|
||||||
className="p-1 rounded hover:bg-[var(--overlay-subtle)] text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 列表区 */}
|
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-hide px-2 py-2 space-y-2">
|
|
||||||
{GROUP_ORDER.map(status => {
|
|
||||||
const items = grouped.get(status)
|
|
||||||
if (!items || items.length === 0) return null
|
|
||||||
|
|
||||||
const style = statusStyle(status)
|
|
||||||
const isCollapsed = collapsedGroups.has(status)
|
|
||||||
const isTerminal = status === 'completed' || status === 'cancelled'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={status}>
|
|
||||||
{/* 分组标题 */}
|
|
||||||
<button
|
|
||||||
onClick={() => toggleGroup(status)}
|
|
||||||
className="flex items-center gap-1.5 w-full px-1 py-1 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
|
|
||||||
>
|
|
||||||
{isCollapsed ? (
|
|
||||||
<ChevronUp className="h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
<span className={style.text}>{style.icon}</span>
|
|
||||||
<span>{style.label}</span>
|
|
||||||
<span className="tabular-nums">({items.length})</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* 卡片列表 */}
|
|
||||||
{!isCollapsed && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{items.map(item => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className={`rounded-lg border-l-2 ${style.border} border border-[var(--border-color)] bg-[var(--overlay-hover)]/50 px-2.5 py-1.5 transition-colors hover:border-[var(--border-accent)]`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className={`mt-0.5 shrink-0 ${priorityDot(item.priority)}`}>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</span>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className={`text-[13px] leading-snug text-[var(--text-primary)] break-words ${
|
|
||||||
isTerminal ? 'line-through opacity-50' : ''
|
|
||||||
}`}>
|
|
||||||
{item.content}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2 mt-0.5">
|
|
||||||
<span className={`text-[10px] ${priorityDot(item.priority)}`}>
|
|
||||||
{item.priority}
|
|
||||||
</span>
|
|
||||||
<span className={`text-[10px] ${style.text}`}>
|
|
||||||
{style.icon} {style.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部刷新 */}
|
|
||||||
<div className="shrink-0 border-t border-[var(--border-color)] px-3 py-1.5">
|
|
||||||
<button
|
|
||||||
onClick={() => sendCommand(requestTodoList())}
|
|
||||||
className="text-[10px] text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors"
|
|
||||||
>
|
|
||||||
刷新
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -20,8 +20,6 @@ import type {
|
|||||||
MemoryList,
|
MemoryList,
|
||||||
SkillSummary,
|
SkillSummary,
|
||||||
SkillList,
|
SkillList,
|
||||||
TodoItemSummary,
|
|
||||||
TodoList,
|
|
||||||
SchedulerJobList,
|
SchedulerJobList,
|
||||||
SchedulerJobSummary,
|
SchedulerJobSummary,
|
||||||
SchedulerJobSessionLookup,
|
SchedulerJobSessionLookup,
|
||||||
@ -94,10 +92,6 @@ interface UseChatReturn {
|
|||||||
skills: SkillSummary[]
|
skills: SkillSummary[]
|
||||||
requestSkillList: () => Command
|
requestSkillList: () => Command
|
||||||
|
|
||||||
// Todo 状态
|
|
||||||
todos: TodoItemSummary[]
|
|
||||||
requestTodoList: () => Command
|
|
||||||
|
|
||||||
// 定时任务状态
|
// 定时任务状态
|
||||||
schedulerJobs: SchedulerJobSummary[]
|
schedulerJobs: SchedulerJobSummary[]
|
||||||
sidebarTab: 'topics' | 'scheduler'
|
sidebarTab: 'topics' | 'scheduler'
|
||||||
@ -146,7 +140,6 @@ export function useChat(): UseChatReturn {
|
|||||||
const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null)
|
const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null)
|
||||||
const [memories, setMemories] = useState<MemorySummary[]>([])
|
const [memories, setMemories] = useState<MemorySummary[]>([])
|
||||||
const [skills, setSkills] = useState<SkillSummary[]>([])
|
const [skills, setSkills] = useState<SkillSummary[]>([])
|
||||||
const [todos, setTodos] = useState<TodoItemSummary[]>([])
|
|
||||||
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
|
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
|
||||||
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
|
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
|
||||||
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
|
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
|
||||||
@ -188,15 +181,6 @@ export function useChat(): UseChatReturn {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract topic_id from a message if present
|
|
||||||
const getTopicId = (message: WsOutbound): string | undefined => {
|
|
||||||
if (message.type === 'tool_call' || message.type === 'tool_result'
|
|
||||||
|| message.type === 'tool_pending' || message.type === 'assistant_response') {
|
|
||||||
return (message as ToolCall | ToolResult | ToolPending | AssistantResponse).topic_id
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a server message to ChatMessage (extracted from handleServerMessage logic)
|
// Convert a server message to ChatMessage (extracted from handleServerMessage logic)
|
||||||
const serverMessageToChatMessage = (message: WsOutbound): ChatMessage | null => {
|
const serverMessageToChatMessage = (message: WsOutbound): ChatMessage | null => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
@ -327,20 +311,12 @@ export function useChat(): UseChatReturn {
|
|||||||
appendToSubAgentViewMessage(message)
|
appendToSubAgentViewMessage(message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 丢弃其他子智能体的消息,避免 fall through 到主消息处理
|
|
||||||
if (msgSubagentTaskId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In main view, skip sub-agent messages (they belong to sub-agent view).
|
// In main view, skip sub-agent messages (they belong to sub-agent view).
|
||||||
// But use the task_id to associate with the running task tool card.
|
// But use the task_id to associate with the running task tool card.
|
||||||
const msgSubagentTaskId = getSubagentTaskId(message)
|
const msgSubagentTaskId = getSubagentTaskId(message)
|
||||||
if (msgSubagentTaskId) {
|
if (msgSubagentTaskId) {
|
||||||
// 只 backfill 当前话题的 task tool_call,避免跨话题串扰
|
|
||||||
const msgTopicId = getTopicId(message)
|
|
||||||
if (msgTopicId && msgTopicId !== selectedTopicRef.current) return
|
|
||||||
|
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
for (let i = prev.length - 1; i >= 0; i--) {
|
for (let i = prev.length - 1; i >= 0; i--) {
|
||||||
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].subagentTaskId) {
|
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].subagentTaskId) {
|
||||||
@ -364,9 +340,6 @@ export function useChat(): UseChatReturn {
|
|||||||
|
|
||||||
case 'task_started': {
|
case 'task_started': {
|
||||||
const msg = message as TaskStarted
|
const msg = message as TaskStarted
|
||||||
// 只 backfill 当前话题的 task tool_call,避免跨话题串扰
|
|
||||||
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) break
|
|
||||||
|
|
||||||
// 立即更新对应的 task tool_call,让用户可以点击查看实时进度
|
// 立即更新对应的 task tool_call,让用户可以点击查看实时进度
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
for (let i = prev.length - 1; i >= 0; i--) {
|
for (let i = prev.length - 1; i >= 0; i--) {
|
||||||
@ -579,11 +552,6 @@ export function useChat(): UseChatReturn {
|
|||||||
setSkills(msg.skills)
|
setSkills(msg.skills)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'todo_list': {
|
|
||||||
const msg = message as TodoList
|
|
||||||
setTodos(msg.todos)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'channel_list': {
|
case 'channel_list': {
|
||||||
const msg = message as ChannelList
|
const msg = message as ChannelList
|
||||||
@ -634,7 +602,6 @@ export function useChat(): UseChatReturn {
|
|||||||
const selectTopic = useCallback((topicId: string) => {
|
const selectTopic = useCallback((topicId: string) => {
|
||||||
setSelectedTopic(topicId)
|
setSelectedTopic(topicId)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setSubAgentView(null)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const createTopic = useCallback((title?: string): Command => {
|
const createTopic = useCallback((title?: string): Command => {
|
||||||
@ -680,7 +647,6 @@ export function useChat(): UseChatReturn {
|
|||||||
setTopics([])
|
setTopics([])
|
||||||
setSelectedTopic(null)
|
setSelectedTopic(null)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setSubAgentView(null)
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
}, [selectedChannel])
|
}, [selectedChannel])
|
||||||
|
|
||||||
@ -690,7 +656,6 @@ export function useChat(): UseChatReturn {
|
|||||||
setTopics([])
|
setTopics([])
|
||||||
setSelectedTopic(null)
|
setSelectedTopic(null)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setSubAgentView(null)
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
}, [selectedSessionId])
|
}, [selectedSessionId])
|
||||||
|
|
||||||
@ -762,10 +727,6 @@ export function useChat(): UseChatReturn {
|
|||||||
return { type: 'list_skills' }
|
return { type: 'list_skills' }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const requestTodoList = useCallback((): Command => {
|
|
||||||
return { type: 'list_todos' }
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 定时任务方法
|
// 定时任务方法
|
||||||
const requestSchedulerJobList = useCallback((): Command => {
|
const requestSchedulerJobList = useCallback((): Command => {
|
||||||
return { type: 'list_scheduler_jobs' }
|
return { type: 'list_scheduler_jobs' }
|
||||||
@ -854,8 +815,6 @@ export function useChat(): UseChatReturn {
|
|||||||
deleteMemory,
|
deleteMemory,
|
||||||
skills,
|
skills,
|
||||||
requestSkillList,
|
requestSkillList,
|
||||||
todos,
|
|
||||||
requestTodoList,
|
|
||||||
schedulerJobs,
|
schedulerJobs,
|
||||||
sidebarTab,
|
sidebarTab,
|
||||||
setSidebarTab,
|
setSidebarTab,
|
||||||
|
|||||||
@ -100,7 +100,6 @@ export interface TaskStarted {
|
|||||||
task_id: string
|
task_id: string
|
||||||
description: string
|
description: string
|
||||||
subagent_type: string
|
subagent_type: string
|
||||||
topic_id?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionEstablished {
|
export interface SessionEstablished {
|
||||||
@ -201,21 +200,6 @@ export interface SkillList {
|
|||||||
skills: SkillSummary[]
|
skills: SkillSummary[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TodoItemSummary {
|
|
||||||
id: string
|
|
||||||
content: string
|
|
||||||
status: string
|
|
||||||
priority: string
|
|
||||||
created_at: number
|
|
||||||
updated_at: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TodoList {
|
|
||||||
type: 'todo_list'
|
|
||||||
todos: TodoItemSummary[]
|
|
||||||
scope_key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SchedulerJobSessionLookup {
|
export interface SchedulerJobSessionLookup {
|
||||||
channel: string
|
channel: string
|
||||||
chat_id: string
|
chat_id: string
|
||||||
@ -274,7 +258,6 @@ export type WsOutbound =
|
|||||||
| SchedulerJobList
|
| SchedulerJobList
|
||||||
| MemoryList
|
| MemoryList
|
||||||
| SkillList
|
| SkillList
|
||||||
| TodoList
|
|
||||||
| ExecutionCancelled
|
| ExecutionCancelled
|
||||||
| Pong
|
| Pong
|
||||||
|
|
||||||
@ -388,10 +371,6 @@ export interface ListSkillsCommand {
|
|||||||
type: 'list_skills'
|
type: 'list_skills'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListTodosCommand {
|
|
||||||
type: 'list_todos'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Command =
|
export type Command =
|
||||||
| CreateSessionCommand
|
| CreateSessionCommand
|
||||||
| ListSessionsCommand
|
| ListSessionsCommand
|
||||||
@ -414,7 +393,6 @@ export type Command =
|
|||||||
| UpdateMemoryCommand
|
| UpdateMemoryCommand
|
||||||
| DeleteMemoryCommand
|
| DeleteMemoryCommand
|
||||||
| ListSkillsCommand
|
| ListSkillsCommand
|
||||||
| ListTodosCommand
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// UI Types
|
// UI Types
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user