💥第9天:用Sonnet 3.5打造专属Agent工具,你也能做到!💥


3 个月前

100天代理工程师挑战第9天:为Agent定制Bubble工具,Sonnet 3.5 vs GPT-4o

低代码解决方案非常棒,你只需要打开浏览器,创建项目,然后开始构建你的应用程序,不需要安装、升级或维护任何包。你甚至可以在全新的电脑或别人的笔记本上启动项目,唯一需要的只是一个浏览器和网络连接。但有时候,你可能需要一个无代码或低代码工具无法直接支持的解决方案。没问题,你总是可以通过自定义代码来扩展它,但有一个重要的注意事项:在集成自定义代码时,你需要确保代码与低代码界面之间的连接保持顺畅。在继续之前,让我们先回顾一下我的日常任务。

我的日常任务

1.锻炼:20个俯卧撑 — 20个俯卧撑轻松完成,感觉越来越简单了 :)

2.七小时睡眠 — 晚睡是个问题,明天得改掉这个习惯。

3.低代码开发:3小时 — 使用Bubble、Langflow、FlutterFlow并进行定制
4.AI助手:排队中
5.PAIC:排队中
6.数据科学:排队中

如果你想了解这些任务的具体内容,请阅读挑战的引言部分。

无代码网页应用工具与代理AI有什么关系?

要构建代理,我们需要一些工具来帮助我们,所以我从构建这些工具开始。我使用低代码来制作这些工具,以便快速构建并随时进行测试,而无需浪费时间在部署或维护上。

使用FLUX.1 Fill进行图像修复与扩展


图片来源:Black Forest Labs

FLUX.1 Fill 提供了强大的图像修复功能,比Ideogram 2.0等工具以及开源选项如AlimamaCreative的FLUX-Controlnet-Inpainting更出色。它使图像编辑变得简单,并且能够自然地融合修改。它还支持图像扩展(outpainting),因此你可以将图像扩展到原始边缘之外。


图片来源:Black Forest Labs

让我们从图像修复功能开始,图像扩展功能改天再讨论。以下是一个示例cUrl代码,它将帮助我们把这个图像修复功能集成到Bubble应用中。

curl https://api.bfl.ml/v1/flux-pro-1.0-fill 
  --request POST 
  --header 'Content-Type: application/json' 
  --header 'X-Key: YOUR_SECRET_TOKEN' 
  --data '{
  "image": "",
  "mask": "",
  "prompt": "ein fantastisches bild",
  "steps": 50,
  "prompt_upsampling": false,
  "seed": 1,
  "guidance": 60,
  "output_format": "jpeg",
  "safety_tolerance": 2
}'

如你所见,我们需要一张图片和另一张标记了需要修改或替换部分的图片,第二张图片应作为遮罩发送。两张图片都需要进行Base64编码。

在Bubble中使用自定义代码进行图像AI编辑

在Bubble中,有一些画布插件,甚至有一个集成了fabric.js的插件,但我需要让它更用户友好,并保持现有的UI。我的想法是上传图片,然后允许用户标记需要替换的部分。我通过HTML元素和自定义的Vanilla JS代码实现了这一点,但最重要的是,只在必要时使用自定义代码,以保持脚本与Bubble之间的连接,并且工作流也应由Bubble管理。

<div id="canvas-container" style="position: relative;">
  <canvas id="drawingCanvas" style="border:1px solid #d3d3d3; display: none;"></canvas>
</div>

