//! generate_image AI 工具声明式注册(调 provider 图像生成端点 + 下载落地)。 //! //! 工具职责:调 provider 的 OpenAI 兼容 `/v1/images/generations` 端点生成图片, //! 自动下载到工作区 output_path。默认 model=sensenova-u1-fast(SenseNova U1 信息图生成), //! 也支持 DALL-E 等 OpenAI 兼容图像端点。 //! //! 风险:High(付费 API 按张计费 + 写磁盘 + 外发网络)。 //! //! 与 download_file 的分工:download_file 是「已知 URL → 落盘」,generate_image 是 //! 「调 provider 生成 + 自动落盘」(封装 provider 凭证 + 端点拼接 + 响应解析 + 图片下载全链路)。 //! 图像生成模型不是 chat 模型,走 /v1/images/generations 非 chat completions, //! 故不能经主对话链路调用,必须用本工具显式触发。 //! //! handler 在 commands/ai/generate_image.rs::execute_generate_image,声明式注册收敛样板。 //! 闭包捕获 db:Arc(选 provider + 解析凭证)+ allowed_dirs:Arc> //! (output_path 白名单校验 + 默认路径锚定)。 //! //! ## 捕获变量说明(declare_tool! 单捕获限制) //! //! declare_tool! 宏只支持单捕获变量(见 crates/df-ai/src/ai_tools_decl.rs),本工具需 db + //! allowed_dirs 两个,故包进 tuple Arc `(Arc, Arc>)` 在闭包内解构。 //! 这是宏单捕获限制的标准绕法(零架构改动:不改宏、不改 process_tool_calls 签名)。 //! //! ## 注册位置说明 //! //! generate_image 涉付费 API(网络)+ 写文件(落盘 output_path),与 download_file 同源 //! (均持 allowed_dirs),归 register_http_tools 同层(register_generate_image_tool 子函数, //! 在 register_http_tools 之后调用)。 use std::sync::Arc; use df_ai::ai_tools::{AiToolRegistry, RiskLevel}; use df_ai::declare_tool; use tokio::sync::RwLock; use crate::state::AllowedDirs; use df_storage::db::Database; /// generate_image 捕获的 (db, allowed_dirs) tuple 类型(declare_tool! 单捕获绕法)。 type GenerateImageCtx = (Arc, Arc>); /// 注册 generate_image 工具到 `$registry`。 /// /// - name/desc/schema 面向 LLM 的工具说明 /// - risk=High(付费 API + 写磁盘 + 外发网络) /// - handler 捕获 (db, allowed_dirs) tuple Arc,解构后转调 generate_image.rs::execute_generate_image /// (provider 选择 + 凭证解析 + 端点拼接 + SSRF 防护下载 + 原子写全在那) pub fn register( registry: &mut AiToolRegistry, db: &Arc, allowed_dirs: &Arc>, ) { // 捕获 tuple:declare_tool! 单捕获限制的绕法。 let ctx: GenerateImageCtx = (db.clone(), allowed_dirs.clone()); // schema:prompt(必填)+ model(默认 sensenova-u1-fast)+ size(可选)+ n(默认 1) // + provider_id(可选)+ output_path(可选)。 let schema = { let mut props = serde_json::Map::new(); props.insert("prompt".into(), serde_json::json!({ "type": "string", "description": "图片描述(中英文均可),越具体生成质量越高。例:'一只戴墨镜的橘猫坐在键盘上,赛博朋克风格,霓虹光'" })); props.insert("model".into(), serde_json::json!({ "type": "string", "description": "图像生成模型名。默认 sensenova-u1-fast(SenseNova U1 信息图生成,走 /v1/images/generations)。也支持 dall-e-3 等 OpenAI 兼容图像模型。不传用默认", "default": "sensenova-u1-fast" })); props.insert("size".into(), serde_json::json!({ "type": "string", "description": "可选图片尺寸字符串,不传则让 provider 用默认。SenseNova U1 常用 2752x1536 / 1536x2752(横/竖海报),DALL-E 常用 1024x1024" })); props.insert("n".into(), serde_json::json!({ "type": "integer", "description": "生成数量(默认 1,clamp [1,4])。多张时取首张 url/b64 落地", "minimum": 1, "maximum": 4, "default": 1 })); props.insert("provider_id".into(), serde_json::json!({ "type": "string", "description": "可选,指定 provider(多 provider 时);不传则按 model 名自动匹配厂商 host(sensenova→SenseNova,dall-e→OpenAI,google/imagen→Google),匹配不到取首个 openai_compat provider" })); props.insert("output_path".into(), serde_json::json!({ "type": "string", "description": "可选落盘路径(相对路径锚定到授权项目根,绝对路径须在授权目录内)。不传则默认 {首个持久授权目录}/generated_images/{model}-{时间戳}.png。父目录不存在自动创建,覆盖既有同名文件。注意:这是图像生成模型(非对话模型),通过本工具调用而非主对话链路" })); serde_json::json!({ "type": "object", "properties": props, "required": ["prompt"], }) }; declare_tool!( registry, ctx: GenerateImageCtx, "generate_image", "调用图像生成模型(默认 SenseNova U1 Fast 信息图生成,也支持 DALL-E 等 OpenAI 兼容图像端点)生成图片并下载到工作区。参数:prompt(图片描述,必填),model(默认 sensenova-u1-fast),size(可选,SenseNova U1 常用 2752x1536/1536x2752,DALL-E 常用 1024x1024),n(默认 1,clamp [1,4]),provider_id(可选,多 provider 时指定,否则按 model 名自动匹配厂商),output_path(可选,默认 generated_images/{model}-{时间戳}.png)。注意:本工具调用图像生成模型(非对话模型),走 /v1/images/generations 端点。返回 {path, url(或 null), model, provider_id, bytes_written, elapsed_ms}。风险:付费 API 按张计费 + 写磁盘", RiskLevel::High, schema: schema, args => { // 解构 tuple 捕获:db 选 provider + 解析凭证,allowed_dirs 解析 output_path 白名单 let (db, allowed_dirs) = ctx; crate::commands::ai::generate_image::execute_generate_image(args, &db, &allowed_dirs).await } ); }