feat: 图片改图 MCP 工具 image_edit
- imagegen.edit:POST multipart /v1/images/edits,输入图 必须是工作区内 plain basename 且存在(上限 20MB),产出 必须是新文件、绝不覆盖输入;响应解析与 generate 共用 _save_image_response(url 或 b64_json、下载落盘、格式探测)。 - imagegen_mcp 新增 image_edit 工具;image-workflow 技能 改图分支从“无专用工具”改为用 image_edit。 - 测试:multipart 体含提示词与原图字节、输入缺失/同名/ 路径逃逸拒绝、MCP 列表含 image_edit;全量相关 16 项过。 实测依据(2026-10-30):中转站图片分组 key 调 gpt-image-2.5-flare edits 返回 200,URL 图为真实 PNG 1024x1024、909K,usage 含输入图 token(1659 image tokens)。
This commit is contained in:
1 parent
5b03e0a85d
commit
e13a95067c
4 files changed
+236
-30
No files matched your search
@@ -87,11 +87,55 @@ class ImageGenTests(unittest.TestCase):
|
||||
self.assertIn("404", str(ctx.exception))
|
||||
self.assertNotIn("k", str(ctx.exception))
|
||||
|
||||
def test_edit_multipart_writes_new_file(self):
|
||||
with open(os.path.join(self._tmp.name, "in.png"), "wb") as fh:
|
||||
fh.write(PNG_1x1)
|
||||
seen = {}
|
||||
|
||||
def fake_multipart(url, key, body, content_type):
|
||||
seen["url"] = url
|
||||
seen["body"] = body
|
||||
seen["content_type"] = content_type
|
||||
self.assertTrue(url.endswith("/v1/images/edits"))
|
||||
self.assertIn(b"Change the background", body)
|
||||
self.assertIn(PNG_1x1, body)
|
||||
return 200, {"data": [{"url": "https://cdn.example/e.png",
|
||||
"revised_prompt": "blue bg"}],
|
||||
"usage": {"total_tokens": 2727}}
|
||||
|
||||
out = imagegen.edit(
|
||||
"Change the background to blue", "in.png",
|
||||
output_filename="out.png",
|
||||
request_multipart=fake_multipart,
|
||||
download=lambda url: PNG_1x1, cwd=self._tmp.name)
|
||||
self.assertEqual(out["filename"], "out.png")
|
||||
self.assertEqual(out["input_filename"], "in.png")
|
||||
self.assertEqual(out["format"], "png")
|
||||
self.assertTrue(seen["content_type"].startswith(
|
||||
"multipart/form-data; boundary="))
|
||||
# Input untouched, output is a separate file.
|
||||
with open(os.path.join(self._tmp.name, "in.png"), "rb") as fh:
|
||||
self.assertEqual(fh.read(), PNG_1x1)
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(self._tmp.name, "out.png")))
|
||||
|
||||
def test_edit_missing_input_and_same_output_rejected(self):
|
||||
with self.assertRaises(ImageGenError):
|
||||
imagegen.edit("x", "nope.png", cwd=self._tmp.name)
|
||||
with open(os.path.join(self._tmp.name, "in.png"), "wb") as fh:
|
||||
fh.write(PNG_1x1)
|
||||
with self.assertRaises(ImageGenError):
|
||||
imagegen.edit("x", "in.png", output_filename="in.png",
|
||||
cwd=self._tmp.name)
|
||||
with self.assertRaises(ImageGenError):
|
||||
imagegen.edit("x", "../in.png", cwd=self._tmp.name)
|
||||
|
||||
def test_mcp_tools_list_and_call(self):
|
||||
listed = handle_request({"jsonrpc": "2.0", "id": 1,
|
||||
"method": "tools/list"})
|
||||
names = [t["name"] for t in listed["result"]["tools"]]
|
||||
self.assertIn("image_generate", names)
|
||||
self.assertIn("image_edit", names)
|
||||
called = handle_request(
|
||||
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
||||
"params": {"name": "image_generate",
|
||||
|
||||
Reference in new issue
Block a user