chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -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) {