add run_shell and run_python tools, deploy-suite target
This commit is contained in:
18
Makefile
18
Makefile
@@ -40,6 +40,24 @@ deploy: test build noc.service
|
|||||||
systemctl --user enable --now noc
|
systemctl --user enable --now noc
|
||||||
systemctl --user restart 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
|
deploy-hera: build
|
||||||
ssh $(HERA) 'mkdir -p ~/bin ~/$(HERA_DIR) ~/.config/systemd/user && systemctl --user stop noc 2>/dev/null || true'
|
ssh $(HERA) 'mkdir -p ~/bin ~/$(HERA_DIR) ~/.config/systemd/user && systemctl --user stop noc 2>/dev/null || true'
|
||||||
scp target/release/noc $(HERA):~/bin/
|
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!({
|
serde_json::json!({
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@@ -360,6 +395,118 @@ pub async fn execute_tool(
|
|||||||
Err(e) => format!("Error: {e}"),
|
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" => {
|
"call_gitea_api" => {
|
||||||
let method = args["method"].as_str().unwrap_or("GET").to_uppercase();
|
let method = args["method"].as_str().unwrap_or("GET").to_uppercase();
|
||||||
let path = args["path"].as_str().unwrap_or("").trim_start_matches('/');
|
let path = args["path"].as_str().unwrap_or("").trim_start_matches('/');
|
||||||
|
|||||||
Reference in New Issue
Block a user