From 0dbdc647ec4e3cf6d671b85fa4f1d358cb2dfce7 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Tue, 2 Jun 2026 20:04:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=94=9F=E6=88=90=E6=8A=80=E8=83=BD=20image-gen=EF=BC=8C?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=20Qwen-Image=20=E7=94=9F=E6=88=90=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E8=87=B3=E5=86=85=E7=BD=AE=20Skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REFACTOR.md | 4 +- .../yuxi/agents/skills/buildin/__init__.py | 7 ++ .../agents/skills/buildin/image-gen/SKILL.md | 82 +++++++++++++++++++ .../yuxi/agents/toolkits/buildin/__init__.py | 3 +- .../yuxi/agents/toolkits/buildin/tools.py | 61 -------------- .../test/unit/services/test_skill_service.py | 10 +++ docs/agents/tools-system.md | 4 +- docs/develop-guides/roadmap.md | 1 + .../ToolCallingResult/tools/ImageTool.vue | 2 +- 9 files changed, 108 insertions(+), 66 deletions(-) create mode 100644 backend/package/yuxi/agents/skills/buildin/image-gen/SKILL.md diff --git a/REFACTOR.md b/REFACTOR.md index 2deb4739..350b1caf 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -48,4 +48,6 @@ - [x] parser 从plugins 移动到 knowledge 里面,guard 移动到services 里面 - [x] neo4j 相关的服务,可以移动到 storage 里面 - [ ] 点开对话的时候要能够自动定位到尾部,而不是最开始。 -- [ ] 将 Qwen-Image 修改为 Skill,暴露一个工具,叫做上传图片到 MINIO,这样生成图片的时候就可以调用别的接口生成图片,并可以上传到 MINIO 上面。也就是将原本生成图片的逻辑移动到 Skill 里面来实现,这样就可以支持多种生成图片的模型了。接入多种的接口 +- [x] 将 Qwen-Image 修改为 Skill +- [ ] 现在输入区域对于不同 mention 的渲染的 ICON 和 human-message 里面的渲染的 ICON; 居然不一样?这个是什么原因,综合并统一一下,使用最优方案。 +- [ ] 现在的消息突然居然 diff --git a/backend/package/yuxi/agents/skills/buildin/__init__.py b/backend/package/yuxi/agents/skills/buildin/__init__.py index b32ff90b..df3e75f1 100644 --- a/backend/package/yuxi/agents/skills/buildin/__init__.py +++ b/backend/package/yuxi/agents/skills/buildin/__init__.py @@ -21,6 +21,13 @@ class BuiltinSkillSpec: _SKILLS_ROOT = Path(__file__).resolve().parent BUILTIN_SKILLS: list[BuiltinSkillSpec] = [ + BuiltinSkillSpec( + slug="image-gen", + source_dir=_SKILLS_ROOT / "image-gen", + description="在 Agent 沙盒中生成图片并保存到 outputs,默认支持 Qwen-Image,也可接入其它图片生成接口。", + version="2026.06.02", + tool_dependencies=("present_artifacts",), + ), BuiltinSkillSpec( slug="deep-reporter", source_dir=_SKILLS_ROOT / "deep-reporter", diff --git a/backend/package/yuxi/agents/skills/buildin/image-gen/SKILL.md b/backend/package/yuxi/agents/skills/buildin/image-gen/SKILL.md new file mode 100644 index 00000000..2f26b624 --- /dev/null +++ b/backend/package/yuxi/agents/skills/buildin/image-gen/SKILL.md @@ -0,0 +1,82 @@ +--- +name: image-gen +description: "在 Agent 沙盒中生成图片并保存到 outputs。当用户要求生成图片、海报、插画、文生图,或指定 Qwen-Image、其它兼容图片生成接口时使用此技能。" +--- + +# 图片生成技能 + +当用户要求生成图片、海报、插画、文生图,或明确提到 Qwen-Image 时,使用此技能组织图片生成流程。 + +## 默认生成接口 + +默认使用 SiliconFlow 的 Qwen-Image 接口: + +- Endpoint: `POST https://api.siliconflow.cn/v1/images/generations` +- Model: `Qwen/Qwen-Image` +- 默认参数: + - `negative_prompt`: `""` + - `num_inference_steps`: `20` + - `guidance_scale`: `7.5` + +调用外部接口时,必须在 Agent 沙盒执行环境中读取 `SILICONFLOW_API_KEY`。不要依赖后端进程环境变量。 + +## 操作流程 + +1. 明确用户要生成的图片内容、风格、尺寸或约束;信息不足但不影响生成时,使用合理默认值,不要反复追问。 +2. 使用可用的执行工具在沙盒中运行脚本,调用图片生成接口,传入用户需求整理后的 `prompt`,并按需传入 `negative_prompt`、`num_inference_steps`、`guidance_scale`。 +3. 从生成接口响应中读取图片地址,默认路径为 `images[0].url`。 +4. 在同一个沙盒脚本中用 `Authorization: Bearer $SILICONFLOW_API_KEY` 下载该图片地址;如果接口直接返回 base64,则直接解码保存。 +5. 将最终图片保存到 `/home/gem/user-data/outputs/` 下,例如 `/home/gem/user-data/outputs/generated-image.png`。 +6. 调用 `present_artifacts`,传入保存后的 outputs 虚拟路径,让前端展示图片产物。 +7. 最终回复简要说明图片已生成,不要把外部临时 URL 当作最终结果展示。 + +## 脚本示例 + +可根据用户需求调整 prompt 和输出文件名: + +```python +import os +import requests +from pathlib import Path + +api_key = os.environ["SILICONFLOW_API_KEY"] +prompt = "根据用户需求整理后的图片提示词" + +response = requests.post( + "https://api.siliconflow.cn/v1/images/generations", + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json={ + "model": "Qwen/Qwen-Image", + "prompt": prompt, + "negative_prompt": "", + "num_inference_steps": 20, + "guidance_scale": 7.5, + }, + timeout=120, +) +response.raise_for_status() +image_url = response.json()["images"][0]["url"] + +image_response = requests.get( + image_url, + headers={"Authorization": f"Bearer {api_key}"}, + timeout=120, +) +image_response.raise_for_status() + +output_path = Path("/home/gem/user-data/outputs/generated-image.png") +output_path.parent.mkdir(parents=True, exist_ok=True) +output_path.write_bytes(image_response.content) +print(output_path.as_posix()) +``` + +## 多模型扩展 + +如果用户指定其它图片生成模型或兼容接口,可以按该接口的协议先生成图片。只要最终拿到图片 bytes 或 base64,就保存到 `/home/gem/user-data/outputs/`,再调用 `present_artifacts` 展示。 + +## 关键约束 + +- 不要把外部生成接口返回的临时 URL 当作最终结果直接展示给用户。 +- 不要调用后端 MinIO 上传工具;图片生成和下载都应在沙盒内完成。 +- 如果 `SILICONFLOW_API_KEY` 缺失,应明确提示用户需要在 Agent 沙盒环境变量中配置。 +- 保存到 outputs 后必须调用 `present_artifacts`,否则前端不会自动展示生成图片。 diff --git a/backend/package/yuxi/agents/toolkits/buildin/__init__.py b/backend/package/yuxi/agents/toolkits/buildin/__init__.py index fb75ee43..77d077f6 100644 --- a/backend/package/yuxi/agents/toolkits/buildin/__init__.py +++ b/backend/package/yuxi/agents/toolkits/buildin/__init__.py @@ -1,10 +1,9 @@ # buildin 工具包 # from .install_skill import install_skill -from .tools import ask_user_question, present_artifacts, text_to_img_qwen_image +from .tools import ask_user_question, present_artifacts __all__ = [ "ask_user_question", # "install_skill", "present_artifacts", - "text_to_img_qwen_image", ] diff --git a/backend/package/yuxi/agents/toolkits/buildin/tools.py b/backend/package/yuxi/agents/toolkits/buildin/tools.py index 087c1bb2..cc55f2b6 100644 --- a/backend/package/yuxi/agents/toolkits/buildin/tools.py +++ b/backend/package/yuxi/agents/toolkits/buildin/tools.py @@ -1,9 +1,7 @@ import os -import uuid from pathlib import Path from typing import Annotated -import requests from langchain.tools import InjectedToolCallId from langchain_core.messages import ToolMessage from langgraph.prebuilt.tool_node import ToolRuntime @@ -11,7 +9,6 @@ from langgraph.types import Command, interrupt from pydantic import BaseModel, Field from yuxi.agents.toolkits.registry import ToolExtraMetadata, _all_tool_instances, _extra_registry, tool -from yuxi.storage.minio import aupload_file_to_minio from yuxi.utils import logger from yuxi.utils.paths import VIRTUAL_PATH_OUTPUTS from yuxi.utils.question_utils import normalize_questions @@ -19,15 +16,6 @@ from yuxi.utils.question_utils import normalize_questions # Lazy initialization for TavilySearch (only when API key is available) _tavily_search_instance = None -QWEN_IMAGE_CONFIG_GUIDE = """ -使用前需要先配置硅基流动的图片生成访问凭证。 - -请在后端运行环境中配置环境变量: -- `SILICONFLOW_API_KEY`:用于调用 SiliconFlow 的图片生成接口 - -配置完成后即可使用该工具生成图片。 -""".strip() - def _create_tavily_search(): """Create and register TavilySearch tool with metadata.""" @@ -216,52 +204,3 @@ def ask_user_question( "questions": normalized_questions, "answer": answer, } - - -@tool( - category="buildin", - tags=["图片", "生成"], - display_name="Qwen-Image", - config_guide=QWEN_IMAGE_CONFIG_GUIDE, -) -async def text_to_img_qwen_image( - prompt: Annotated[str, "用于生成图片的文本描述"], - negative_prompt: Annotated[str, "负面提示词,用于指定不想出现在图片中的元素"] = "", - num_inference_steps: Annotated[int, "推理步数,范围1-100"] = 20, - guidance_scale: Annotated[float, "引导强度,控制图片与提示词的匹配程度"] = 7.5, - uid: Annotated[str, "UID,用于图片归档路径"] = "unknown", -) -> str: - """使用 Qwen-Image 模型生成图片,返回图片的URL,需要注意的是,生成结果不会默认展示,需要将返回的URL进行展示处理。""" - url = "https://api.siliconflow.cn/v1/images/generations" - - payload = { - "model": "Qwen/Qwen-Image", - "prompt": prompt, - "negative_prompt": negative_prompt, - "num_inference_steps": num_inference_steps, - "guidance_scale": guidance_scale, - } - headers = {"Authorization": f"Bearer {os.getenv('SILICONFLOW_API_KEY')}", "Content-Type": "application/json"} - - try: - response = requests.post(url, json=payload, headers=headers) - response_json = response.json() - except Exception as e: - logger.error(f"Failed to generate image with: {e}") - raise ValueError(f"Image generation failed: {e}") - - try: - image_url = response_json["images"][0]["url"] - except (KeyError, IndexError, TypeError) as e: - logger.error(f"Failed to parse image URL from response: {e}, {response_json=}") - raise ValueError(f"Image URL extraction failed: {e}") - - # Upload to MinIO - response = requests.get(image_url) - file_data = response.content - - safe_uid = str(uid or "unknown").replace("/", "_").replace("\\", "_") - file_name = f"user/{safe_uid}/generated-images/{uuid.uuid4()}.jpg" - image_url = await aupload_file_to_minio(bucket_name="public", file_name=file_name, data=file_data) - logger.info(f"Image uploaded. URL: {image_url}") - return image_url diff --git a/backend/test/unit/services/test_skill_service.py b/backend/test/unit/services/test_skill_service.py index bbcc3df0..65944be3 100644 --- a/backend/test/unit/services/test_skill_service.py +++ b/backend/test/unit/services/test_skill_service.py @@ -38,6 +38,16 @@ def test_parse_skill_markdown_requires_frontmatter(): svc._parse_skill_markdown("# missing") +def test_image_gen_builtin_skill_spec(): + specs = {spec["slug"]: spec for spec in svc.list_builtin_skill_specs()} + + assert "image-gen" in specs + image_gen = specs["image-gen"] + assert image_gen["name"] == "image-gen" + assert image_gen["tool_dependencies"] == ["present_artifacts"] + assert (image_gen["source_dir"] / "SKILL.md").exists() + + def test_is_valid_skill_slug(): # Test valid slugs assert svc.is_valid_skill_slug("demo-skill") is True diff --git a/docs/agents/tools-system.md b/docs/agents/tools-system.md index ed01df9a..db3c03b6 100644 --- a/docs/agents/tools-system.md +++ b/docs/agents/tools-system.md @@ -41,9 +41,11 @@ from yuxi.agents.toolkits import buildin, mysql # 触发 @tool 装饰器执行 |------|------| | `calculator` | 计算器,支持加减乘除 | | `ask_user_question` | 向用户发起交互式提问 | -| `text_to_img_qwen_image` | 使用 Qwen-Image 生成图片 | +| `present_artifacts` | 展示 Agent 沙盒 outputs 目录下的产物文件 | | `tavily_search` | Tavily 网页搜索(需配置 `TAVILY_API_KEY`) | +Qwen-Image 生成能力已迁移为内置 Skill `image-gen`。模型调用与图片下载在 Agent 沙盒中完成,生成后的图片保存到 `/home/gem/user-data/outputs/`,再通过 `present_artifacts` 展示。 + ### MySQL 工具 (mysql) | 工具 | 说明 | diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index 1bdf8462..1e1acf41 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -38,6 +38,7 @@ ### 0.7.0 开发记录 +- 图片生成能力迁移为 Skill:Qwen-Image 从内置 Python 生成工具迁移到内置 Skill `image-gen`,模型调用与图片下载在 Agent 沙盒中完成,生成结果保存到 outputs 并通过 `present_artifacts` 展示,为多图片生成模型接入复用同一产物展示链路。 - 降低知识库路由与工具模块复杂度:示例问题生成迁移到知识库 utils,文件上传统一 100 MB 限制,URL 预处理入库路径与旧 `content_type=url` 行为收敛,并修复 uid、导出 MIME 与异常透传等路由问题。 - 重构智能体配置语义:用户可见的 `AgentConfig` 收敛为数据库持久化的一级 `Agent`,内置 Python Agent 改为智能体后端;新增 `/api/agent` 管理与运行接口,聊天、运行任务、恢复审批和文件预览均从线程绑定的 Agent 解析运行时上下文,前端只提交 `agent_id`,并在模型配置页新增“智能体”管理页签。 - 删除 Upload 与 LightRAG 图谱/知识库能力:知识库类型收敛为 Milvus 与 Dify,只保留 Milvus 知识库内图谱构建/展示/检索,移除独立 `/graph` 页面和默认上传图谱工具。 diff --git a/web/src/components/ToolCallingResult/tools/ImageTool.vue b/web/src/components/ToolCallingResult/tools/ImageTool.vue index 697a977d..d417954d 100644 --- a/web/src/components/ToolCallingResult/tools/ImageTool.vue +++ b/web/src/components/ToolCallingResult/tools/ImageTool.vue @@ -39,7 +39,7 @@ const parsedContent = computed(() => { const imageUrl = computed(() => { const content = parsedContent.value - // text_to_img_qwen_image 返回 URL 字符串 + // 图片类工具返回 URL 字符串时直接渲染图片 if (content && typeof content === 'string' && content.startsWith('http')) { return content }