news 2026/5/12 2:33:34

开源数字白板the-board:基于React+Fabric.js的实时协作技术解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
开源数字白板the-board:基于React+Fabric.js的实时协作技术解析

1. 项目概述:一个开源的“数字白板”能做什么?

最近在GitHub上看到一个挺有意思的项目,叫the-board。乍一看名字,可能觉得平平无奇,但点进去你会发现,它其实是一个功能相当完整的在线白板应用。简单来说,你可以把它理解为一个开源的、可以自己部署的“Miro”或“Figma画板”的简化版。它的核心价值在于,为那些需要线上协作、头脑风暴、绘制流程图或简单设计的团队和个人,提供了一个完全自主可控的解决方案。

我自己在团队协作和项目规划中,经常需要用到这类工具。无论是梳理一个复杂的技术架构图,还是和产品经理一起碰撞新功能的交互逻辑,一个直观、流畅的白板工具能极大提升沟通效率。但市面上的主流产品要么是付费订阅,要么对数据存储位置有顾虑。the-board的出现,正好切中了这部分需求:它开源、免费,你可以把它部署在自己的服务器上,所有数据都在自己手里,这对于很多对数据安全有要求的企业或开发者来说,吸引力巨大。

这个项目由loyl-ee维护,技术栈选型非常“现代”且务实,前端基于 React 和 TypeScript,后端使用 Node.js,数据存储则采用了 PostgreSQL。整个架构清晰,文档也相对齐全,对于有一定全栈开发经验的朋友来说,无论是直接使用、二次开发,还是学习其实现思路,都是一个不错的参考对象。接下来,我就结合自己的部署和试用体验,来深度拆解一下这个项目,看看它到底怎么用,背后有哪些技术考量,以及在实际操作中可能会遇到哪些“坑”。

2. 项目整体设计与技术栈选型解析

2.1 核心功能定位与场景分析

the-board的目标很明确:做一个够用的在线协作白板。它没有追求大而全,而是聚焦在几个核心场景上。首先是实时协作,这是在线白板的灵魂,允许多个用户同时在一个画板上操作,看到彼此的光标和编辑痕迹。其次是基础图形绘制,比如矩形、圆形、箭头、连接线、便签(Sticky Note)等,这些是构成思维导图、流程图的基本元素。再者是自由绘制,支持鼠标或触控笔的自由涂鸦,满足手绘草图的需求。最后是基础的文件与文本支持,允许上传图片、插入文本,并对所有元素进行基本的样式调整(颜色、粗细、字体等)。

这些功能组合起来,足以覆盖大多数非设计专业的协作场景:

  • 技术架构讨论:用矩形和箭头快速画出系统组件和数据流。
  • 产品需求梳理:用便签贴出用户故事,然后进行归类、连线。
  • 敏捷开发站会:共享一个看板,更新任务状态(To Do, Doing, Done)。
  • 在线教学与会议:代替物理白板,进行讲解和图示。

项目在技术选型上,充分考虑了实现这些功能的复杂度、开发效率以及未来的可维护性。

2.2 前端技术栈:React + TypeScript + Zustand + Fabric.js

前端是用户体验的直接载体,the-board的选型堪称“黄金组合”。

  • React & TypeScript:这是当前企业级前端开发的事实标准。TypeScript 提供了强大的类型检查,能在开发阶段就规避大量低级错误,对于the-board这种需要管理众多图形对象状态和复杂交互的项目来说,类型安全至关重要。它能明确一个“图形对象”应该有哪些属性(位置、颜色、旋转角度等),让代码更健壮。
  • 状态管理 - Zustand:相比 Redux 的繁琐,Zustand 以其极简的 API 和出色的性能受到了青睐。白板应用的状态非常复杂:当前画布视图、所有图形对象的列表、选中的对象、用户操作历史(用于撤销/重做)、协作成员状态等。Zustand 允许我们将这些状态拆分成多个逻辑 store,例如一个useBoardStore管理画布和图形,一个useUserStore管理用户和协作状态,代码结构清晰,且状态更新高效。
  • 图形渲染库 - Fabric.js:这是整个前端的核心技术。为什么不直接用 SVG 或 Canvas 原生 API 从头写?因为图形交互(选择、拖拽、缩放、旋转)和渲染优化是巨大的坑。Fabric.js 是一个功能强大的 Canvas 库,它抽象出了“对象模型”,每个图形(矩形、路径、文本)都是一个对象,自带交互能力。这让我们可以专注于业务逻辑(如协作同步),而不是去处理如何检测一个不规则图形的点击事件这种底层问题。

    注意:Fabric.js 虽然强大,但本身比较庞大,且其事件体系、坐标系变换需要一定学习成本。在二次开发时,建议先花时间熟悉它的核心概念,如Canvas,Object,Group等。

