feat: 添加图片生成技能 image-gen,迁移 Qwen-Image 生成能力至内置 Skill

This commit is contained in:
Wenjie Zhang 2026-06-02 20:04:33 +08:00
parent d4bc51714c
commit 0dbdc647ec
9 changed files with 108 additions and 66 deletions

View File

@ -48,4 +48,6 @@
- [x] parser 从plugins 移动到 knowledge 里面guard 移动到services 里面 - [x] parser 从plugins 移动到 knowledge 里面guard 移动到services 里面
- [x] neo4j 相关的服务,可以移动到 storage 里面 - [x] neo4j 相关的服务,可以移动到 storage 里面
- [ ] 点开对话的时候要能够自动定位到尾部,而不是最开始。 - [ ] 点开对话的时候要能够自动定位到尾部,而不是最开始。
- [ ] 将 Qwen-Image 修改为 Skill暴露一个工具叫做上传图片到 MINIO这样生成图片的时候就可以调用别的接口生成图片并可以上传到 MINIO 上面。也就是将原本生成图片的逻辑移动到 Skill 里面来实现,这样就可以支持多种生成图片的模型了。接入多种的接口 - [x] 将 Qwen-Image 修改为 Skill
- [ ] 现在输入区域对于不同 mention 的渲染的 ICON 和 human-message 里面的渲染的 ICON; 居然不一样?这个是什么原因,综合并统一一下,使用最优方案。
- [ ] 现在的消息突然居然

View File

