fix(image-generate): save generated images into the caller's workspace
Nightly Build / build (push) Successful in 5m42s
Nightly Build / build (push) Successful in 5m42s
image_generate wrote the file into the server's own data/images/ and handed
that host path to the model. It is a path in nobody's vocabulary: not the
caller's home, not their container. Telegram's send_attachment therefore
resolved it under the user's home and answered "file not found", and
read_file, execute_cmd and the viewer could not reach it either. The web URL
was the only surface that worked, which is why the failure only ever showed
on Telegram -- and why the model there, having no working way to hand the
file over, started inventing send_photo and send_media.
Placement moves to the tool, the one place holding a ToolContext:
- The manager returns bytes (generate_bytes) and no longer knows where an
image goes. It has no UserFs and no session, so it never could have.
- run_with saves through uploads::save_to_home into uploads/{session}/. The
returned path is agent vocabulary, so every consumer resolves it, and that
is the one directory the media inliner is authorized to read from -- a
vision model can be shown the image it just made. execute_async, the
context-free path, now fails loudly rather than writing somewhere nobody
can read; same shape as execute_cmd.
- The extension is sniffed rather than assumed png: it is what decides
whether Telegram sends the picture inline or as an anonymous document, and
providers return jpeg and webp too. The file is named after the prompt, so
it reads as something in the explorer and in Telegram.
The result still carries a url, since the chat renders Markdown images and
 beats naming a file the user then has to open. It points at
/api/file?path=..., which resolves through the caller's own UserFs. The old
/api/images/{id} route is removed: it had no writer left once placement
moved, and it addressed one instance-wide directory behind require_auth
alone, with no notion of who owned the image -- the same shape as the /data
static mount removed before it. That leaves data_root unused, so the manager
no longer knows about the server's filesystem at all.
Docs: the Telegram page explains send_attachment as the channel's equivalent
of show_file_to_user; the ComfyUI page says where a generated image lands and
which of the two handles to use where.
Also introduces CHANGELOG.md and the standing rule for it in CLAUDE.md.
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderValue, StatusCode, header},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
use std::sync::Arc;
|
||||
use skald_core::skald::Skald;
|
||||
|
||||
/// GET /api/images/:task_id
|
||||
///
|
||||
/// Serves a generated image from `data/images/<task_id>.png`.
|
||||
pub async fn get_image(
|
||||
State(skald): State<Arc<Skald>>,
|
||||
Path(task_id): Path<String>,
|
||||
) -> Response {
|
||||
// Reject any path traversal attempts.
|
||||
if task_id.contains('/') || task_id.contains('\\') || task_id.contains("..") {
|
||||
return StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
|
||||
let task_id = task_id.trim_end_matches(".png");
|
||||
let path = skald.image_generator_manager().images_dir().join(format!("{task_id}.png"));
|
||||
|
||||
match fs::read(&path).await {
|
||||
Ok(bytes) => {
|
||||
let mut response = bytes.into_response();
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("image/png"),
|
||||
);
|
||||
response
|
||||
}
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ pub mod guard;
|
||||
pub mod stats;
|
||||
pub mod files;
|
||||
pub mod image_generate_models;
|
||||
pub mod images;
|
||||
pub mod inbox;
|
||||
pub mod llm;
|
||||
pub mod marketplace;
|
||||
@@ -229,8 +228,6 @@ pub fn router() -> Router<Arc<Skald>> {
|
||||
.route("/shared-folders/{id}", patch(shared_folders::update_description).delete(shared_folders::delete))
|
||||
.route("/shared-folders/{id}/members", post(shared_folders::add_member))
|
||||
.route("/shared-folders/{id}/members/{user_id}", delete(shared_folders::remove_member))
|
||||
// Images (generated by image_generate tool)
|
||||
.route("/images/{task_id}", get(images::get_image))
|
||||
// MCP tool-result media (images/audio/files returned by MCP servers)
|
||||
.route("/mcp-media/{file}", get(mcp_media::get_media))
|
||||
// Files
|
||||
|
||||
Reference in New Issue
Block a user