use super::*;
pub(crate) fn build_turn_input_items(
request: &SendTurnRequest,
staging_root: &Path,
) -> Result<(Vec<Value>, Vec<PathBuf>)> {
let mut input_items = Vec::new();
let mut staged_paths = Vec::new();
if let Some(items) = request.input_items.as_ref() {
for item in items {
match item {
SendTurnInputItem::Text { text } => {
if text.trim().is_empty() {
continue;
}
input_items.push(text_input_item(text));
}
SendTurnInputItem::Image { url } => {
if url.trim().is_empty() {
continue;
}
input_items.push(json!({
"type": "image",
"url": url,
}));
}
SendTurnInputItem::LocalImage { path } => {
let path = validate_staged_image_path(path, staging_root)?;
staged_paths.push(path.clone());
input_items.push(json!({
"type": "localImage",
"path": path.to_string_lossy().to_string(),
}));
}
}
}
}
if input_items.is_empty() && !request.text.trim().is_empty() {
input_items.push(text_input_item(&request.text));
}
if input_items.is_empty() {
bail!("输入内容不能为空");
}
Ok((input_items, staged_paths))
}
fn text_input_item(text: &str) -> Value {
json!({
"type": "text",
"text": text,
"text_elements": [],
})
}
fn validate_staged_image_path(path: &str, staging_root: &Path) -> Result<PathBuf> {
let candidate = PathBuf::from(path);
if !candidate.is_absolute() {
bail!("图片路径必须为绝对路径");
}
if !candidate.starts_with(staging_root) {
bail!("图片路径不属于 bridge staging 目录");
}
let metadata = fs::metadata(&candidate)
.with_context(|| format!("图片暂存文件不存在: {}", candidate.display()))?;
if !metadata.is_file() {
bail!("图片暂存路径无效: {}", candidate.display());
}
Ok(candidate)
}
pub(super) fn infer_image_extension(
file_name: Option<&str>,
mime_type: Option<&str>,
) -> Option<&'static str> {
file_name
.and_then(|value| Path::new(value).extension().and_then(|ext| ext.to_str()))
.and_then(normalize_extension)
.or_else(|| {
mime_type.and_then(|value| match value.trim().to_ascii_lowercase().as_str() {
"image/png" => Some("png"),
"image/jpeg" | "image/jpg" => Some("jpg"),
"image/webp" => Some("webp"),
"image/gif" => Some("gif"),
"image/bmp" => Some("bmp"),
"image/heic" => Some("heic"),
"image/heif" => Some("heif"),
_ => None,
})
})
}
fn normalize_extension(extension: &str) -> Option<&'static str> {
match extension
.trim()
.trim_start_matches('.')
.to_ascii_lowercase()
.as_str()
{
"png" => Some("png"),
"jpg" | "jpeg" => Some("jpg"),
"webp" => Some("webp"),
"gif" => Some("gif"),
"bmp" => Some("bmp"),
"heic" => Some("heic"),
"heif" => Some("heif"),
_ => None,
}
}
pub(super) fn sanitize_file_name(file_name: &str) -> Option<String> {
let file_name = Path::new(file_name)
.file_name()?
.to_string_lossy()
.trim()
.to_string();
(!file_name.is_empty()).then_some(file_name)
}
pub(super) fn normalize_optional_trimmed(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}