@ -21,6 +21,13 @@ class BuiltinSkillSpec:
_SKILLS_ROOT = Path(__file__).resolve().parent _SKILLS_ROOT = Path(__file__).resolve().parent
BUILTIN_SKILLS: list[BuiltinSkillSpec] = [ 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( BuiltinSkillSpec(
slug="deep-reporter", slug="deep-reporter",
source_dir=_SKILLS_ROOT / "deep-reporter", source_dir=_SKILLS_ROOT / "deep-reporter",

View File

@ -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`,否则前端不会自动展示生成图片。

View File

@ -1,10 +1,9 @@
# buildin 工具包 # buildin 工具包
# from .install_skill import install_skill # 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__ = [ __all__ = [
"ask_user_question", "ask_user_question",
# "install_skill", # "install_skill",
"present_artifacts", "present_artifacts",
"text_to_img_qwen_image",
] ]

View File

@ -1,9 +1,7 @@
import os import os
import uuid
from pathlib import Path from pathlib import Path
from typing import Annotated from typing import Annotated
import requests
from langchain.tools import InjectedToolCallId from langchain.tools import InjectedToolCallId
from langchain_core.messages import ToolMessage from langchain_core.messages import ToolMessage
from langgraph.prebuilt.tool_node import ToolRuntime from langgraph.prebuilt.tool_node import ToolRuntime
@ -11,7 +9,6 @@ from langgraph.types import Command, interrupt
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from yuxi.agents.toolkits.registry import ToolExtraMetadata, _all_tool_instances, _extra_registry, tool 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 import logger
from yuxi.utils.paths import VIRTUAL_PATH_OUTPUTS from yuxi.utils.paths import VIRTUAL_PATH_OUTPUTS
from yuxi.utils.question_utils import normalize_questions 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) # Lazy initialization for TavilySearch (only when API key is available)
_tavily_search_instance = None _tavily_search_instance = None
QWEN_IMAGE_CONFIG_GUIDE = """
使用前需要先配置硅基流动的图片生成访问凭证
请在后端运行环境中配置环境变量
- `SILICONFLOW_API_KEY`用于调用 SiliconFlow 的图片生成接口
配置完成后即可使用该工具生成图片
""".strip()
def _create_tavily_search(): def _create_tavily_search():
"""Create and register TavilySearch tool with metadata.""" """Create and register TavilySearch tool with metadata."""
@ -216,52 +204,3 @@ def ask_user_question(
"questions": normalized_questions, "questions": normalized_questions,
"answer": answer, "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

View File

@ -38,6 +38,16 @@ def test_parse_skill_markdown_requires_frontmatter():
svc._parse_skill_markdown("# missing") 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(): def test_is_valid_skill_slug():
# Test valid slugs # Test valid slugs
assert svc.is_valid_skill_slug("demo-skill") is True assert svc.is_valid_skill_slug("demo-skill") is True

View File

@ -41,9 +41,11 @@ from yuxi.agents.toolkits import buildin, mysql # 触发 @tool 装饰器执行
|------|------| |------|------|
| `calculator` | 计算器,支持加减乘除 | | `calculator` | 计算器,支持加减乘除 |
| `ask_user_question` | 向用户发起交互式提问 | | `ask_user_question` | 向用户发起交互式提问 |
| `text_to_img_qwen_image` | 使用 Qwen-Image 生成图片 | | `present_artifacts` | 展示 Agent 沙盒 outputs 目录下的产物文件 |
| `tavily_search` | Tavily 网页搜索(需配置 `TAVILY_API_KEY` | | `tavily_search` | Tavily 网页搜索(需配置 `TAVILY_API_KEY` |
Qwen-Image 生成能力已迁移为内置 Skill `image-gen`。模型调用与图片下载在 Agent 沙盒中完成,生成后的图片保存到 `/home/gem/user-data/outputs/`,再通过 `present_artifacts` 展示。
### MySQL 工具 (mysql) ### MySQL 工具 (mysql)
| 工具 | 说明 | | 工具 | 说明 |

View File

@ -38,6 +38,7 @@
### 0.7.0 开发记录 ### 0.7.0 开发记录
<!-- 0.7.0 的内容请放在这里 --> <!-- 0.7.0 的内容请放在这里 -->
- 图片生成能力迁移为 SkillQwen-Image 从内置 Python 生成工具迁移到内置 Skill `image-gen`,模型调用与图片下载在 Agent 沙盒中完成,生成结果保存到 outputs 并通过 `present_artifacts` 展示,为多图片生成模型接入复用同一产物展示链路。
- 降低知识库路由与工具模块复杂度:示例问题生成迁移到知识库 utils文件上传统一 100 MB 限制URL 预处理入库路径与旧 `content_type=url` 行为收敛,并修复 uid、导出 MIME 与异常透传等路由问题。 - 降低知识库路由与工具模块复杂度:示例问题生成迁移到知识库 utils文件上传统一 100 MB 限制URL 预处理入库路径与旧 `content_type=url` 行为收敛,并修复 uid、导出 MIME 与异常透传等路由问题。
- 重构智能体配置语义:用户可见的 `AgentConfig` 收敛为数据库持久化的一级 `Agent`,内置 Python Agent 改为智能体后端;新增 `/api/agent` 管理与运行接口,聊天、运行任务、恢复审批和文件预览均从线程绑定的 Agent 解析运行时上下文,前端只提交 `agent_id`,并在模型配置页新增“智能体”管理页签。 - 重构智能体配置语义:用户可见的 `AgentConfig` 收敛为数据库持久化的一级 `Agent`,内置 Python Agent 改为智能体后端;新增 `/api/agent` 管理与运行接口,聊天、运行任务、恢复审批和文件预览均从线程绑定的 Agent 解析运行时上下文,前端只提交 `agent_id`,并在模型配置页新增“智能体”管理页签。
- 删除 Upload 与 LightRAG 图谱/知识库能力:知识库类型收敛为 Milvus 与 Dify只保留 Milvus 知识库内图谱构建/展示/检索,移除独立 `/graph` 页面和默认上传图谱工具。 - 删除 Upload 与 LightRAG 图谱/知识库能力:知识库类型收敛为 Milvus 与 Dify只保留 Milvus 知识库内图谱构建/展示/检索,移除独立 `/graph` 页面和默认上传图谱工具。

View File

@ -39,7 +39,7 @@ const parsedContent = computed(() => {
const imageUrl = computed(() => { const imageUrl = computed(() => {
const content = parsedContent.value const content = parsedContent.value
// text_to_img_qwen_image URL // URL
if (content && typeof content === 'string' && content.startsWith('http')) { if (content && typeof content === 'string' && content.startsWith('http')) {
return content return content
} }