2.3 后端技术栈:Node.js + Express + PostgreSQL + Socket.IO

后端的职责清晰:提供 REST API 进行项目管理、用户认证(如果实现)、资源上传;更重要的是,通过 WebSocket 实现实时协作。

  • Node.js & Express:轻量且高效,非常适合构建实时应用。JavaScript 全栈开发也降低了上下文切换成本。
  • 数据库 - PostgreSQL:选择 Postgres 而非 MongoDB,体现了对数据一致性和关系型数据的重视。白板数据虽然最终以 JSON 形式存储(一个画板的所有图形数据),但项目元信息(画板ID、创建者、更新时间)、用户信息(如果扩展)等是典型的结构化数据。Postgres 的 JSONB 类型可以高效存储和查询画板的 JSON 数据,同时又能利用 SQL 的强大能力管理元数据,是一种非常平衡的选择。
  • 实时通信 - Socket.IO:这是实现多用户实时协作的关键。当用户A移动了一个图形,前端会通过 Socket.IO 将这个操作事件(如object:modified,包含对象ID和新位置)发送到服务器,服务器再立即广播给连接在同一画板房间的其他所有用户。其他用户的前端收到事件后,调用 Fabric.js 的 API 更新本地对应图形的位置,从而实现“所见即所得”的同步。
    • 冲突处理:这是协作的难点。简单的“最后写入获胜”会导致操作被覆盖。the-board可能需要实现更复杂的逻辑,比如为每个操作分配一个递增的版本号或时间戳,或者使用 Operational Transformation (OT) 或 Conflict-Free Replicated Data Types (CRDT) 算法。从项目现状看,它可能采用了相对简单但有效的“状态同步”或“命令同步”模式,适用于小规模、非高频操作的场景。

2.4 部署与运维:Docker 化与环境配置

项目提供了Dockerfiledocker-compose.yml,这大大降低了部署门槛。一键式部署将前端、后端、PostgreSQL 数据库甚至 Redis(用于 Socket.IO 的适配器存储)都集成好了。

# docker-compose.yml 示例片段 version: '3.8' services: postgres: image: postgres:15 environment: POSTGRES_DB: theboard POSTGRES_USER: user POSTGRES_PASSWORD: password volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7-alpine server: build: ./server depends_on: - postgres - redis environment: DATABASE_URL: postgresql://user:password@postgres:5432/theboard REDIS_URL: redis://redis:6379 ports: - "3001:3001" client: build: ./client depends_on: - server ports: - "3000:3000"

这种容器化部署的优势在于环境一致,避免了“在我机器上是好的”这类问题。对于想快速体验或用于小型团队的开发者来说,几乎可以做到开箱即用。

3. 核心功能模块深度拆解与实操

3.1 画布渲染与图形系统:Fabric.js 的实战应用

the-board的核心交互界面是一个无限大的画布。这里主要依靠 Fabric.js 来搭建。

初始化与视口控制: 首先,需要创建一个 Fabric.js 的Canvas实例,挂载到页面的一个div元素上。为了实现无限画布和流畅的平移缩放,项目通常会监听鼠标滚轮和拖拽事件,动态调整画布的viewportTransform矩阵。这不是简单地缩放canvas元素本身,而是通过变换矩阵来改变整个观察视角,这样所有图形对象的坐标计算依然保持逻辑一致。

