feat: update automation workspace and release pipeline

This commit is contained in:
coso
2026-03-25 23:13:17 +08:00
parent b8c10ad8ea
commit 8b9f91bd31
26 changed files with 2476 additions and 813 deletions
+1 -1
View File
@@ -237,6 +237,7 @@ tauri-plugin-dialog.workspace = true
tauri-plugin-single-instance.workspace = true
tauri-plugin-global-shortcut.workspace = true
tauri-plugin-updater.workspace = true
tauri-plugin-deep-link.workspace = true
# 序列化
serde.workspace = true
@@ -343,7 +344,6 @@ winreg.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
cocoa.workspace = true
objc.workspace = true
tauri-plugin-deep-link.workspace = true
[dev-dependencies]
proptest.workspace = true
@@ -3,17 +3,25 @@
//! 负责把结构化自动化任务映射到 Aster 执行链路。
use super::{AutomationJobRecord, AutomationPayload};
use crate::agent::{AsterAgentState, AsterAgentWrapper};
use crate::agent::AsterAgentWrapper;
use crate::app::AppState;
use crate::commands::api_key_provider_cmd::ApiKeyProviderServiceState;
use crate::commands::aster_agent_cmd::{
build_queued_turn_task, build_runtime_queue_executor, AsterChatRequest,
};
use crate::commands::browser_runtime_cmd::{
launch_browser_session_with_db, LaunchBrowserSessionRequest,
};
use crate::config::GlobalConfigManagerState;
use crate::database::DbConnection;
use crate::mcp::McpManagerState;
use crate::services::automation_service::AutomationServiceState;
use crate::services::workspace_health_service::ensure_workspace_ready_with_auto_relocate;
use crate::workspace::WorkspaceManager;
use crate::LogState;
use chrono::Utc;
use lime_browser_runtime::CdpSessionState;
use serde_json::{json, Value};
use serde_json::{json, Map, Value};
use tauri::{AppHandle, Manager};
#[derive(Debug)]
@@ -50,8 +58,20 @@ pub async fn execute_job(
prompt,
system_prompt,
web_search,
request_metadata,
content_id,
} => {
execute_agent_turn(job, db, app_handle, prompt, system_prompt, web_search).await
execute_agent_turn(
job,
db,
app_handle,
prompt,
system_prompt,
web_search,
request_metadata,
content_id,
)
.await
}
AutomationPayload::BrowserSession {
profile_id,
@@ -91,11 +111,13 @@ async fn execute_agent_turn(
prompt: String,
system_prompt: Option<String>,
web_search: bool,
request_metadata: Option<Value>,
content_id: Option<String>,
) -> Result<JobExecutionResult, String> {
let app = app_handle
.as_ref()
.ok_or_else(|| "应用句柄不可用,无法执行自动化任务".to_string())?;
let prompt = build_prompt(job, &prompt, system_prompt.as_deref(), web_search);
let prompt = build_prompt(job, &prompt, web_search);
let workspace_manager = WorkspaceManager::new(db.clone());
let workspace = workspace_manager
@@ -115,16 +137,58 @@ async fn execute_agent_turn(
)?;
let agent_state = app
.try_state::<AsterAgentState>()
.try_state::<crate::agent::AsterAgentState>()
.ok_or_else(|| "AsterAgentState 未初始化".to_string())?;
let api_key_provider_service = app
.try_state::<ApiKeyProviderServiceState>()
.ok_or_else(|| "ApiKeyProviderServiceState 未初始化".to_string())?;
let logs = app
.try_state::<LogState>()
.ok_or_else(|| "LogState 未初始化".to_string())?;
let config_manager = app
.try_state::<GlobalConfigManagerState>()
.ok_or_else(|| "GlobalConfigManagerState 未初始化".to_string())?;
let mcp_manager = app
.try_state::<McpManagerState>()
.ok_or_else(|| "McpManagerState 未初始化".to_string())?;
let automation_state = app
.try_state::<AutomationServiceState>()
.ok_or_else(|| "AutomationServiceState 未初始化".to_string())?;
let event_name = format!("automation:agent:{}:{}", job.id, Utc::now().timestamp());
AsterAgentWrapper::send_message(
&agent_state,
db,
app,
prompt,
session_id.clone(),
let runtime_request = AsterChatRequest {
message: prompt,
session_id: session_id.clone(),
event_name,
images: None,
provider_config: None,
provider_preference: None,
model_preference: None,
thinking_enabled: None,
project_id: None,
workspace_id: job.workspace_id.clone(),
web_search: Some(web_search),
search_mode: None,
execution_strategy: None,
auto_continue: None,
system_prompt,
metadata: normalize_agent_turn_request_metadata(request_metadata, content_id.clone()),
turn_id: None,
queue_if_busy: Some(false),
queued_turn_id: None,
};
let queued_task = build_queued_turn_task(runtime_request)?;
crate::agent::runtime_queue_service::submit_runtime_turn(
app.clone(),
agent_state.inner(),
db,
api_key_provider_service.inner(),
logs.inner(),
config_manager.inner(),
mcp_manager.inner(),
automation_state.inner(),
queued_task,
false,
build_runtime_queue_executor(),
)
.await?;
@@ -136,6 +200,7 @@ async fn execute_agent_turn(
"job_name": job.name.clone(),
"workspace_id": job.workspace_id.clone(),
"session_id": session_id.clone(),
"content_id": content_id,
"status": "success",
})),
session_id: Some(session_id),
@@ -184,24 +249,13 @@ async fn execute_browser_session(
})
}
fn build_prompt(
job: &AutomationJobRecord,
prompt: &str,
system_prompt: Option<&str>,
web_search: bool,
) -> String {
fn build_prompt(job: &AutomationJobRecord, prompt: &str, web_search: bool) -> String {
let mut sections = vec![
"你是一个自动化任务执行助手。".to_string(),
format!("任务名称:{}", job.name),
format!("任务描述:{}", job.description.clone().unwrap_or_default()),
format!("工作区 ID:{}", job.workspace_id),
];
if let Some(system_prompt) = system_prompt {
let trimmed = system_prompt.trim();
if !trimmed.is_empty() {
sections.push(format!("附加系统指令:{trimmed}"));
}
}
if web_search {
sections.push("允许按需使用 WebSearch。".to_string());
}
@@ -209,3 +263,89 @@ fn build_prompt(
sections.push(prompt.trim().to_string());
sections.join("\n\n")
}
fn normalize_agent_turn_request_metadata(
request_metadata: Option<Value>,
content_id: Option<String>,
) -> Option<Value> {
let normalized_content_id = content_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string);
if request_metadata.is_none() && normalized_content_id.is_none() {
return None;
}
let mut root = match request_metadata {
Some(Value::Object(object)) => object,
Some(other) => {
let mut object = Map::new();
object.insert("request_metadata".to_string(), other);
object
}
None => Map::new(),
};
if let Some(content_id) = normalized_content_id {
let harness_entry = root
.entry("harness".to_string())
.or_insert_with(|| Value::Object(Map::new()));
if !harness_entry.is_object() {
*harness_entry = Value::Object(Map::new());
}
if let Some(harness) = harness_entry.as_object_mut() {
harness.insert("content_id".to_string(), Value::String(content_id));
}
}
Some(Value::Object(root))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn normalize_agent_turn_request_metadata_should_attach_content_id_to_harness() {
let normalized = normalize_agent_turn_request_metadata(
Some(json!({
"artifact": {
"artifact_mode": "draft",
"artifact_kind": "analysis"
}
})),
Some("content-1".to_string()),
)
.expect("normalized metadata");
assert_eq!(
normalized
.pointer("/harness/content_id")
.and_then(Value::as_str),
Some("content-1")
);
assert_eq!(
normalized
.pointer("/artifact/artifact_kind")
.and_then(Value::as_str),
Some("analysis")
);
}
#[test]
fn normalize_agent_turn_request_metadata_should_create_minimal_harness_when_only_content_id_exists(
) {
let normalized = normalize_agent_turn_request_metadata(None, Some("content-2".to_string()))
.expect("normalized metadata");
assert_eq!(
normalized
.pointer("/harness/content_id")
.and_then(Value::as_str),
Some("content-2")
);
}
}
@@ -74,6 +74,10 @@ pub enum AutomationPayload {
system_prompt: Option<String>,
#[serde(default)]
web_search: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
request_metadata: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
content_id: Option<String>,
},
BrowserSession {
profile_id: String,
@@ -749,10 +753,25 @@ fn validate_job(conn: &Connection, job: &AutomationJobRecord) -> Result<(), Stri
fn validate_payload(payload: &AutomationPayload) -> Result<(), String> {
match payload {
AutomationPayload::AgentTurn { prompt, .. } => {
AutomationPayload::AgentTurn {
prompt,
request_metadata,
content_id,
..
} => {
if prompt.trim().is_empty() {
return Err("自动化任务内容不能为空".to_string());
}
if let Some(content_id) = content_id {
if content_id.trim().is_empty() {
return Err("自动化任务 content_id 不能为空字符串".to_string());
}
}
if let Some(request_metadata) = request_metadata {
if !request_metadata.is_object() {
return Err("自动化任务 request_metadata 必须为对象".to_string());
}
}
}
AutomationPayload::BrowserSession { profile_id, .. } => {
if profile_id.trim().is_empty() {
@@ -1355,4 +1374,40 @@ mod tests {
assert!(context.attempt_id.starts_with("dlv-"));
assert_eq!(context.execution_retry_count, 2);
}
#[test]
fn validate_payload_should_reject_blank_agent_turn_content_id() {
let payload = AutomationPayload::AgentTurn {
prompt: "汇总今日异常".to_string(),
system_prompt: None,
web_search: false,
request_metadata: Some(json!({
"harness": {
"theme": "social-media",
}
})),
content_id: Some(" ".to_string()),
};
assert_eq!(
validate_payload(&payload),
Err("自动化任务 content_id 不能为空字符串".to_string())
);
}
#[test]
fn validate_payload_should_reject_non_object_request_metadata() {
let payload = AutomationPayload::AgentTurn {
prompt: "汇总今日异常".to_string(),
system_prompt: None,
web_search: false,
request_metadata: Some(json!(["invalid"])),
content_id: Some("content-1".to_string()),
};
assert_eq!(
validate_payload(&payload),
Err("自动化任务 request_metadata 必须为对象".to_string())
);
}
}