<script>
  (function() {
    const canvas = document.getElementById('drawingCanvas');
    const ctx = canvas.getContext('2d');
    const img = new Image();
    img.crossOrigin = "Anonymous";

    let isDrawing = false;
    let path = [];

    // 更新画布图片URL的函数
    function loadImage(url) {
      img.src = url;
      img.onload = function() {
        canvas.width = img.width;
        canvas.height = img.height;
        canvas.style.display = "block";
        ctx.drawImage(img, 0, 0);
        addSaveButton();
      };
    }

    let dynamicImageUrl = "YOUR_DYNAMIC_IMAGE_URL";

    function updateImageUrl(newUrl) {
      if (newUrl && newUrl !== dynamicImageUrl) {
        dynamicImageUrl = newUrl;
        loadImage(dynamicImageUrl);
      }
    }

    canvas.addEventListener('mousedown', (e) => {
      isDrawing = true;
      path = [{ x: e.offsetX, y: e.offsetY }];
    });

    canvas.addEventListener('mousemove', (e) => {
      if (isDrawing) {
        const point = { x: e.offsetX, y: e.offsetY };
        path.push(point);

        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(img, 0, 0);

        ctx.beginPath();
        ctx.moveTo(path[0].x, path[0].y);
        path.forEach(p => ctx.lineTo(p.x, p.y));

        // 在画布上提供视觉反馈
        ctx.lineWidth = 20;
        ctx.strokeStyle = 'rgba(255, 0, 0, 0.3)';
        ctx.lineJoin = 'round';
        ctx.lineCap = 'round';
        ctx.stroke();
      }
    });

    canvas.addEventListener('mouseup', () => {
      isDrawing = false;
    });

    window.saveMask = function() {
      const maskCanvas = document.createElement("canvas");
      const maskCtx = maskCanvas.getContext("2d");

      maskCanvas.width = canvas.width;
      maskCanvas.height = canvas.height;

      // 将整个背景设为黑色
      maskCtx.fillStyle = "black";
      maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);

      // 用纯白色绘制选中的路径
      maskCtx.beginPath();
      maskCtx.moveTo(path[0].x, path[0].y);
      path.forEach(p => maskCtx.lineTo(p.x, p.y));

      maskCtx.lineWidth = 20;
      maskCtx.lineJoin = 'round';
      maskCtx.lineCap = 'round';
      maskCtx.strokeStyle = "white"; // 选中区域用纯白色
      maskCtx.stroke();

      const maskData = maskCanvas.toDataURL("image/png").replace(/^data:image/png;base64,/, "");

      if (typeof window.bubble_fn_saveMaskData === "function") {
        window.bubble_fn_saveMaskData("");
      }

      setTimeout(() => {
        if (typeof window.bubble_fn_saveMaskData === "function") {
          window.bubble_fn_saveMaskData(maskData);
        } else {
          console.error("JavaScript to Bubble function is not defined.");
        }
      }, 50);
    };

    function addSaveButton() {
      const existingButton = document.getElementById('saveMaskButton');
      if (existingButton) return;

      const saveButton = document.createElement('button');
      saveButton.id = 'saveMaskButton';
      saveButton.innerHTML = '保存遮罩';
      saveButton.style.position = 'absolute';
      saveButton.style.top = '10px';
      saveButton.style.left = '10px';
      saveButton.style.padding = '10px';
      saveButton.style.backgroundColor = '#4CAF50';
      saveButton.style.color = 'white';
      saveButton.style.border = 'none';
      saveButton.style.cursor = 'pointer';
      saveButton.onclick = saveMask;

      const canvasContainer = document.getElementById('canvas-container');
      canvasContainer.appendChild(saveButton);
    }

    window.updateImageUrl = updateImageUrl;

  })();
</script>

我使用了Toolbox插件及其JavaScript到Bubble的元素。它运行得很好,令人惊讶的是,通过如此简短的脚本就能添加自定义功能。

Claude Sonnet 3.5 vs OpenAI GPT-4o

我尝试了Sonnet和GPT-4o来帮助我编写脚本。Claude Sonnet 立即处理了绘图功能,而GPT-4o则开始添加一些奇怪的方块,而不是简单的绘图。但有趣的是,GPT-4o更了解如何将脚本连接到Bubble的基础设施。有时候,使用两个模型进行开发确实是有意义的。

FluxAI 中文

© 2025. All Rights Reserved