chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -131,12 +131,30 @@ impl AgentLoop {
|
||||
|
||||
/// Create tool context for tool execution
|
||||
fn create_tool_context(&self, session_id: SessionId) -> ToolContext {
|
||||
// If no path_validator is configured, create a default one with user home as workspace.
|
||||
// This allows file_read/file_write tools to work without explicit workspace config,
|
||||
// while still restricting access to the user's home directory for security.
|
||||
let path_validator = self.path_validator.clone().unwrap_or_else(|| {
|
||||
let home = std::env::var("USERPROFILE")
|
||||
.or_else(|_| std::env::var("HOME"))
|
||||
.unwrap_or_else(|_| ".".to_string());
|
||||
let home_path = std::path::PathBuf::from(&home);
|
||||
tracing::info!(
|
||||
"[AgentLoop] No path_validator configured, using user home as workspace: {}",
|
||||
home_path.display()
|
||||
);
|
||||
PathValidator::new().with_workspace(home_path)
|
||||
});
|
||||
|
||||
let working_dir = path_validator.workspace_root()
|
||||
.map(|p| p.to_string_lossy().to_string());
|
||||
|
||||
ToolContext {
|
||||
agent_id: self.agent_id.clone(),
|
||||
working_directory: None,
|
||||
working_directory: working_dir,
|
||||
session_id: Some(session_id.to_string()),
|
||||
skill_executor: self.skill_executor.clone(),
|
||||
path_validator: self.path_validator.clone(),
|
||||
path_validator: Some(path_validator),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +240,14 @@ impl AgentLoop {
|
||||
total_input_tokens += response.input_tokens;
|
||||
total_output_tokens += response.output_tokens;
|
||||
|
||||
// Calibrate token estimation on first iteration
|
||||
if iterations == 1 {
|
||||
compaction::update_calibration(
|
||||
compaction::estimate_messages_tokens(&messages),
|
||||
response.input_tokens,
|
||||
);
|
||||
}
|
||||
|
||||
// Extract tool calls from response
|
||||
let tool_calls: Vec<(String, String, serde_json::Value)> = response.content.iter()
|
||||
.filter_map(|block| match block {
|
||||
@@ -230,30 +256,49 @@ impl AgentLoop {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Extract text and thinking separately
|
||||
let text_parts: Vec<String> = response.content.iter()
|
||||
.filter_map(|block| match block {
|
||||
ContentBlock::Text { text } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let thinking_parts: Vec<String> = response.content.iter()
|
||||
.filter_map(|block| match block {
|
||||
ContentBlock::Thinking { thinking } => Some(thinking.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let text_content = text_parts.join("\n");
|
||||
let thinking_content = if thinking_parts.is_empty() { None } else { Some(thinking_parts.join("")) };
|
||||
|
||||
// If no tool calls, we have the final response
|
||||
if tool_calls.is_empty() {
|
||||
// Extract text content
|
||||
let text = response.content.iter()
|
||||
.filter_map(|block| match block {
|
||||
ContentBlock::Text { text } => Some(text.clone()),
|
||||
ContentBlock::Thinking { thinking } => Some(format!("[思考] {}", thinking)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
// Save final assistant message
|
||||
self.memory.append_message(&session_id, &Message::assistant(&text)).await?;
|
||||
// Save final assistant message with thinking
|
||||
let msg = if let Some(thinking) = &thinking_content {
|
||||
Message::assistant_with_thinking(&text_content, thinking)
|
||||
} else {
|
||||
Message::assistant(&text_content)
|
||||
};
|
||||
self.memory.append_message(&session_id, &msg).await?;
|
||||
|
||||
break AgentLoopResult {
|
||||
response: text,
|
||||
response: text_content,
|
||||
input_tokens: total_input_tokens,
|
||||
output_tokens: total_output_tokens,
|
||||
iterations,
|
||||
};
|
||||
}
|
||||
|
||||
// There are tool calls - add assistant message with tool calls to history
|
||||
// There are tool calls - push assistant message with thinking before tool calls
|
||||
// (required by Kimi and other thinking-enabled APIs)
|
||||
let assistant_msg = if let Some(thinking) = &thinking_content {
|
||||
Message::assistant_with_thinking(&text_content, thinking)
|
||||
} else {
|
||||
Message::assistant(&text_content)
|
||||
};
|
||||
messages.push(assistant_msg);
|
||||
|
||||
for (id, name, input) in &tool_calls {
|
||||
messages.push(Message::tool_use(id, zclaw_types::ToolId::new(name), input.clone()));
|
||||
}
|
||||
@@ -417,19 +462,29 @@ impl AgentLoop {
|
||||
let mut stream = driver.stream(request);
|
||||
let mut pending_tool_calls: Vec<(String, String, serde_json::Value)> = Vec::new();
|
||||
let mut iteration_text = String::new();
|
||||
let mut reasoning_text = String::new(); // Track reasoning separately for API requirement
|
||||
|
||||
// Process stream chunks
|
||||
tracing::debug!("[AgentLoop] Starting to process stream chunks");
|
||||
let mut chunk_count: usize = 0;
|
||||
let mut text_delta_count: usize = 0;
|
||||
let mut thinking_delta_count: usize = 0;
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
match chunk_result {
|
||||
Ok(chunk) => {
|
||||
chunk_count += 1;
|
||||
match &chunk {
|
||||
StreamChunk::TextDelta { delta } => {
|
||||
text_delta_count += 1;
|
||||
tracing::debug!("[AgentLoop] TextDelta #{}: {} chars", text_delta_count, delta.len());
|
||||
iteration_text.push_str(delta);
|
||||
let _ = tx.send(LoopEvent::Delta(delta.clone())).await;
|
||||
}
|
||||
StreamChunk::ThinkingDelta { delta } => {
|
||||
let _ = tx.send(LoopEvent::Delta(format!("[思考] {}", delta))).await;
|
||||
thinking_delta_count += 1;
|
||||
tracing::debug!("[AgentLoop] ThinkingDelta #{}: {} chars", thinking_delta_count, delta.len());
|
||||
// Accumulate reasoning separately — not mixed into iteration_text
|
||||
reasoning_text.push_str(delta);
|
||||
}
|
||||
StreamChunk::ToolUseStart { id, name } => {
|
||||
tracing::debug!("[AgentLoop] ToolUseStart: id={}, name={}", id, name);
|
||||
@@ -458,6 +513,13 @@ impl AgentLoop {
|
||||
tracing::debug!("[AgentLoop] Stream complete: input_tokens={}, output_tokens={}", it, ot);
|
||||
total_input_tokens += *it;
|
||||
total_output_tokens += *ot;
|
||||
// Calibrate token estimation on first iteration
|
||||
if iteration == 1 {
|
||||
compaction::update_calibration(
|
||||
compaction::estimate_messages_tokens(&messages),
|
||||
*it,
|
||||
);
|
||||
}
|
||||
}
|
||||
StreamChunk::Error { message } => {
|
||||
tracing::error!("[AgentLoop] Stream error: {}", message);
|
||||
@@ -471,16 +533,27 @@ impl AgentLoop {
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::debug!("[AgentLoop] Stream ended, pending_tool_calls count: {}", pending_tool_calls.len());
|
||||
tracing::info!("[AgentLoop] Stream ended: {} total chunks (text={}, thinking={}, tools={}), iteration_text={} chars",
|
||||
chunk_count, text_delta_count, thinking_delta_count, pending_tool_calls.len(),
|
||||
iteration_text.len());
|
||||
if iteration_text.is_empty() {
|
||||
tracing::warn!("[AgentLoop] WARNING: iteration_text is EMPTY after {} chunks! text_delta={}, thinking_delta={}",
|
||||
chunk_count, text_delta_count, thinking_delta_count);
|
||||
}
|
||||
|
||||
// If no tool calls, we have the final response
|
||||
if pending_tool_calls.is_empty() {
|
||||
tracing::debug!("[AgentLoop] No tool calls, returning final response");
|
||||
// Save final assistant message
|
||||
let _ = memory.append_message(&session_id_clone, &Message::assistant(&iteration_text)).await;
|
||||
tracing::info!("[AgentLoop] No tool calls, returning final response: {} chars (reasoning: {} chars)", iteration_text.len(), reasoning_text.len());
|
||||
// Save final assistant message with reasoning
|
||||
if let Err(e) = memory.append_message(&session_id_clone, &Message::assistant_with_thinking(
|
||||
&iteration_text,
|
||||
&reasoning_text,
|
||||
)).await {
|
||||
tracing::warn!("[AgentLoop] Failed to save final assistant message: {}", e);
|
||||
}
|
||||
|
||||
let _ = tx.send(LoopEvent::Complete(AgentLoopResult {
|
||||
response: iteration_text,
|
||||
response: iteration_text.clone(),
|
||||
input_tokens: total_input_tokens,
|
||||
output_tokens: total_output_tokens,
|
||||
iterations: iteration,
|
||||
@@ -488,7 +561,13 @@ impl AgentLoop {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
tracing::debug!("[AgentLoop] Processing {} tool calls", pending_tool_calls.len());
|
||||
tracing::debug!("[AgentLoop] Processing {} tool calls (reasoning: {} chars)", pending_tool_calls.len(), reasoning_text.len());
|
||||
|
||||
// Push assistant message with reasoning before tool calls (required by Kimi and other thinking-enabled APIs)
|
||||
messages.push(Message::assistant_with_thinking(
|
||||
&iteration_text,
|
||||
&reasoning_text,
|
||||
));
|
||||
|
||||
// There are tool calls - add to message history
|
||||
for (id, name, input) in &pending_tool_calls {
|
||||
@@ -519,12 +598,21 @@ impl AgentLoop {
|
||||
}
|
||||
LoopGuardResult::Allowed => {}
|
||||
}
|
||||
// Use pre-resolved path_validator (already has default fallback from create_tool_context logic)
|
||||
let pv = path_validator.clone().unwrap_or_else(|| {
|
||||
let home = std::env::var("USERPROFILE")
|
||||
.or_else(|_| std::env::var("HOME"))
|
||||
.unwrap_or_else(|_| ".".to_string());
|
||||
PathValidator::new().with_workspace(std::path::PathBuf::from(&home))
|
||||
});
|
||||
let working_dir = pv.workspace_root()
|
||||
.map(|p| p.to_string_lossy().to_string());
|
||||
let tool_context = ToolContext {
|
||||
agent_id: agent_id.clone(),
|
||||
working_directory: None,
|
||||
working_directory: working_dir,
|
||||
session_id: Some(session_id_clone.to_string()),
|
||||
skill_executor: skill_executor.clone(),
|
||||
path_validator: path_validator.clone(),
|
||||
path_validator: Some(pv),
|
||||
};
|
||||
|
||||
let (result, is_error) = if let Some(tool) = tools.get(&name) {
|
||||
|
||||
Reference in New Issue
Block a user