add run_shell and run_python tools, deploy-suite target
This commit is contained in:
parent
8a5b65f128
commit
c0e12798ee
18
Makefile
18
Makefile
@ -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/
|
||||
|
||||
147
src/tools.rs
147
src/tools.rs
@ -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('/');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user