Refactor syscall modules to use async/await patterns

- Updated `DisplaySyscall` to use `tokio::io::AsyncWriteExt` for asynchronous text output.
- Refactored `FsSyscall` to read files asynchronously with offset and length handling.
- Modified `MemorySyscall` to use `tokio::task::spawn_blocking` for database operations, allowing async access to SQLite.
- Enhanced `NetworkSyscall` to utilize `reqwest` for HTTP requests, replacing the previous `curl` command execution.
- Improved `ProcessSyscall` to manage subprocesses with async tasks for stdin, stdout, and stderr handling.
- Updated `TimerSyscall` to simplify timer management.
- Adjusted plugin implementations for better async support and error handling.
- Added `tokio-stream` and `tracing-subscriber` dependencies to `Cargo.toml` for enhanced async stream handling and logging.
This commit is contained in:
2026-06-10 01:26:46 +08:00
parent f7919eca40
commit d533d0a30e
21 changed files with 1950 additions and 436 deletions

View File

@@ -14,6 +14,8 @@ anyhow = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tonic = { workspace = true }
futures-core = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }

View File

@@ -12,7 +12,7 @@ use tonic::{transport::Server, Request, Response, Status};
const PLUGIN_ID: &str = "claudecode";
const DEFAULT_PORT: u16 = 50053;
#[derive(Debug, Deserialize)]
#[derive(Debug, Default, Deserialize)]
struct CallArgs {
#[serde(default)]
prompt: String,
@@ -32,26 +32,22 @@ fn find_claude_bin() -> Option<String> {
return Some(bin);
}
let candidates = vec![
"claude",
"claude.cmd",
&std::env::var("APPDATA")
.map(|p| format!("{}\\npm\\claude.cmd", p))
.unwrap_or_default(),
];
let appdata_claude = std::env::var("APPDATA")
.ok()
.map(|p| format!("{}\\npm\\claude.cmd", p));
let mut candidates = vec!["claude".to_string(), "claude.cmd".to_string()];
if let Some(path) = appdata_claude {
candidates.push(path);
}
for candidate in candidates {
if std::process::Command::new(candidate)
candidates.into_iter().find(|candidate| {
std::process::Command::new(candidate)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok()
{
return Some(candidate.to_string());
}
}
None
})
}
async fn call_claude(
@@ -97,9 +93,12 @@ async fn call_claude(
drop(stdin);
}
let output = tokio::time::timeout(std::time::Duration::from_secs(300), child.wait_with_output())
.await
.context("claude timeout (300s)")??;
let output = tokio::time::timeout(
std::time::Duration::from_secs(300),
child.wait_with_output(),
)
.await
.context("claude timeout (300s)")??;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -148,8 +147,10 @@ impl ClaudeCodePlugin {
.await
}
"review" => {
let review_prompt =
format!("Review the following code or changes. Be concise and actionable:\n\n{}", prompt);
let review_prompt = format!(
"Review the following code or changes. Be concise and actionable:\n\n{}",
prompt
);
call_claude(
&self.claude_bin,
&review_prompt,
@@ -184,7 +185,10 @@ impl ClaudeCodePlugin {
#[tonic::async_trait]
impl PluginBridge for ClaudeCodePlugin {
async fn register(&self, _req: Request<PluginInfo>) -> Result<Response<RegisterResponse>, Status> {
async fn register(
&self,
_req: Request<PluginInfo>,
) -> Result<Response<RegisterResponse>, Status> {
Err(Status::unimplemented("not a bridge"))
}
@@ -195,13 +199,19 @@ impl PluginBridge for ClaudeCodePlugin {
Ok(Response::new(response))
}
type StreamCallStream = futures_core::stream::Empty<Result<CallResponse, Status>>;
type StreamCallStream = tokio_stream::Empty<Result<CallResponse, Status>>;
async fn stream_call(&self, _req: Request<CallRequest>) -> Result<Response<Self::StreamCallStream>, Status> {
async fn stream_call(
&self,
_req: Request<CallRequest>,
) -> Result<Response<Self::StreamCallStream>, Status> {
Err(Status::unimplemented("stream_call not supported"))
}
async fn heartbeat(&self, _req: Request<HeartbeatRequest>) -> Result<Response<HeartbeatResponse>, Status> {
async fn heartbeat(
&self,
_req: Request<HeartbeatRequest>,
) -> Result<Response<HeartbeatResponse>, Status> {
Ok(Response::new(HeartbeatResponse { ok: true }))
}
}
@@ -210,7 +220,8 @@ impl PluginBridge for ClaudeCodePlugin {
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let claude_bin = find_claude_bin().context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?;
let claude_bin = find_claude_bin()
.context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?;
tracing::info!("Found Claude Code CLI: {}", claude_bin);
let endpoint = agentsd_plugin_sdk::plugin_endpoint_from_env(DEFAULT_PORT);
@@ -224,21 +235,42 @@ async fn main() -> Result<()> {
"Claude Code",
env!("CARGO_PKG_VERSION"),
"standard",
&["process.spawn", "display.text", "fs.read", "fs.write", "network.request"],
&[
"process.spawn",
"display.text",
"fs.read",
"fs.write",
"network.request",
],
&["hermes"],
vec![
export("chat", "Send prompt to Claude Code, get text response (no tools)"),
export("code", "Generate or edit code with Claude Code (Bash/Read/Edit/Write)"),
export(
"chat",
"Send prompt to Claude Code, get text response (no tools)",
),
export(
"code",
"Generate or edit code with Claude Code (Bash/Read/Edit/Write)",
),
export("review", "Review code changes (Read/Glob/Grep only)"),
export("ask", "Ask about the codebase (Read/Glob/Grep, max 3 turns)"),
export("run", "Full Claude Code access (all tools, unlimited turns)"),
export(
"ask",
"Ask about the codebase (Read/Glob/Grep, max 3 turns)",
),
export(
"run",
"Full Claude Code access (all tools, unlimited turns)",
),
],
&endpoint,
),
)
.await?;
tracing::info!("Registered with agentsd: session={}", register_resp.session_id);
tracing::info!(
"Registered with agentsd: session={}",
register_resp.session_id
);
let plugin = ClaudeCodePlugin::new(claude_bin);
let addr = listen_addr.parse()?;