目录

 

团队信息

一、本期核心任务

二、需求分析

三、方案设计

1.方案选择

方案一:手动可视化编辑方案

方案二:全量文件传输后端处理方案

方案三:精准定位源码位置编辑方案

2.最终选定方案

3.关键技术

四、前端开发:可视化编辑交互

1.同源配置

2.可视化编辑器工具类

3.对话页面集成

五、工程项目增量修改

1. 方案设计

2.工具开发

六、总结

七、后续计划


团队信息

  • 组号:69 组

  • 项目:AI 零代码应用生成平台

  • 负责人:樊伟彤

  • 成员:者亚杰、蒋宇轩、张旭、李重昊

一、本期核心任务

        本期重点开发平台可视化修改核心能力,解决纯文字描述修改定位不准、易改错、沟通成本高的痛点,经过本次开发做到:

        点击网页元素 + 输入一句话=AI精准修改,大幅降低用户使用门槛,完成从生成到精细化定制的闭环。

二、需求分析

        在前面迭代中,平台已经支持对话记忆、上下文迭代优化。但用户修改网站时,仍存在明显问题:

  • 只能靠文字描述位置,比如“改第二个卡片标题”

  • 描述稍模糊,AI就容易改错位置、改错内容、甚至改崩页面

  • 元素多时,用户很难说清是哪个元素

        因此,我们需要实现让用户能够直接点击网页上的元素,然后输入修改需求,AI就能精确知道要修改的是哪个部分:可视化选中元素 + AI 提示词修改

三、方案设计

1.方案选择

        我们梳理出几种实现方案:

方案一:手动可视化编辑方案

        想要实现该方案,需要前端搭建完整的可视化代码编辑器,还需要自行制定一套DSL规范,用来规范代码修改逻辑。

        整体开发难度大,中间存在很多未知问题,开发周期长、成本偏高,对于本项目而言实用性不强,有些舍本逐末。

方案二:全量文件传输后端处理方案

        即将项目所有文件统一传递到后端进行处理。

        这种方式虽然稳定性较高,但会产生大量网络数据传输,同时加重后端运算压力,资源消耗大,并不适合我们目前的服务器配置条件。

方案三:精准定位源码位置编辑方案

        该思路是获取页面元素后,直接锁定元素在项目源码里的具体位置进行修改。

        但实际开发中局限性很大,尤其是经过打包编译的Vue项目,很难精准匹配源码位置,极易引发各类程序错误,稳定性较差。

2.最终选定方案

        综合以上三种方案存在的各类问题,我们最终确定采用可视化选择+AI提示词编辑的实现方式。

        不做复杂手动编辑器,专注:可视化选中+ AI 提示词修改 流程:

  1. 用户开启「编辑模式」

  2. 点击预览页(iframe)上任意元素

  3. 前端自动抓取元素信息:标签、id、class、CSS选择器、文本、位置

  4. 把元素信息自动拼接到用户输入

  5. 后端交给AI,精准修改对应元素

  6. 前端刷新预览,完成修改

        优点:成本低、逻辑清晰、好维护。

        权衡:修改精度依赖AI,但对零代码平台完全够用。

3.关键技术

  • 父子页面通信:postMessage

  • 同源问题:Vite代理 + 相对路径 API

  • 元素定位:自动生成精准 CSS 选择器

  • 修改策略:

    • 原生 HTML / 多文件:全量返回完整文件

    • Vue 项目:增量工具调用修改

四、前端开发:可视化编辑交互

1.同源配置

        在进入可视化编辑模式时,父网站会动态向子网站注入代码,但这一操作必须要求父子网站同源才能正常执行。

        为了解决同源限制问题,我们在前端通过配置代理实现父子网站同源:

        本地开发环境使用 Vite 配置代理后端接口,线上环境则通过 Nginx 实现相同效果,具体修改 vite.config.ts 即可完成配置。

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue(), vueDevTools()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        secure: false,
      },
    },
  },
})

        修改环境配置 .env.development 中本地请求后端的地址为相对目录:

