add run_shell and run_python tools, deploy-suite target

This commit is contained in:
Fam Zheng
2026-04-10 21:47:14 +01:00
parent 8a5b65f128
commit c0e12798ee
2 changed files with 165 additions and 0 deletions

View File

@@ -40,6 +40,24 @@ deploy: test build noc.service
systemctl --user enable --now noc
systemctl --user restart noc
SUITE := noc
SUITE_DIR := noc
GITEA_VERSION := 1.23
deploy-suite: build
ssh $(SUITE) 'mkdir -p ~/bin /data/noc/tools ~/.config/systemd/user && systemctl --user stop noc 2>/dev/null || true'
scp target/release/noc $(SUITE):~/bin/
scp config.suite.yaml $(SUITE):/data/noc/config.yaml
scp noc.service.in $(SUITE):/data/noc/
scp -r tools/ $(SUITE):/data/noc/tools/
ssh $(SUITE) 'bash -lc "\
cd /data/noc \
&& sed -e \"s|@REPO@|/data/noc|g\" -e \"s|@PATH@|\$$PATH|g\" noc.service.in > ~/.config/systemd/user/noc.service \
&& systemctl --user daemon-reload \
&& systemctl --user enable --now noc \
&& systemctl --user restart noc \
&& systemctl --user status noc"'
deploy-hera: build
ssh $(HERA) 'mkdir -p ~/bin ~/$(HERA_DIR) ~/.config/systemd/user && systemctl --user stop noc 2>/dev/null || true'
scp target/release/noc $(HERA):~/bin/

View File

@@ -198,6 +198,41 @@ pub fn discover_tools() -> serde_json::Value {
}
}
}),
serde_json::json!({
"type": "function",
"function": {
"name": "run_shell",
"description": "在服务器上执行 shell 命令。可执行任意 bash 命令,支持管道和重定向。超时 60 秒。",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "要执行的 shell 命令"},
"timeout": {"type": "integer", "description": "超时秒数(默认 60最大 300"}
},
"required": ["command"]
}
}
}),
serde_json::json!({
"type": "function",
"function": {
"name": "run_python",
"description": "用 uv run 执行 Python 代码。支持 inline dependencies通过 deps 参数自动安装),无需手动管理虚拟环境。超时 120 秒。",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "要执行的 Python 代码"},
"deps": {
"type": "array",
"items": {"type": "string"},
"description": "依赖包列表(如 [\"requests\", \"pandas\"]),会自动通过 uv 安装"
},
"timeout": {"type": "integer", "description": "超时秒数(默认 120最大 300"}
},
"required": ["code"]
}
}
}),
serde_json::json!({
"type": "function",
"function": {
@@ -360,6 +395,118 @@ pub async fn execute_tool(
Err(e) => format!("Error: {e}"),
}
}
"run_shell" => {
let cmd = args["command"].as_str().unwrap_or("");
if cmd.is_empty() {
return "Error: command is required".to_string();
}
let timeout_secs = args["timeout"].as_u64().unwrap_or(60).min(300);
info!(cmd = %cmd, "run_shell");
let result = tokio::time::timeout(
std::time::Duration::from_secs(timeout_secs),
Command::new("bash")
.args(["-c", cmd])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output(),
)
.await;
match result {
Ok(Ok(out)) => {
let mut s = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.is_empty() {
if !s.is_empty() {
s.push_str("\n[stderr]\n");
}
s.push_str(&stderr);
}
let exit = out.status.code().unwrap_or(-1);
if s.len() > 8000 {
s = format!("{}...(truncated)", &s[..8000]);
}
if exit != 0 {
s.push_str(&format!("\n[exit={exit}]"));
}
if s.is_empty() {
format!("(exit={exit})")
} else {
s
}
}
Ok(Err(e)) => format!("exec error: {e}"),
Err(_) => format!("timeout after {timeout_secs}s"),
}
}
"run_python" => {
let code = args["code"].as_str().unwrap_or("");
if code.is_empty() {
return "Error: code is required".to_string();
}
let timeout_secs = args["timeout"].as_u64().unwrap_or(120).min(300);
let deps = args["deps"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
})
.unwrap_or_default();
// Build uv run command with inline script metadata for deps
let script = if deps.is_empty() {
code.to_string()
} else {
let dep_lines: String = deps.iter().map(|d| format!("# \"{d}\",\n")).collect();
format!(
"# /// script\n# [project]\n# dependencies = [\n{dep_lines}# ]\n# ///\n{code}"
)
};
// Write script to temp file
let tmp = format!("/tmp/noc_py_{}.py", std::process::id());
if let Err(e) = std::fs::write(&tmp, &script) {
return format!("Failed to write temp script: {e}");
}
info!(deps = ?deps, "run_python");
let result = tokio::time::timeout(
std::time::Duration::from_secs(timeout_secs),
Command::new("uv")
.args(["run", &tmp])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output(),
)
.await;
let _ = std::fs::remove_file(&tmp);
match result {
Ok(Ok(out)) => {
let mut s = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.is_empty() {
if !s.is_empty() {
s.push_str("\n[stderr]\n");
}
s.push_str(&stderr);
}
let exit = out.status.code().unwrap_or(-1);
if s.len() > 8000 {
s = format!("{}...(truncated)", &s[..8000]);
}
if exit != 0 {
s.push_str(&format!("\n[exit={exit}]"));
}
if s.is_empty() {
format!("(exit={exit})")
} else {
s
}
}
Ok(Err(e)) => format!("exec error: {e} (is uv installed?)"),
Err(_) => format!("timeout after {timeout_secs}s"),
}
}
"call_gitea_api" => {
let method = args["method"].as_str().unwrap_or("GET").to_uppercase();
let path = args["path"].as_str().unwrap_or("").trim_start_matches('/');