// 伪代码示例:初始化画布与基础交互 import { Canvas } from 'fabric'; const canvasElement = document.getElementById('board-canvas'); const canvas = new Canvas(canvasElement, { width: window.innerWidth, height: window.innerHeight, selection: true, // 启用选择 preserveObjectStacking: true, // 保持对象堆叠顺序 }); // 处理画布缩放 canvasElement.addEventListener('wheel', (event) => { event.preventDefault(); const delta = event.deltaY; const zoom = canvas.getZoom(); const newZoom = zoom * (delta > 0 ? 0.95 : 1.05); // 缩放因子 canvas.zoomToPoint({ x: event.offsetX, y: event.offsetY }, newZoom); }); // 处理画布平移(抓手工具) let isPanning = false; canvasElement.addEventListener('mousedown', (event) => { if (event.button === 1 || (event.button === 0 && event.ctrlKey)) { // 中键或Ctrl+左键 isPanning = true; canvas.selection = false; // 临时禁用选择 } });

图形对象管理: 所有用户添加的图形,都是 Fabric.js 的对象实例。the-board需要维护一个与画布上对象同步的状态。当用户添加一个矩形时,代码会创建一个fabric.Rect对象,设置其left,top,fill,stroke等属性,然后将其添加到画布 (canvas.add())。同时,这个对象的引用和其序列化后的数据(用于保存和同步)需要被存入前端的 Zustand store 中。

实操心得:Fabric.js 对象的事件(如object:modified,object:added)是同步状态的关键钩子。一定要在这些事件处理函数中,将变更同步到你的应用状态(Zustand store)和通过 Socket.IO 发送给服务器。否则会导致状态不同步,撤销/重做或协作功能出错。

3.2 实时协作引擎的实现细节

这是项目中最具挑战性的部分。实现流程可以概括为:本地操作 -> 生成操作事件 -> 广播 -> 远端应用

  1. 事件监听与序列化:当用户在本地画布上修改一个图形(移动、缩放、旋转、更改样式)后,Fabric.js 会触发object:modified事件。监听器需要捕获这个事件,并从事件对象中提取出最小化的变更信息。例如,不需要传递整个图形的新 JSON,而只需传递图形ID和变化的属性{ id: 'rect1', left: 100, top: 50 }。这能极大减少网络传输的数据量。

  2. 通过 Socket.IO 发射事件:将上一步得到的最小化变更对象,通过 Socket.IO 客户端发送到服务器。事件名称可以自定义,如board:object-update

    // 前端:发送更新 socket.emit('board:object-update', { boardId: '当前画板ID', objectId: 'rect1', properties: { left: 100, top: 50, version: 2 } // 加入版本号用于冲突处理 });
  3. 服务器广播:服务器端的 Socket.IO 监听board:object-update事件。它会进行简单的验证(如用户是否有权限操作此画板),然后将这个更新事件(可能附带上操作者ID和时间戳)广播到同一个boardId“房间”内的所有其他连接客户端。

    // 后端 Node.js (Socket.IO 服务器端) io.on('connection', (socket) => { socket.on('join-board', (boardId) => { socket.join(boardId); // 加入房间 }); socket.on('board:object-update', (data) => { // 可选:将操作日志存入数据库 // 然后广播给房间内其他用户 socket.to(data.boardId).emit('board:object-updated', data); }); });
  4. 远端应用更新:其他客户端收到board:object-updated事件后,根据objectId找到画布上对应的 Fabric.js 对象,然后使用object.set({ ...data.properties })来应用这些变更。最后调用canvas.requestRenderAll()重绘画布。

    关键技巧:在远端应用更新时,需要临时禁用对应图形对象的 Fabric.js 事件监听,或者设置一个标志位,避免这个由网络同步触发的修改又反过来触发一个新的object:modified事件,导致死循环。

3.3 数据持久化与版本管理

画板的数据需要保存,以便下次打开。这里涉及两个层面:

  1. 全量快照保存:当用户主动点击保存,或系统设置自动保存时,需要将当前画布的状态完整导出。Fabric.js 提供了canvas.toJSON()方法,可以将所有图形对象序列化为一个 JSON 对象。这个 JSON 数据会被通过 REST API 发送到后端,后端将其以 JSONB 格式存入 PostgreSQL 的boards表的data字段中。

  2. 增量操作日志:为了支持更高级的功能,如细粒度的撤销/重做、离线后同步、甚至未来实现“时光机”查看历史版本,仅保存快照是不够的。更优的方案是同时保存一个操作日志(Operation Log)。每一个通过 Socket.IO 广播的协作操作,都可以在广播前先追加到数据库的一个operations表中,记录操作类型、目标对象、参数、版本号、作者和时间戳。恢复画板时,可以先加载一个基础快照,然后按顺序重放之后的所有操作日志,即可得到最新状态。这为冲突解决和版本回溯提供了可能。