VITE_DEPLOY_DOMAIN=http://localhost
VITE_API_BASE_URL=/api

        这样一来,前端请求后端接口时,因为没有使用绝对路径,会自动请求与前端同域名、同端口的地址,再通过 Vite 代理转发到真实的后端服务地址。

        这套方案完美解决了父子网站同源限制问题,让父网站可以正常向子网站动态注入代码,也不需要让 AI 额外生成兼容代码,整体实现更简洁稳定。

2.可视化编辑器工具类

        可视化编辑文件负责定义子父网站通讯事件,由父组件向子组件注入代码。这个文件极其复杂,封装了所有与可视化编辑相关的逻辑,包括脚本注入、事件监听、元素选择等功能。

/**
 * 可视化编辑器工具类
 * 负责管理iframe内的可视化编辑功能
 */
export interface ElementInfo {
  tagName: string
  id: string
  className: string
  textContent: string
  selector: string
  pagePath: string
  rect: {
    top: number
    left: number
    width: number
    height: number
  }
}
export interface VisualEditorOptions {
  onElementSelected?: (elementInfo: ElementInfo) => void
  onElementHover?: (elementInfo: ElementInfo) => void
}
export class VisualEditor {
  private iframe: HTMLIFrameElement | null = null
  private isEditMode = false
  private options: VisualEditorOptions
  constructor(options: VisualEditorOptions = {}) {
    this.options = options
  }
  /**
   * 初始化编辑器
   */
  init(iframe: HTMLIFrameElement) {
    this.iframe = iframe
  }
  /**
   * 开启编辑模式
   */
  enableEditMode() {
    if (!this.iframe) {
      return
    }
    this.isEditMode = true
    setTimeout(() => {
      this.injectEditScript()
    }, 300)
  }
  /**
   * 关闭编辑模式
   */
  disableEditMode() {
    this.isEditMode = false
    this.sendMessageToIframe({
      type: 'TOGGLE_EDIT_MODE',
      editMode: false,
    })
    // 清除所有编辑状态
    this.sendMessageToIframe({
      type: 'CLEAR_ALL_EFFECTS',
    })
  }
  /**
   * 切换编辑模式
   */
  toggleEditMode() {
    if (this.isEditMode) {
      this.disableEditMode()
    } else {
      this.enableEditMode()
    }
    return this.isEditMode
  }
  /**
   * 强制同步状态并清理
   */
  syncState() {
    if (!this.isEditMode) {
      this.sendMessageToIframe({
        type: 'CLEAR_ALL_EFFECTS',
      })
    }
  }
  /**
   * 清除选中的元素
   */
  clearSelection() {
    this.sendMessageToIframe({
      type: 'CLEAR_SELECTION',
    })
  }
  /**
   * iframe 加载完成时调用
   */
  onIframeLoad() {
    if (this.isEditMode) {
      setTimeout(() => {
        this.injectEditScript()
      }, 500)
    } else {
      // 确保非编辑模式时清理状态
      setTimeout(() => {
        this.syncState()
      }, 500)
    }
  }
  /**
   * 处理来自 iframe 的消息
   */
  handleIframeMessage(event: MessageEvent) {
    const { type, data } = event.data
    switch (type) {
      case 'ELEMENT_SELECTED':
        if (this.options.onElementSelected && data.elementInfo) {
          this.options.onElementSelected(data.elementInfo)
        }
        break
      case 'ELEMENT_HOVER':
        if (this.options.onElementHover && data.elementInfo) {
          this.options.onElementHover(data.elementInfo)
        }
        break
    }
  }
  /**
   * 向 iframe 发送消息
   */
  private sendMessageToIframe(message: Record<string, any>) {
    if (this.iframe?.contentWindow) {
      this.iframe.contentWindow.postMessage(message, '*')
    }
  }
  /**
   * 注入编辑脚本到 iframe
   */
  private injectEditScript() {
    if (!this.iframe) return
    const waitForIframeLoad = () => {
      try {
        if (this.iframe!.contentWindow && this.iframe!.contentDocument) {
          // 检查是否已经注入过脚本
          if (this.iframe!.contentDocument.getElementById('visual-edit-script')) {
            this.sendMessageToIframe({
              type: 'TOGGLE_EDIT_MODE',
              editMode: true,
            })
            return
          }
          const script = this.generateEditScript()
          const scriptElement = this.iframe!.contentDocument.createElement('script')
          scriptElement.id = 'visual-edit-script'
          scriptElement.textContent = script
          this.iframe!.contentDocument.head.appendChild(scriptElement)
        } else {
          setTimeout(waitForIframeLoad, 100)
        }
      } catch {
        // 静默处理注入失败
      }
    }
    waitForIframeLoad()
  }
  /**
   * 生成编辑脚本内容
   */
  private generateEditScript() {
  //...省略
  }
}

