当前位置: 首页 > article >正文

Simple Web Serial:Web与Arduino的轻量级事件驱动串口通信库

1. 项目概述Simple Web Serial 是一个面向嵌入式与 Web 跨域协同开发的轻量级双向通信桥梁库其核心目标是消除 Web Serial API 的底层复杂性让 Arduino 等基于 UART 的微控制器能以事件驱动event-driven范式与浏览器端 JavaScript 应用无缝交互。它并非对 Web Serial API 的简单封装而是构建了一套语义清晰、协议内聚、错误收敛的跨平台通信抽象层——在浏览器侧提供声明式事件注册接口在 MCU 侧提供低开销状态机驱动的串口解析引擎。该库严格遵循“分层解耦、职责单一”原则JavaScript 部分运行于 Chromium 内核浏览器Chrome 89、Edge 89、Opera 75依赖原生navigator.serial接口Arduino 部分兼容所有支持HardwareSerial的平台Arduino Uno/Nano/Leonardo/Mega2560、ESP32、ESP8266、Teensy 等不依赖任何特定硬件抽象层HAL仅需标准Serial对象初始化。工程本质这不是一个通用串口协议栈而是一个面向原型验证prototyping的语义化消息总线。它将原始字节流byte stream升维为带名称name、负载payload和类型type的结构化事件使开发者可直接表达业务意图如led-toggle、sensor-read而非纠缠于帧头校验、长度字段、JSON 解析失败等底层细节。2. 核心架构与通信协议2.1 协议设计哲学Simple Web Serial 采用极简文本协议Minimal Text Protocol摒弃二进制帧格式全程使用 UTF-8 编码的 ASCII 文本。其设计基于三项硬性约束可调试性优先所有通信内容可在串口监视器中直接阅读无需专用解析工具MCU 友好性避免动态内存分配malloc/free杜绝 JSON 解析器如 ArduinoJson依赖全部使用栈上固定缓冲区Web 兼容性天然适配TextDecoder/TextEncoder规避Uint8Array到字符串的手动转换。协议格式定义如下EBNF 表示message event-name : payload \n event-name ALPHA *(ALPHA / DIGIT / - / _) payload / number / string / json-object / json-array number [-] 1*DIGIT [. 1*DIGIT] string *CHAR json-object { *member } json-array [ *value ]关键约束事件名event-name必须为纯 ASCII 字符长度 ≤ 32 字节Arduino 端硬编码限制payload若为 JSON必须为合法 JSON 片段无换行、无注释且总长度 ≤ 256 字节可配置消息以\nLF结尾不支持\r\n所有字段间无空格冒号:后紧跟 payload无空格分隔。2.2 Arduino 端协议解析引擎Arduino 库的核心是SimpleWebSerial.h中的SimpleWebSerial类其内部维护三个关键状态状态变量类型作用典型值state_enum { IDLE, READING_NAME, READING_PAYLOAD }当前解析阶段READING_NAMEname_buffer_[33]char[33]事件名暂存区含终止符led-statepayload_buffer_[257]char[257]负载暂存区含终止符true解析流程为单字符状态机在check()函数中逐字节处理Serial.read()返回值void SimpleWebSerial::check() { while (Serial.available()) { char c Serial.read(); switch (state_) { case IDLE: if (c \n) break; // 忽略空行 if (isEventNameChar(c) name_len_ 32) { name_buffer_[name_len_] c; } else if (c :) { name_buffer_[name_len_] \0; state_ READING_PAYLOAD; payload_len_ 0; } break; case READING_PAYLOAD: if (c \n) { payload_buffer_[payload_len_] \0; dispatchEvent(); // 触发回调 resetState(); } else if (payload_len_ 256) { payload_buffer_[payload_len_] c; } break; } } }关键工程决策该状态机不校验 JSON 语法。当payload_buffer_以{或[开头时库直接调用JSON.parse()JS 端或JSONVar::parse()ArduinoJson 6.x。若解析失败JS 端抛出SyntaxErrorArduino 端静默丢弃该消息——这是为保障实时性而做的明确取舍原型阶段应快速暴露数据格式错误而非在 MCU 端增加重量级错误处理逻辑。2.3 浏览器端协议序列化逻辑JavaScript 端setupSerialConnection()返回的connection对象其send()方法执行以下序列化步骤事件名合法性检查正则/^[a-zA-Z0-9_-]{1,32}$/校验Payload 类型归一化number→String(payload)如3.14→3.14string→ 双引号包裹如on→onobject/array→JSON.stringify(payload)自动转义undefined/null→ 序列化为空字符串拼接消息${eventName}:${payload}\n写入串口通过writer.write(new TextEncoder().encode(message))发送。此设计确保了协议的确定性无论 JS 端传入何种类型Arduino 端收到的始终是符合 EBNF 的文本流极大降低了跨端联调成本。3. API 详解与工程化使用3.1 Arduino 端 API构造与初始化#include SimpleWebSerial.h SimpleWebSerial WebSerial; // 全局单例无参数构造注意该类不管理Serial对象生命周期。Serial.begin()必须由用户在setup()中显式调用推荐波特率57600平衡兼容性与抗干扰性。若需更高吞吐量可设为115200但需确保 USB-UART 芯片如 CH340、CP2102稳定支持。事件注册on()void on(const char* event_name, void (*callback)(JSONVar));参数说明event_nameC 字符串长度 ≤32仅含字母、数字、-、_callback函数指针接收JSONVar类型参数ArduinoJson 6.x 提供。工程实践回调函数内禁止调用delay()应使用millis()非阻塞逻辑若需处理非 JSON 负载如纯数字123可直接data.asint()提取支持嵌套 JSONdata[sensor][temperature].asfloat()。消息发送send()void send(const char* event_name, const char* payload); void send(const char* event_name, int payload); void send(const char* event_name, float payload); void send(const char* event_name, JSONVar payload);重载设计意图覆盖最常用数据类型避免用户手动sprintf或JSON.stringify内存安全const char*重载要求字符串常量或全局缓冲区禁止传入局部数组地址栈内存释放后访问导致未定义行为。主循环驱动check()void check();调用时机必须在loop()中周期性调用建议间隔5msdelay(5)为什么不是中断驱动Web Serial 协议无硬件流控信号RTS/CTS且Serial.available()在中断上下文中不可靠。轮询是唯一可移植方案。完整 Arduino 示例LED 控制与状态上报#include SimpleWebSerial.h #include ArduinoJson.h // v6.x required SimpleWebSerial WebSerial; const int LED_PIN LED_BUILTIN; void setup() { Serial.begin(57600); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // 注册浏览器发来的控制事件 WebSerial.on(led-control, ledControlCallback); WebSerial.on(get-status, getStatusCallback); // 启动时向浏览器发送初始状态 WebSerial.send(device-ready, Arduino Nano); } void loop() { WebSerial.check(); // 关键必须调用 delay(5); } void ledControlCallback(JSONVar data) { if (data.containsKey(state)) { bool state data[state].asbool(); digitalWrite(LED_PIN, state ? HIGH : LOW); // 立即回传确认 WebSerial.send(led-state, state); } } void getStatusCallback(JSONVar) { // 构建状态对象并发送 StaticJsonDocument128 doc; doc[led] digitalRead(LED_PIN) HIGH; doc[uptime_ms] millis(); WebSerial.send(device-status, doc.asJSONVar()); }3.2 JavaScript 端 API连接建立setupSerialConnection()interface ConnectionOptions { requestAccessOnPageLoad?: boolean; // 是否自动弹出权限请求 baudRate?: number; // 波特率默认 57600 dataBits?: 7 | 8; // 数据位默认 8 stopBits?: 1 | 2; // 停止位默认 1 parity?: none | even | odd; // 校验位默认 none } function setupSerialConnection(options?: ConnectionOptions): Connection;权限策略requestAccessOnPageLoad: true会在页面加载时立即触发navigator.serial.requestPort()但 Chrome 要求此操作必须由用户手势如 click触发否则被拒绝。生产环境应设为false并在 UI 按钮点击后手动调用connection.connect()。Connection 对象接口方法签名说明on()on(event: string, callback: (data: any) void): void注册事件监听器data自动解析为 JS 原生类型number/string/object/arraysend()send(event: string, payload: number | string | object | array): Promisevoid发送事件返回写入完成 Promiseconnect()connect(): Promisevoid显式连接串口需先调用requestAccessOnPageLoad: falsedisconnect()disconnect(): Promisevoid断开连接完整 JavaScript 示例控制界面与实时监控!DOCTYPE html html head script typemodule import { setupSerialConnection } from https://unpkg.com/simple-web-seriallatest/dist/index.js; const connection setupSerialConnection({ requestAccessOnPageLoad: false // 手动触发 }); // 连接按钮 document.getElementById(connectBtn).addEventListener(click, async () { try { await connection.connect(); document.getElementById(status).textContent ✅ Connected; document.getElementById(connectBtn).disabled true; } catch (err) { console.error(Connection failed:, err); alert(Failed to connect: err.message); } }); // LED 控制 document.getElementById(ledToggle).addEventListener(change, (e) { connection.send(led-control, { state: e.target.checked }); }); // 监听设备状态 connection.on(led-state, (state) { document.getElementById(ledToggle).checked state; }); connection.on(device-status, (status) { document.getElementById(uptime).textContent status.uptime_ms; document.getElementById(ledStatus).textContent status.led ? ON : OFF; }); // 错误处理 connection.on(error, (err) { console.error(Serial error:, err); document.getElementById(status).textContent ❌ Error: ${err.message}; }); /script /head body button idconnectBtnConnect to Arduino/button divStatus: span idstatusNot connected/span/div labelinput typecheckbox idledToggle LED/label divUptime: span iduptime--/span ms/div divLED Status: span idledStatus--/span/div /body /html4. 工程集成与高级配置4.1 与 FreeRTOS 集成ESP32 场景在 ESP32 上运行 FreeRTOS 时WebSerial.check()不应放在loop()FreeRTOS 中loop()被vTaskStartScheduler()替代。正确做法是创建独立任务#include SimpleWebSerial.h #include freertos/FreeRTOS.h #include freertos/task.h SimpleWebSerial WebSerial; void webSerialTask(void* pvParameters) { for(;;) { WebSerial.check(); vTaskDelay(5 / portTICK_PERIOD_MS); // 5ms 延迟 } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore( webSerialTask, WebSerial, 4096, // stack size NULL, 1, // priority NULL, 0 // core 0 ); } void loop() { /* FreeRTOS scheduler running */ }4.2 自定义缓冲区大小内存受限 MCU对于 RAM 极其紧张的平台如 ATmega328P可修改SimpleWebSerial.h中的宏定义// 默认值适用于大多数场景 #define SWS_EVENT_NAME_MAX_LEN 32 #define SWS_PAYLOAD_MAX_LEN 256 // 内存受限时可缩减需同步修改 JS 端最大消息长度 #define SWS_EVENT_NAME_MAX_LEN 16 #define SWS_PAYLOAD_MAX_LEN 128警告缩减后JS 端send()若发送超长 payloadArduino 端将截断导致 JSON 解析失败。务必两端缓冲区配置一致。4.3 错误诊断与调试技巧串口监视器日志在SimpleWebSerial.cpp的dispatchEvent()前添加Serial.print(RECV: ); Serial.println(name_buffer_);可实时观察原始接收帧JS 端抓包在 Chrome DevTools 的 Application Frames 面板中启用Serial日志查看原始读写字节常见故障树Failed to execute requestPort on Serial: 浏览器不支持仅 Chromium 系、非 HTTPS 环境、未用户手势触发Received event xxx with parameter undefined: Arduino 端 payload 为空或 JSON 解析失败Serial is not defined: 未在setup()中调用Serial.begin()No events received:WebSerial.check()未在loop()中调用或波特率不匹配。5. 典型应用场景与扩展思路5.1 物理交互原型Physical Prototyping旋钮控制网页参数电位器 →analogRead()→WebSerial.send(knob-value, value)→ JS 端实时更新input typerange按钮触发网页动作机械开关 →digitalRead()→WebSerial.send(button-press, {id:1, time:millis()})→ JS 端播放音效、记录日志传感器数据可视化DHT22 → 温湿度 →WebSerial.send(sensor-data, {temp:23.5, humi:45.2})→ Chart.js 实时折线图。5.2 嵌入式设备远程管理固件 OTA 触发网页点击“升级” →send(ota-start, {url:http://.../firmware.bin})→ Arduino 启动 HTTP 下载并烧录日志流式上传MCU 将Serial1调试串口数据通过WebSerial.send(debug-log, line)转发至浏览器控制台替代物理串口线配置参数下发网页表单提交 →send(set-config, {wifi_ssid:myssid, wifi_pass:mypass})→ MCU 存储至 EEPROM 并重启。5.3 与主流嵌入式生态集成PlatformIO 项目配置在platformio.ini中添加lib_deps https://github.com/arduino-libraries/ArduinoJson.git#6.x https://github.com/adafruit/Adafruit-GFX-Library.gitSTM32 HAL 移植要点替换Serial为huart1重写check()使用HAL_UART_Receive_IT() 回调在HAL_UART_RxCpltCallback()中解析字节流Zephyr RTOS 适配利用uart_driver_api获取struct device *uart_dev在k_poll()循环中调用uart_read()。Simple Web Serial 的真正价值不在于它实现了什么高深技术而在于它用一行WebSerial.on(xxx, callback)替代了数百行串口协议解析代码让嵌入式工程师能聚焦于传感器融合算法让前端工程师能专注 UI 交互动效——这种跨领域协作效率的提升正是开源硬件生态持续演进的核心动力。

相关文章:

Simple Web Serial:Web与Arduino的轻量级事件驱动串口通信库

1. 项目概述Simple Web Serial 是一个面向嵌入式与 Web 跨域协同开发的轻量级双向通信桥梁库,其核心目标是消除 Web Serial API 的底层复杂性,让 Arduino 等基于 UART 的微控制器能以事件驱动(event-driven)范式与浏览器端 JavaSc…...

多层PCB结构与过孔工艺深度解析

1. 多层PCB内部结构探秘作为一名硬件工程师,第一次拆解十层PCB板时,那种震撼感至今难忘。密密麻麻的过孔像微型蚁穴般贯穿板体,各层铜箔线路在灯光下泛着金属光泽。本文将用3D视角为你拆解这块"电子千层糕"的构造奥秘。多层PCB的核…...

【IEEE复现】基于神经网络观测器+自适应滑模的无人船,舰艇,船舶轨迹跟踪研究(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...

2026届毕业生推荐的十大AI辅助论文平台实测分析

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 知网于近期发布了有关人工智能生成内容也就是AIGC的投稿须知,其要求清晰且明确&…...

2026届毕业生推荐的五大AI写作助手推荐榜单

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 维普AIGC检测系统,作为学术不端防范方面重要的工具,在高校与科研机构…...

成本控制艺术:OpenClaw+Phi-3-vision-128k-instruct任务级计费方案

成本控制艺术:OpenClawPhi-3-vision-128k-instruct任务级计费方案 1. 当Token消耗成为拦路虎 上个月收到账单时,我的手指在鼠标滚轮上停滞了整整三秒——Phi-3-vision-128k-instruct的API调用费用比预期高出47%。这个数字让我意识到,在享受…...

AD7193高精度ADC驱动设计与嵌入式集成实践

1. PRDC_AD7193 库概述:面向高精度测量的 AD7193 嵌入式驱动设计与工程实践AD7193 是 Analog Devices(ADI)推出的一款专为高精度、低噪声测量场景优化的 Σ-Δ 型 24 位模数转换器(ADC)。其核心特性包括:集…...

嵌入式调试实战:常见错误与高效排查方法

1. 程序员调试中的那些"荒唐"错误 作为一名从业多年的嵌入式工程师,我深知调试过程中的酸甜苦辣。那些看似简单的问题往往耗费我们最多时间,而最终解决方案却常常让人哭笑不得。今天就来分享几个真实的调试故事,希望能给同行们带来…...

硬件电路设计方法论与实战技巧

1. 硬件电路设计系统方法论作为一名从业十年的硬件工程师,我深知从理论到实践的鸿沟有多大。很多新手工程师在掌握了基础电路知识后,面对实际项目时仍然手足无措。硬件设计不是简单的元器件堆砌,而是一个系统工程,需要建立完整的设…...

嵌入式开发中静态代码扫描的必要性与实践

1. 为什么嵌入式开发需要静态代码扫描? 在嵌入式系统开发中,代码质量直接关系到产品的稳定性和安全性。由于嵌入式设备通常部署在关键基础设施、工业控制或消费电子产品中,代码缺陷可能导致严重后果。静态代码扫描作为代码质量保障的重要手段…...

Arduino I²C pH传感器库:高鲁棒性嵌入式pH测量方案

1. 项目概述 iarduino_I2C_pH 是一款专为 iArduino 系列 IC 接口 pH 传感器模块设计的 Arduino 兼容 C 库。该库面向嵌入式硬件工程师与固件开发者,提供对 pH-метр(pH 计)模块的完整底层控制能力,支持标准硬件 IC 外设&#…...

JTAG接口原理、故障诊断与安全操作指南

1. JTAG接口基础解析作为一名从事FPGA开发多年的工程师,我经常需要与JTAG接口打交道。这个看似简单的四线接口,在实际工作中却经常给我们带来各种"惊喜"。今天我就结合自己踩过的坑,系统地讲讲JTAG那些事儿。JTAG(Joint Test Actio…...

OpenClaw+Phi-3-vision-128k-instruct图文处理实战:本地部署与多模态任务自动化

OpenClawPhi-3-vision-128k-instruct图文处理实战:本地部署与多模态任务自动化 1. 为什么选择这个技术组合? 去年我开始尝试用AI处理日常工作中的图文混合内容时,遇到了一个典型困境:现有的云端多模态服务要么价格昂贵&#xff…...

【AI实战课程】第三章:⾃然语⾔处理的常⻅任务和⽅法

分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来!请轻击人工智能教程​​​https://www.captainai.net/troubleshooter 本阶段重点讲解AI⾃然语⾔处理中的主流任务,如⽂本分…...

Azure IoT Hub AMQP传输层深度解析与嵌入式实践

1. Azure IoT Hub AMQP 传输层技术深度解析Azure IoT Hub 是微软面向物联网场景构建的高可靠、可扩展云平台,其核心能力依赖于多种协议栈的协同支持。在众多通信协议中,AMQP(Advanced Message Queuing Protocol)因其固有的消息可靠…...

STM32智能灌溉系统设计与实现

1. 项目概述这个智能灌溉控制系统是我去年为一个农业科技公司做的实际项目,当时他们需要在200亩的蓝莓种植基地部署一套自动化灌溉方案。经过三个月的开发和实地测试,最终形成了这套基于STM32的稳定系统。现在把整个设计过程整理出来,希望能给…...

从脉冲到CAN总线:一文搞懂Emm42 V5.0步进闭环驱动的四种控制方式(含Arduino/PLC接线示例)

从脉冲到CAN总线:Emm42 V5.0步进闭环驱动的四种控制方式深度解析 在工业自动化和嵌入式开发领域,步进电机的精确控制一直是工程师们关注的重点。Emm42 V5.0步进闭环驱动器作为新一代高性能驱动解决方案,凭借其丰富的控制接口和先进的FOC矢量…...

TM1620驱动数码管的8个常见坑点及解决方案(基于STM32实战)

TM1620驱动数码管的8个常见坑点及解决方案(基于STM32实战) 当你在STM32项目中使用TM1620驱动数码管时,可能会遇到各种令人头疼的问题。本文将深入探讨8个最常见的坑点,并提供经过实战验证的解决方案,帮助开发者快速定位…...

从“能用”到“好用”:给你的GoLand 2022.2.3装上这些插件,开发体验大不同

从“能用”到“好用”:给你的GoLand 2022.2.3装上这些插件,开发体验大不同 每天面对代码编辑器的时间可能比面对家人还长——这不是玩笑,而是许多开发者的真实写照。当GoLand从单纯的代码工具转变为你的"数字工作台",插…...

2026届必备的六大AI论文助手实测分析

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 此刻,针对学术写作情形的AI辅助网站已然构建起多元化生态,这类平台一…...

抖音批量下载工具终极指南:免费下载去水印视频的完整教程

抖音批量下载工具终极指南:免费下载去水印视频的完整教程 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback su…...

2025届学术党必备的降重复率网站横评

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 研究人工智能开题报告的工具,借助自然语言处理技术,靠着学术大数据分…...

ExtendedChars:Adafruit GFX的UTF-8扩展字符支持方案

1. 项目概述 ExtendedChars 是一个专为 Adafruit GFX 图形库设计的轻量级扩展组件,其核心工程目标是突破原生 GFX 库对 ASCII 字符集(0x00–0x7F)的硬性限制,实现对 UTF-8 编码多字节字符的可靠解析与渲染。该库并非重写显示驱动…...

Linux五种I/O模型详解与性能对比

1. Linux I/O 模型基础概念解析在深入探讨五种I/O模型之前,我们需要先理解几个关键的基础概念。这些概念是理解不同I/O模型差异的基石,也是很多开发者在实际工作中容易混淆的地方。1.1 用户态与内核态Linux系统将运行环境分为用户态(User mode)和内核态(…...

LSM6DS3TR-C驱动开发指南:寄存器配置与嵌入式IMU工程实践

1. JoyIT_LSM6DS3TR-C库深度解析:面向嵌入式工程师的LSM6DS3TR-C驱动开发指南LSM6DS3TR-C是意法半导体(STMicroelectronics)推出的超低功耗、高精度6轴惯性测量单元(IMU),集成三轴加速度计与三轴陀螺仪&…...

STM32温室智能监控系统开发实战

1. 项目概述这个温室培育系统项目是我去年为一个农业科技公司开发的实战案例。整套系统基于STM32F103RCT6主控,整合了12种硬件模块,实现了温室环境的全自动化监控与调控。最让我自豪的是,系统上线后客户反馈作物产量提升了23%,水电…...

大厂真实高频的 LLM 大模型面试 36 题例题详解

一、基础原理篇(8 题) 1. 什么是 Transformer?核心结构是什么? 答:Transformer 是基于自注意力机制的 seq2seq 模型,完全替代 RNN 结构。核心结构: Encoder(编码)+ Decoder(解码) 多头注意力(Multi-Head Attention) 前馈网络 FFN 层归一化、残差连接举例:GPT 只…...

HUSB238 USB-C PD物理层驱动设计与ESP32集成指南

1. HUSB238 驱动库概述HUSB238 是由 Microchip 推出的 USB Type-C 和 USB PD(Power Delivery)源端(Source)控制器,专为高集成度、小尺寸 USB-C 充电应用设计。其核心功能包括:USB-C 插拔检测(CC…...

告别‘一视同仁’:用HAN(异质图注意力网络)搞定电影推荐里的‘导演偏好’与‘演员偏好’

异构图注意力网络在电影推荐中的实战:如何让算法读懂导演偏好与演员偏好 想象这样一个场景:你刚看完詹姆斯卡梅隆执导的《终结者》,流媒体平台紧接着推荐了同样由施瓦辛格主演的《终结者2》和卡梅隆的另一部作品《泰坦尼克号》。虽然这三部电…...

AI Memory 全景解析:让 Agent 真正记住你

AI Memory 全景解析:让 Agent 真正"记住"你 你有没有遇到过这种场景:明明昨天告诉 AI 助手你喜欢简洁的代码风格,今天它又开始写冗长的注释;或者你费心纠正了一个错误,下次对话它照犯不误。这就是 AI 没有记…...