4. 本地部署与二次开发实战指南

4.1 基于 Docker 的一键部署流程

对于大多数只想快速搭建一个自用白板的用户,Docker 部署是最佳选择。

步骤一:环境准备确保你的服务器或本地开发机已经安装了 Docker 和 Docker Compose。这是唯一的前提条件。

步骤二:获取代码

git clone https://github.com/loyl-ee/the-board.git cd the-board

步骤三:配置环境变量项目根目录下通常有一个.env.example文件。复制它并创建自己的.env文件,根据注释填写关键配置,如数据库密码、JWT 密钥(用于认证,如果项目实现了的话)、文件上传大小限制等。

cp .env.example .env # 使用文本编辑器修改 .env 文件

步骤四:启动服务使用 Docker Compose 启动所有服务。-d参数表示在后台运行。

docker-compose up -d

这个命令会依次构建客户端、服务器镜像,并启动 PostgreSQL、Redis 容器。首次运行会因为构建镜像和数据库初始化而花费几分钟。

步骤五:访问应用服务启动后,前端应用通常运行在http://localhost:3000,后端 API 在http://localhost:3001。打开浏览器访问前端地址即可。

注意事项:如果部署在云服务器上,需要确保安全组或防火墙开放了 3000 端口(前端)。强烈不建议将测试环境直接暴露在公网,应考虑使用 Nginx 反向代理并配置 HTTPS,或者通过 SSH 隧道访问。

4.2 常见部署问题与排查