3.对话页面集成

        选中元素后,自动把元素信息拼接到prompt

let message = userInput.value.trim()
​
// 有选中元素,自动追加上下文
if (selectedElementInfo.value) {
  const info = selectedElementInfo.value
  message += `
​
选中元素信息:
- 标签:${info.tagName.toLowerCase()}
- 选择器:${info.selector}
- 页面路径:${info.pagePath}
- 当前内容:${info.textContent || '无'}`
}

        前端交互效果:

  • 悬浮:蓝色虚线框

  • 选中:绿色实线框

  • 顶部提示:编辑模式已开启

五、工程项目增量修改

1. 方案设计

        在 Vue 工程项目中,生成的代码量通常非常大,如果每次修改都让 AI 从零返回完整文件内容,效率极低且不现实。因此我们采用AI 工具调用能力,让大模型通过工具实现精准的增量修改,而不是全量重写。

        为此,我们为 AI 提供了一系列专用工具,每个工具独立封装为一个类,方便调用:

  1. 读取单个文件,让 AI 能够查看项目中已有的代码内容

  2. 递归获取目录下的所有文件结构,帮助 AI 理解项目整体组织

  3. 删除单个文件,用于清理无用或错误的文件

  4. 修改单个文件,支持通过匹配旧内容、替换新内容的方式实现精准更新

  5. 创建单个文件(该功能已提前实现)

        同时,我们还需要优化提示词,在提示词底部补充修改规则,明确告诉 AI 如何使用这些工具完成精确的代码修改:

## 特别注意
在生成代码后,用户可能会提出修改要求并给出要修改的元素信息。
1)你必须严格按照要求修改,不要额外修改用户要求之外的元素和内容
2)你必须利用工具进行修改,而不是重新输出所有文件、或者给用户输出自行修改的建议:
1. 首先使用【目录读取工具】了解当前项目结构
2. 使用【文件读取工具】查看需要修改的文件内容
3. 根据用户需求,使用对应的工具进行修改:- 【文件修改工具】:修改现有文件的部分内容- 【文件写入工具】:创建新文件或完全重写文件- 【文件删除工具】:删除不需要的文件

2.工具开发

        为了提升用户体验,每个 AI 工具的入参和输出格式都需要做区分。例如修改文件这个工具,就需要清晰展示文件相对路径、被替换的旧内容、以及更新后的新内容。

        如果直接在处理 AI 流式返回的代码里用大量 if else 判断工具类型,逻辑会变得非常混乱且难以维护。因此我们采用了策略模式 + 工厂模式结合的设计思路:把每个工具类当作一个独立策略,各自实现专属的信息展示与输出方法;再通过一个工厂类统一创建、管理所有工具,让代码结构更清晰、扩展性更强,也方便后续新增工具。

1、工具基类

        定义所有工具必须实现的方法:

/**
 * 工具基类
 * 定义所有工具的通用接口
 */
public abstract class BaseTool {
    /**
     * 获取工具的英文名称(对应方法名)
     *
     * @return 工具英文名称
     */
    public abstract String getToolName();
    /**
     * 获取工具的中文显示名称
     *
     * @return 工具中文名称
     */
    public abstract String getDisplayName();
    /**
     * 生成工具请求时的返回值(显示给用户)
     *
     * @return 工具请求显示内容
     */
    public String generateToolRequestResponse() {
        return String.format("\n\n[选择工具] %s\n\n", getDisplayName());
    }
    /**
     * 生成工具执行结果格式(保存到数据库)
     *
     * @param arguments 工具执行参数
     * @return 格式化的工具执行结果
     */
    public abstract String generateToolExecutedResult(JSONObject arguments);
}

