feat(ssa): Implement dual-channel architecture Phase 1-3 (QPER + LLM Agent pipeline)
Completed: - Phase 1: DB schema (execution_mode + ssa_agent_executions), ModeToggle component, Session PATCH API - Phase 2: AgentPlannerService + AgentCoderService (streaming) + CodeRunnerService + R Docker /execute-code endpoint - Phase 3: AgentCodePanel (3-step confirmation UI), SSE event handling (7 agent events), streaming code display - Three-step confirmation pipeline: plan -> user confirm -> stream code -> user confirm -> execute R code -> results - R Docker sandbox /execute-code endpoint with 120s timeout + block_helpers preloaded - E2E dual-channel test script (8 tests) - Updated R engine architecture doc (v1.5) and SSA module status doc (v4.0) Technical details: - AgentCoderService uses LLM streaming (chatStream) for real-time code generation feedback - ReviewerAgent temporarily disabled, prioritizing Plan -> Code -> Execute flow - CodeRunnerService wraps user code with auto data loading (df variable injection) - Frontend handles agent_planning, agent_plan_ready, code_generating, code_generated, code_executing, code_result events - ask_user mechanism used for plan and code confirmation steps Files: 24 files (4 new services, 2 new components, 1 migration, 1 E2E test, 16 modified) Made-with: Cursor
This commit is contained in:
@@ -167,6 +167,123 @@ function(req) {
|
||||
})
|
||||
}
|
||||
|
||||
#* Agent 通道:执行任意 R 代码(沙箱模式)
|
||||
#* @post /api/v1/execute-code
|
||||
#* @serializer unboxedJSON
|
||||
function(req) {
|
||||
tryCatch({
|
||||
input <- jsonlite::fromJSON(req$postBody, simplifyVector = FALSE)
|
||||
|
||||
code <- input$code
|
||||
session_id <- input$session_id
|
||||
timeout_sec <- as.numeric(input$timeout %||% 120)
|
||||
|
||||
if (is.null(code) || nchar(trimws(code)) == 0) {
|
||||
return(list(
|
||||
status = "error",
|
||||
error_code = "E400",
|
||||
message = "Missing 'code' parameter",
|
||||
user_hint = "R 代码不能为空"
|
||||
))
|
||||
}
|
||||
|
||||
# 安全限制:最长 120 秒
|
||||
if (timeout_sec > 120) timeout_sec <- 120
|
||||
|
||||
message(glue::glue("[ExecuteCode] session={session_id}, code_length={nchar(code)}, timeout={timeout_sec}s"))
|
||||
|
||||
# 在隔离环境中执行,预加载 block_helpers 和 data_loader
|
||||
sandbox_env <- new.env(parent = globalenv())
|
||||
|
||||
# 如果有 session_id,尝试预设数据路径变量
|
||||
if (!is.null(session_id) && nchar(session_id) > 0) {
|
||||
sandbox_env$SESSION_ID <- session_id
|
||||
}
|
||||
|
||||
start_time <- proc.time()
|
||||
|
||||
# 捕获输出和结果
|
||||
output_capture <- tryCatch(
|
||||
withTimeout(
|
||||
{
|
||||
# 捕获打印输出
|
||||
captured_output <- utils::capture.output({
|
||||
result <- eval(parse(text = code), envir = sandbox_env)
|
||||
})
|
||||
|
||||
list(
|
||||
result = result,
|
||||
output = captured_output,
|
||||
error = NULL
|
||||
)
|
||||
},
|
||||
timeout = timeout_sec,
|
||||
onTimeout = "error"
|
||||
),
|
||||
error = function(e) {
|
||||
list(
|
||||
result = NULL,
|
||||
output = NULL,
|
||||
error = e$message
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
elapsed_ms <- round((proc.time() - start_time)["elapsed"] * 1000)
|
||||
|
||||
if (!is.null(output_capture$error)) {
|
||||
message(glue::glue("[ExecuteCode] ERROR after {elapsed_ms}ms: {output_capture$error}"))
|
||||
return(list(
|
||||
status = "error",
|
||||
error_code = "E_EXEC",
|
||||
message = output_capture$error,
|
||||
user_hint = paste0("R 代码执行出错 (", elapsed_ms, "ms): ", output_capture$error),
|
||||
duration_ms = elapsed_ms
|
||||
))
|
||||
}
|
||||
|
||||
message(glue::glue("[ExecuteCode] SUCCESS in {elapsed_ms}ms"))
|
||||
|
||||
# 将结果标准化
|
||||
final_result <- output_capture$result
|
||||
|
||||
# 如果结果是 list 且包含 report_blocks,直接返回
|
||||
if (is.list(final_result) && !is.null(final_result$report_blocks)) {
|
||||
return(list(
|
||||
status = "success",
|
||||
result = final_result,
|
||||
console_output = output_capture$output,
|
||||
duration_ms = elapsed_ms
|
||||
))
|
||||
}
|
||||
|
||||
# 否则包装为通用结果
|
||||
return(list(
|
||||
status = "success",
|
||||
result = list(
|
||||
data = final_result,
|
||||
report_blocks = list()
|
||||
),
|
||||
console_output = output_capture$output,
|
||||
duration_ms = elapsed_ms
|
||||
))
|
||||
|
||||
}, error = function(e) {
|
||||
message(glue::glue("[ExecuteCode] FATAL ERROR: {e$message}"))
|
||||
return(map_r_error(e$message))
|
||||
})
|
||||
}
|
||||
|
||||
#' 超时执行包装器
|
||||
#' @param expr 表达式
|
||||
#' @param timeout 超时秒数
|
||||
#' @param onTimeout 超时行为
|
||||
withTimeout <- function(expr, timeout = 120, onTimeout = "error") {
|
||||
setTimeLimit(cpu = timeout, elapsed = timeout, transient = TRUE)
|
||||
on.exit(setTimeLimit(cpu = Inf, elapsed = Inf, transient = FALSE))
|
||||
eval(expr, envir = parent.frame())
|
||||
}
|
||||
|
||||
#* 执行统计工具
|
||||
#* @post /api/v1/skills/<tool_code>
|
||||
#* @param tool_code:str 工具代码(如 ST_T_TEST_IND)
|
||||
|
||||
Reference in New Issue
Block a user