即使使用 Docker,部署时也可能遇到问题。以下是一些常见情况:

  1. 容器启动失败,提示数据库连接错误

    • 原因:后端服务 (server) 可能比数据库 (postgres) 先启动完成,导致连接失败。虽然depends_on控制了启动顺序,但并未等待数据库“就绪”。
    • 解决:一种方法是在后端服务的启动命令中添加重试逻辑。更简单的办法是重启一次后端服务:docker-compose restart server。Docker Compose 文件也可以使用healthcheck来确保依赖服务健康后再启动。
  2. 前端能访问,但无法创建画板或操作无响应

    • 原因:前端无法连接到后端 API 或 WebSocket 服务。
    • 排查
      • 打开浏览器开发者工具(F12),查看“网络”(Network)标签页。前端页面加载时,是否有对http://localhost:3001的请求失败?
      • 检查前端代码中配置的后端 API 地址。有时前端代码需要构建时注入环境变量,确认docker-compose.ymlclient服务的环境变量REACT_APP_API_URL等配置是否正确指向了后端容器名(如http://server:3001)或你的公网IP。
      • 检查后端服务日志:docker-compose logs server,看是否有错误信息。
  3. 上传图片失败

    • 原因:可能是服务器端文件存储目录权限问题,或者 Nginx(如果用了)配置了文件大小限制。
    • 解决:检查服务器端处理上传的代码,确认临时目录和最终存储目录是否存在且可写。如果用了 Nginx,在配置中添加client_max_body_size 20M;以增大上传限制。

4.3 二次开发入门:添加一个新图形工具

假设我们想为the-board添加一个“菱形”工具。这涉及到前端 UI、图形创建逻辑和状态管理的修改。

步骤一:在前端添加工具按钮在侧边栏工具栏的 React 组件中,添加一个新的按钮图标,例如一个菱形。为其绑定一个点击事件,用于设置当前的绘图模式。

// 在工具组件中,例如 Toolbar.jsx import { useBoardStore } from '../stores/useBoardStore'; function Toolbar() { const { setDrawingMode, setCurrentTool } = useBoardStore(); const handleAddDiamond = () => { setCurrentTool('diamond'); setDrawingMode(false); // 非自由绘制模式 }; return ( <div className="toolbar"> {/* 其他工具按钮... */} <button onClick={handleAddDiamond} title="添加菱形"> <DiamondIcon /> </button> </div> ); }

步骤二:扩展状态管理在 Zustand 的useBoardStore中,需要管理当前选中的工具 (currentTool)。当工具是diamond时,画布的点击行为应变为创建菱形。

步骤三:修改画布点击事件处理逻辑在画布的主事件监听函数中,检查currentTool状态。如果是'diamond',则在鼠标点击的位置创建一个 Fabric.js 菱形对象并添加到画布。Fabric.js 没有原生的菱形,但我们可以用fabric.Polygon来绘制。

// 在画布组件的事件处理中 canvas.on('mouse:down', (opt) => { const { currentTool } = useBoardStore.getState(); if (currentTool === 'diamond') { const pointer = canvas.getPointer(opt.e); // 定义菱形的四个点(相对于中心) const diamondPoints = [ { x: 0, y: -50 }, // 上 { x: 50, y: 0 }, // 右 { x: 0, y: 50 }, // 下 { x: -50, y: 0 } // 左 ]; const diamond = new fabric.Polygon(diamondPoints, { left: pointer.x, top: pointer.y, fill: '#ffeb3b', strokeWidth: 2, stroke: '#333', objectId: `diamond_${Date.now()}` // 自定义ID,用于协作同步 }); canvas.add(diamond); canvas.setActiveObject(diamond); // 触发状态更新和协作同步 useBoardStore.getState().addObject(diamond.toJSON()); socket.emit('object:added', { boardId, object: diamond.toJSON() }); // 重置工具为选择模式 useBoardStore.getState().setCurrentTool('select'); } });

步骤四:处理协作同步新创建的图形对象,其数据(通过toJSON()序列化)需要被发送到服务器,并广播给其他协作者。其他客户端收到object:added事件后,用fabric.Object.fromObject()方法将 JSON 数据还原为图形对象并添加到他们的画布中。

这个过程清晰地展示了一个新功能从 UI 到交互,再到数据流和协作同步的完整链路。二次开发时,理解这条链路至关重要。

5. 性能优化与安全考量

5.1 前端渲染与操作性能优化

当画布上图形对象过多(比如超过500个)时,性能可能会下降。以下是一些优化思路:

  • 虚拟化渲染:只渲染当前视口及周边缓冲区的图形。Fabric.js 本身不直接支持,但我们可以通过监听画布视口变换 (viewportTransform),计算哪些对象在视野内,然后动态地将不可见对象从画布中移除 (canvas.remove) 或设置为不可见 (object.visible = false),以减轻渲染压力。当视口移动时,再动态添加或显示进入视野的对象。
  • 对象分组:对于大量重复且不需要单独操作的图形(如一片背景网格点),可以将它们合并成一个 Fabric.jsGroup对象。一个组在内部管理多个子对象,但对外被视为一个整体进行渲染和碰撞检测,能显著提升性能。
  • 操作防抖与节流:对于频繁触发的事件,如画布缩放、图形拖拽,在向服务器发送同步消息时,需要使用防抖 (debounce) 或节流 (throttle) 技术。例如,拖拽一个图形时,可以每100毫秒发送一次位置更新,而不是每触发一次object:modified就发送一次。
  • 简化序列化数据canvas.toJSON()默认会包含很多内部属性。在保存或同步时,可以传递一个属性列表给toJSON方法,只导出必要的属性,减少数据体积。

5.2 后端与数据安全加固

对于自部署的应用,安全不容忽视。

  • 输入验证与消毒:所有通过 REST API 和 WebSocket 接收到的数据都必须进行严格的验证。特别是图形对象的 JSON 数据,要防止注入恶意脚本(虽然 Fabric.js 渲染在 Canvas 上,XSS 风险较低,但数据存入数据库再取出可能影响其他客户端)。使用如JoiZod这样的库定义数据模式并进行验证。
  • 身份认证与授权:开源版本可能只实现了基础的房间号共享链接。对于企业级应用,需要集成 JWT (JSON Web Tokens) 或 OAuth 2.0 进行用户认证。每个画板应有明确的访问控制列表 (ACL),区分所有者、编辑者、查看者等角色。
  • WebSocket 连接安全:确保生产环境使用wss://(WebSocket Secure)。Socket.IO 服务器端要对连接进行认证,可以在握手阶段验证 token,防止未授权用户加入任意画板房间。
  • 数据库安全
    • 不要使用默认的 PostgreSQL 端口(5432)。
    • 为应用数据库创建专用用户,并赋予最小必要权限。
    • 定期备份数据库。画板 JSON 数据可能很大,要考虑备份策略和存储成本。
  • 文件上传安全:如果支持上传图片,务必限制文件类型(MIME类型检查)、文件大小,并对上传的文件进行重命名(避免原始文件名可能带来的问题)。理想情况下,应将上传的文件存储到对象存储服务(如 AWS S3、MinIO)或独立的文件服务器,而不是应用服务器本地。

5.3 扩展功能设想与架构演进

the-board作为一个基础框架,有很大的扩展空间:

  • 插件系统:设计一个插件接口,允许开发者通过注入的方式添加新的工具(如图表生成器、图标库)、侧边栏面板或导出格式。这能让核心代码保持简洁。
  • 离线优先与冲突解决:利用浏览器的 IndexedDB 实现离线编辑,在线后自动同步。采用 CRDT 数据结构来处理离线编辑可能产生的冲突,确保最终一致性。这是实现可靠协作编辑的关键一步,但复杂度很高。
  • 版本历史与分支:像 Git 一样,为画板保存每一次重要更改的快照,允许用户查看历史版本、创建分支进行不同思路的探索,甚至合并分支。
  • 模板社区:允许用户将创建好的白板(如项目规划图、用户旅程地图)保存为模板并分享,其他用户可以一键复用,提升创作效率。

这些扩展都会对现有架构提出挑战,需要在设计之初就考虑状态管理的可扩展性、数据模型的兼容性以及前后端通信协议的版本化。the-board目前的简洁架构,为这些演进提供了良好的起点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/12 2:28:36

DDrawCompat完整指南:5个步骤让Windows 11上的老游戏完美运行

DDrawCompat完整指南&#xff1a;5个步骤让Windows 11上的老游戏完美运行 【免费下载链接】DDrawCompat DirectDraw and Direct3D 1-7 compatibility, performance and visual enhancements for Windows Vista, 7, 8, 10 and 11 项目地址: https://gitcode.com/gh_mirrors/dd…

作者头像 李华
网站建设 2026/5/12 2:22:31

基于FPGA的PCIe设备全模拟:从DMA原理到硬件安全测试实践

1. 项目概述如果你对硬件安全研究、系统调试或者仅仅是FPGA的深度玩法感兴趣&#xff0c;那么“全设备模拟”这个概念绝对值得你花时间钻研。简单来说&#xff0c;它指的是利用一块可编程的FPGA板卡&#xff0c;通过定制化的固件&#xff0c;让它从硬件层面“伪装”成另一个PCI…

作者头像 李华
网站建设 2026/5/12 2:22:09

论文降AI教程:从底层算法到实操,5款降AI工具与3大微调技巧

最近不少学弟学妹在后台跟我倒苦水&#xff0c;说查重率好不容易低了&#xff0c;结果AI率越改越高。眼看临近DDL&#xff0c;生怕又因为这个耽误答辩。 作为已经摸爬滚打出来的老学长&#xff0c;今天我就根据我总结出来的经验&#xff0c;从检测系统的底层逻辑开始讲起&…

作者头像 李华
网站建设 2026/5/12 2:20:47

LangChain韩语优化版:构建本地化AI应用的技术实践

1. 项目概述&#xff1a;为什么我们需要一个韩语版的LangChain&#xff1f;如果你在AI应用开发领域&#xff0c;特别是围绕大语言模型&#xff08;LLM&#xff09;构建智能代理或知识库系统&#xff0c;那么“LangChain”这个名字对你来说一定不陌生。它是一个强大的框架&#…

作者头像 李华
网站建设 2026/5/12 2:20:37

AI代理操作系统:从对话驱动到自主管理的项目开发范式转变

1. 项目概述&#xff1a;用AI代理构建产品的结构化方法如果你和我一样&#xff0c;在过去一年里尝试过用各种AI编码助手&#xff08;Claude Code、Cursor、GitHub Copilot&#xff09;来构建项目&#xff0c;那你一定经历过这种状态&#xff1a;一开始充满激情&#xff0c;让AI…

作者头像 李华