2、工具管理类

        创建工具管理类,自动注册所有的工具 Bean,并且提供了根据名称获取工具的方法。

        本质上就是维护一个工具 Map,代码如下:

/**
 * 工具管理器
 * 统一管理所有工具,提供根据名称获取工具的功能
 */
@Slf4j
@Component
public class ToolManager {
    /**
     * 工具名称到工具实例的映射
     */
    private final Map<String, BaseTool> toolMap = new HashMap<>();
    /**
     * 自动注入所有工具
     */
    @Resource
    private BaseTool[] tools;
    /**
     * 初始化工具映射
     */
    @PostConstruct
    public void initTools() {
        for (BaseTool tool : tools) {
            toolMap.put(tool.getToolName(), tool);
            log.info("注册工具: {} -> {}", tool.getToolName(), tool.getDisplayName());
        }
        log.info("工具管理器初始化完成,共注册 {} 个工具", toolMap.size());
    }
    /**
     * 根据工具名称获取工具实例
     *
     * @param toolName 工具英文名称
     * @return 工具实例
     */
    public BaseTool getTool(String toolName) {
        return toolMap.get(toolName);
    }
    /**
     * 获取已注册的工具集合
     *
     * @return 工具实例集合
     */
    public BaseTool[] getAllTools() {
        return tools;
    }
}

3、修改流处理逻辑

        最后修改流处理逻辑,从 AI 响应中获取到执行的工具名称,然后通过T oolManager获取到对应的工具实例,并通过调用方法来输出信息:

@Resource
private ToolManager toolManager;
case TOOL_REQUEST -> {
    ToolRequestMessage toolRequestMessage = JSONUtil.toBean(chunk, ToolRequestMessage.class);
    String toolId = toolRequestMessage.getId();
    String toolName = toolRequestMessage.getName();
    // 检查是否是第一次看到这个工具 ID
    if (toolId != null && !seenToolIds.contains(toolId)) {
        // 第一次调用这个工具,记录 ID 并返回工具信息
        seenToolIds.add(toolId);
        // 根据工具名称获取工具实例
        BaseTool tool = toolManager.getTool(toolName);
        // 返回格式化的工具调用信息
        return tool.generateToolRequestResponse();
    } else {
        // 不是第一次调用这个工具,直接返回空
        return "";
    }
}
case TOOL_EXECUTED -> {
    ToolExecutedMessage toolExecutedMessage = JSONUtil.toBean(chunk, ToolExecutedMessage.class);
    String toolName = toolExecutedMessage.getName();
    JSONObject jsonObject = JSONUtil.parseObj(toolExecutedMessage.getArguments());
    // 根据工具名称获取工具实例并生成相应的结果格式
    BaseTool tool = toolManager.getTool(toolName);
    String result = tool.generateToolExecutedResult(jsonObject);
    // 输出前端和要持久化的内容
    String output = String.format("\n\n%s\n\n", result);
    chatHistoryStringBuilder.append(output);
    return output;
}

六、总结

        本期聚焦平台可视化修改核心能力开发,在原有AI零代码生成基础上,补齐了从生成到精细化定制的关键闭环,主要收获如下:

  1. 可视化交互落地:通过父子页面通信、同源配置与精准CSS选择器生成,实现了「点击选中元素+AI提示词修改」的核心交互,解决了纯文字描述修改定位不准、易改错的痛点,大幅降低用户使用门槛。

  2. 跨域问题解决:通过Vite代理(本地)+Nginx配置(线上)统一父子网站域名端口,完美解决同源限制,保障元素选择、脚本注入等核心功能的稳定运行。

七、后续计划

1.精度提升:优化CSS选择器生成逻辑,结合元素层级与唯一标识减少选择器冲突,进一步提升AI修改的精准度。

2.AI 工作流:统一 AI 工作流,支持原生 HTML、多文件项目、Vue 工程等不同类型项目的无缝修改。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