mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: update automation workspace and release pipeline
This commit is contained in:
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user