gitdataai/libs/fctool/src/git_tools/blob.rs
ZhenYi c7a8bc0458 refactor(fctool): extract tool modules into standalone fctool crate
Move git_tools, file_tools, and project_tools from libs/service into a
new libs/fctool crate with correct workspace dependencies. Fixes the
rev.len() >= 40 bug in all git tool resolve functions (OID check needs
exact 40-char hex, not just >= 40). Adds 4 new git blob tools
(blob_get, blob_exists, blob_content, blob_create). Fixes parameter
naming inconsistency in repos.rs and adds project_name to list_repos
output. Removes unused excel/pdf/ppt/word file tools.
2026-04-26 23:58:16 +08:00

274 lines
11 KiB
Rust

//! Git blob tools — raw object-level operations on blob OIDs.
use super::ctx::GitToolCtx;
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
use base64::Engine;
use std::collections::HashMap;
async fn git_blob_info_exec(
ctx: GitToolCtx,
args: serde_json::Value,
) -> Result<serde_json::Value, String> {
let p: serde_json::Map<String, serde_json::Value> =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
let domain = ctx.open_repo(project_name, repo_name).await?;
let commit_oid = resolve_oid(&domain, oid)?;
let info = domain.blob_get(&commit_oid).map_err(|e| e.to_string())?;
Ok(serde_json::json!({
"oid": info.oid.to_string(),
"size": info.size,
"is_binary": info.is_binary,
}))
}
async fn git_blob_exists_exec(
ctx: GitToolCtx,
args: serde_json::Value,
) -> Result<serde_json::Value, String> {
let p: serde_json::Map<String, serde_json::Value> =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
let domain = ctx.open_repo(project_name, repo_name).await?;
let commit_oid = resolve_oid(&domain, oid)?;
let exists = domain.blob_exists(&commit_oid);
Ok(serde_json::json!({ "oid": commit_oid.to_string(), "exists": exists }))
}
async fn git_blob_content_exec(
ctx: GitToolCtx,
args: serde_json::Value,
) -> Result<serde_json::Value, String> {
let p: serde_json::Map<String, serde_json::Value> =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
let max_size = p.get("max_size").and_then(|v| v.as_u64()).unwrap_or(1_048_576) as usize; // 1MB default
let domain = ctx.open_repo(project_name, repo_name).await?;
let commit_oid = resolve_oid(&domain, oid)?;
let blob = domain.blob_content(&commit_oid).map_err(|e| e.to_string())?;
if blob.size > max_size {
return Err(format!(
"blob too large ({} bytes), max {} bytes. Use a smaller max_size or retrieve the raw OID.",
blob.size, max_size
));
}
let (content, is_binary) = if blob.is_binary {
(base64::engine::general_purpose::STANDARD.encode(&blob.content), true)
} else {
(String::from_utf8_lossy(&blob.content).to_string(), false)
};
Ok(serde_json::json!({
"oid": blob.oid.to_string(),
"size": blob.size,
"is_binary": is_binary,
"content": content,
}))
}
async fn git_blob_create_exec(
ctx: GitToolCtx,
args: serde_json::Value,
) -> Result<serde_json::Value, String> {
let p: serde_json::Map<String, serde_json::Value> =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
let content = p.get("content").and_then(|v| v.as_str()).ok_or("missing content")?;
let encoding = p.get("encoding").and_then(|v| v.as_str()).unwrap_or("utf-8");
let data = match encoding {
"base64" => base64::engine::general_purpose::STANDARD
.decode(content)
.map_err(|e| format!("invalid base64: {}", e))?,
"utf-8" => content.as_bytes().to_vec(),
other => return Err(format!("unsupported encoding '{}'. Use 'utf-8' or 'base64'.", other)),
};
let domain = ctx.open_repo(project_name, repo_name).await?;
let oid = domain.blob_create(&data).map_err(|e| e.to_string())?;
let info = domain.blob_get(&oid).map_err(|e| e.to_string())?;
Ok(serde_json::json!({
"oid": info.oid.to_string(),
"size": info.size,
"is_binary": info.is_binary,
}))
}
fn resolve_oid(
domain: &git::GitDomain,
rev: &str,
) -> Result<git::commit::types::CommitOid, String> {
if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
Ok(git::commit::types::CommitOid::new(rev))
} else {
domain.commit_get_prefix(rev).map_err(|e| e.to_string()).map(|m| m.oid)
}
}
pub fn register_git_tools(registry: &mut ToolRegistry) {
// git_blob_info
let p = HashMap::from([
("project_name".into(), ToolParam {
name: "project_name".into(), param_type: "string".into(),
description: Some("Project name (slug)".into()),
required: true, properties: None, items: None,
}),
("repo_name".into(), ToolParam {
name: "repo_name".into(), param_type: "string".into(),
description: Some("Repository name".into()),
required: true, properties: None, items: None,
}),
("oid".into(), ToolParam {
name: "oid".into(), param_type: "string".into(),
description: Some("Blob OID (full 40-char hex or short prefix)".into()),
required: true, properties: None, items: None,
}),
]);
let schema = ToolSchema {
schema_type: "object".into(),
properties: Some(p),
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
};
registry.register(
ToolDefinition::new("git_blob_info")
.description("Get metadata about a git blob by its OID. Returns size and whether the blob is binary.")
.parameters(schema),
ToolHandler::new(|ctx, args| {
let gctx = GitToolCtx::new(ctx);
Box::pin(async move {
git_blob_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
})
}),
);
// git_blob_exists
let p = HashMap::from([
("project_name".into(), ToolParam {
name: "project_name".into(), param_type: "string".into(),
description: Some("Project name (slug)".into()),
required: true, properties: None, items: None,
}),
("repo_name".into(), ToolParam {
name: "repo_name".into(), param_type: "string".into(),
description: Some("Repository name".into()),
required: true, properties: None, items: None,
}),
("oid".into(), ToolParam {
name: "oid".into(), param_type: "string".into(),
description: Some("Blob OID to check".into()),
required: true, properties: None, items: None,
}),
]);
let schema = ToolSchema {
schema_type: "object".into(),
properties: Some(p),
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
};
registry.register(
ToolDefinition::new("git_blob_exists")
.description("Check whether a git blob exists in the repository by its OID.")
.parameters(schema),
ToolHandler::new(|ctx, args| {
let gctx = GitToolCtx::new(ctx);
Box::pin(async move {
git_blob_exists_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
})
}),
);
// git_blob_content
let p = HashMap::from([
("project_name".into(), ToolParam {
name: "project_name".into(), param_type: "string".into(),
description: Some("Project name (slug)".into()),
required: true, properties: None, items: None,
}),
("repo_name".into(), ToolParam {
name: "repo_name".into(), param_type: "string".into(),
description: Some("Repository name".into()),
required: true, properties: None, items: None,
}),
("oid".into(), ToolParam {
name: "oid".into(), param_type: "string".into(),
description: Some("Blob OID to retrieve content for".into()),
required: true, properties: None, items: None,
}),
("max_size".into(), ToolParam {
name: "max_size".into(), param_type: "integer".into(),
description: Some("Maximum blob size in bytes (default: 1MB)".into()),
required: false, properties: None, items: None,
}),
]);
let schema = ToolSchema {
schema_type: "object".into(),
properties: Some(p),
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
};
registry.register(
ToolDefinition::new("git_blob_content")
.description("Retrieve the raw content of a git blob by its OID. Binary content is base64-encoded. Limits to 1MB by default.")
.parameters(schema),
ToolHandler::new(|ctx, args| {
let gctx = GitToolCtx::new(ctx);
Box::pin(async move {
git_blob_content_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
})
}),
);
// git_blob_create
let p = HashMap::from([
("project_name".into(), ToolParam {
name: "project_name".into(), param_type: "string".into(),
description: Some("Project name (slug)".into()),
required: true, properties: None, items: None,
}),
("repo_name".into(), ToolParam {
name: "repo_name".into(), param_type: "string".into(),
description: Some("Repository name".into()),
required: true, properties: None, items: None,
}),
("content".into(), ToolParam {
name: "content".into(), param_type: "string".into(),
description: Some("Blob content (utf-8 string or base64-encoded bytes)".into()),
required: true, properties: None, items: None,
}),
("encoding".into(), ToolParam {
name: "encoding".into(), param_type: "string".into(),
description: Some("Encoding of content: 'utf-8' (default) or 'base64'".into()),
required: false, properties: None, items: None,
}),
]);
let schema = ToolSchema {
schema_type: "object".into(),
properties: Some(p),
required: Some(vec!["project_name".into(), "repo_name".into(), "content".into()]),
};
registry.register(
ToolDefinition::new("git_blob_create")
.description("Create a new git blob in the repository. Writes the raw content to the object database and returns the new blob OID. Supports both utf-8 text and base64-encoded binary content.")
.parameters(schema),
ToolHandler::new(|ctx, args| {
let gctx = GitToolCtx::new(ctx);
Box::pin(async move {
git_blob_create_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
})
}),
);
}