会插卡片的编辑器:在 Agent 输入框里混排文字与结构化内容

作者:桃酥

  • 摘要:为了让用户在智能体输入框中编排复杂任务、复用已有能力,实现了一个「文字 + 卡片」混排的指令编辑器。本文从需求分析和技术选型出发,介绍整体架构、核心实现、实际踩坑,以及这套方案的适用范围与后续改进。

为什么做:让用户在输入框里组合已有能力

在我们做的智能体功能中,用户不仅需要提出一个问题,还需要组织更复杂的执行要求:先做什么,满足什么条件后再做什么,以及过程中要用到哪些已有能力。

这些要求涉及两类内容。自然语言负责描述任务、条件和前后衔接;已经定义好的指令和动作,则是可以被引用、复用的能力。我们希望用户在同一个输入框内,把它们组合起来交给 Agent。

以一个示意场景为例:

每天早上 指令:查询报表 ,如果超过阈值,就 动作:发送通知。

这里的方括号代表卡片。"每天早上""如果超过阈值"是用户自由编写的文字;"查询报表""发送通知"则引用已有的指令或动作,不需要用户在每次编写任务时重新描述其全部内容。

因此,要解决的不是普通文本输入,也不是文章排版,而是让自然语言和结构化引用在同一个编辑区域中混排。文字可以自由修改,卡片可以插入、替换和调整位置,最终共同组成一段执行要求。

编辑器负责这段内容的编写与表示;Agent 如何理解条件、调度能力和执行任务,不在本文的讨论范围内。

如何分析:从混排需求到技术选型

1.先确定编辑器需要处理什么

从编辑行为看,两类内容的区别不是"是否自定义",而是"如何修改":普通文字按字符编辑,已有卡片则需要作为一个整体操作。即使支持创建自定义指令,创建完成后,它仍然是一张完整的卡片。

这带来了几项具体要求:

  • 混合输入

    文字与卡片处于同一条编辑流中,可以在卡片前后继续打字、换行。

  • 卡片操作

    通过菜单插入、点击替换、拖拽排序;删除时不能把卡片拆成半截。

  • 内容转换

    界面展示卡片,保存时输出约定的文本标记,再次打开能够还原。

  • 输入体验

    菜单不能抢走正常输入,中文输入法、光标和选区需要协调处理。

判断方案是否合适,不能只看"能不能画出一个标签",还要看标签参与编辑后,这些行为能否一起成立。

2.比较几条实现路线

我调研了 textarea 叠加标签、React Quill、CodeMirror,以及直接使用 contentEditable 的方案。下面的取舍针对本次需求,并不代表其他方案无法实现类似交互。

最终,我们选择基于 React 和 contentEditable 实现一个专用编辑器:普通文字保留原生编辑能力,卡片渲染为不可直接编辑的节点,内部维护文本与 HTML 的转换,再补上菜单、键盘和拖拽交互。

这个选择的收益是可以直接控制混排 DOM 和交互细节,代价是必须承担编辑行为的维护成本。contentEditable 提供的是可编辑的容器,不是一套已经处理好所有边界的编辑器。

如何实现:整体架构、核心机制与实际踩坑

1.整体架构:对外传递内容,对内管理状态与 DOM

先看整体职责和数据流。下图按逻辑职责组织,不代表这些能力都已经拆成独立模块;转换模块和光标 Hook 的进一步抽取,放在后续改进中。

bash 复制代码
flowchart TB

    Parent["业务父组件"]


    subgraph Editor["指令编辑器"]

        Input["内容入口:初始标记文本"]

        Output["对外通知:内容变化、失焦"]


        subgraph Interaction["交互与状态"]

            Events["输入 / 菜单选择 / 键盘 / 拖拽"]

            UI["React state:菜单、搜索词、编辑模式、拖拽展示"]

            Flags["ref:创建中标志、当前拖拽卡片等过程信息"]

        end


        subgraph Common["通用处理职责"]

            Convert["内容转换:标记文本 ↔ HTML"]

            Position["位置处理:文本位置、DOM 位置、屏幕坐标"]

            Operations["节点操作:插入、替换、删除、换行、移动"]

            Normalize["DOM 规范化:异常结构转回约定结构"]

        end


        DOM["contentEditable DOM:文字 + 卡片 + 换行"]

    end


    Parent -->|传入初始内容| Input

    Input --> Convert

    Convert -->|渲染| DOM

    DOM -->|读取内容| Convert

    Convert -->|标记文本| Output

    DOM -->|失焦| Output

    Output --> Parent

    DOM -->|编辑事件| Events

    Events --> UI

    Events --> Flags

    Events --> Position

    Events --> Operations

    Position -->|插入位置与落点| Operations

    Position -->|菜单坐标| UI

    Operations --> DOM

    Events -->|发现异常结构| Normalize

    Normalize -->|复用双向转换| Convert

对外,组件保持简单。 父组件传入初始内容,监听内容变化和失焦,不需要自己处理卡片节点、光标位置或浏览器生成的 HTML。

对内,UI 状态与编辑 DOM 分开管理。 菜单显隐、位置、搜索词和编辑模式等影响界面的信息放在 state 中;内容转换、光标定位和节点修改则直接操作编辑器 DOM。

"是否正在创建新指令""当前拖着哪张卡片"这类过程信息用 ref 保存,便于事件回调读取最新值。是否使用 ref 的依据是这份信息是否需要驱动渲染,而不是用它替代所有状态管理。

通用能力主要集中在转换、定位和节点操作上:初始化、粘贴和规范化都要用到内容转换;菜单、插入和拖拽都需要位置处理。把这些职责放在一起看,可以避免每个交互各自维护一套内容规则。

2.核心实现:让混排内容能够连续编辑

2.1.同一份内容,两种表示

界面上,卡片是带图标的标签;保存时,卡片对应一段特殊标记文本。当前有指令(Command)和动作(Action)两类,例如:

bash 复制代码
每天早上 <|command_查询报表|>,如果超过阈值,就 <|action_发送通知|>。

编辑器再次打开时,把标记还原成卡片;内容输出时,再把卡片还原成标记。这样,展示结构与存储表示不必相同,但需要遵守同一套转换规则。

文本转 HTML 分为三步:

  1. 先用占位符保护卡片标记,再转义普通文本中的 HTML 字符,避免把用户文本当成 HTML 执行,也避免卡片语法被一起转义。

  2. 还原卡片标记,并转换为带图标的标签节点。普通卡片设置 contenteditable="false",同时支持点击和拖拽。

  3. 将换行符转换成编辑区域中的换行节点。

HTML 转文本 时则反过来:卡片节点恢复为 <|command_xxx|> 或 <|action_xxx|>,换行恢复为 \n,清理用于定位光标的零宽字符,并处理转换产生的多余末尾空行。

粘贴复用这套转换:只读取剪贴板中的纯文本,再转换成编辑器可接受的 HTML 插入,避免把外部页面的样式和复杂结构一起带进来。

2.2.卡片与菜单:文字可编辑,引用整体操作

卡片有三种状态:

菜单提供两个入口:输入 / 时,在光标位置弹出插入菜单;点击已有卡片时,在卡片旁弹出替换菜单。

菜单内支持搜索、指令与动作分组、方向键选择和回车确认。按 + 可以创建新指令,此时插入的是一张临时可编辑卡片。

这里需要同时处理两种位置:一是编辑内容中要替换或插入的位置,二是菜单应该显示的屏幕坐标。两者不能混用。

触发斜杠菜单时,先获取光标前的文本、定位最后一个 /,再找到它对应的 DOM 位置。实现中使用 TreeWalker 遍历文本节点、累计字符长度,将文本位置映射到具体节点及偏移,然后结合光标位置计算菜单坐标。

2.3.键盘与换行:同一个按键,要区分编辑状态

Backspace 可能删除一个字符、一整张卡片或一个换行;Enter 在普通输入中表示换行,在创建新指令时则表示确认。如果正在创建的卡片内容为空,再按 Backspace,就删除这张临时卡片。

换行时,编辑器插入换行节点。如果后面没有内容,就补一个零宽字符作为光标锚点,让用户能够在新行继续输入。这个字符只服务于编辑过程,输出文本时会清理掉。

2.4.中文输入法:先完成选字,再更新搜索

中文输入不是一次性得到最终文字。拼音组合期间会触发多次 input,如果每次都更新搜索,菜单就会跟着尚未确认的内容变化,甚至因为暂时搜不到结果而关闭。

我通过组合输入的开始和结束事件区分这两个阶段:组合期间暂停搜索词更新,组合结束后再使用确认的文字更新搜索。

此外,搜索无结果时不会立即关闭菜单,而是延迟约 1 秒再判断。这是对暂时无结果状态的缓冲,不能替代对输入法组合态的判断。

2.5.DOM 规范化:复用转换,但不在每次输入时重建

浏览器可能生成额外标签,创建指令的过程也可能留下临时可编辑结构。编辑器需要把这些结构收敛回约定的"文字、卡片、换行"。

处理方式是复用双向转换:先将当前 DOM 转成文本,再由文本生成标准 HTML。但不会每次输入都重建编辑区域,只在发现异常结构时进行规范化,减少对正在进行的编辑操作的干扰。

创建新指令期间需要跳过规范化,因为此时的可编辑卡片还处在临时状态,直接按普通卡片规则重建会破坏创建过程。

2.6.拖拽:把鼠标落点变成 DOM 插入点

拖拽开始后,卡片以半透明状态显示。放下时,先将鼠标坐标转换为 DOM 中的插入位置,再克隆卡片插入、补充空格,最后删除原位置的卡片。

其中,落点计算需要适配不同浏览器。原实现对 Chrome/Safari 与 Firefox 分别处理,后续的节点插入和删除流程则遵循同一套规则。

3.实际踩坑:问题往往出在两种行为交接的地方

单独展示文字或卡片并不复杂。难点集中在它们交接的位置,以及输入、菜单、拖拽等事件相互影响的时候。

下面把具体问题和处理方式放在一起,补充前面实现机制背后的原因。

还有两个容易被忽略的时序与回退问题。

菜单尚未选中,编辑器先失焦。 点击菜单时,编辑器的失焦逻辑可能先关闭菜单,导致点击无法完成。当前实现将失焦回调延迟约 200ms,让菜单点击有机会先处理。这个时间值是本次实现的处理方式,不是所有编辑器都适用的固定规则。

局部 DOM 操作失败。 插入节点失败时,回退到"修改文本后重新渲染"的路径。这条路径用于兜底,不作为每次输入的默认方式。

这些问题说明,只完成插入和展示还不够。要让混排编辑器真正可用,必须继续处理删除、换行、输入法和事件顺序,尤其是卡片与普通文字相邻时的行为。

适用范围:什么问题值得用这套方案

这套方案适合内容类型有限、结构化引用明确、交互需要定制的编辑场景。

这些场景可以复用设计思路,但仍需按业务定义自己的标记格式、卡片状态和交互规则。

以下情况则不适合直接沿用:

  • 只需要纯文本输入

    textarea 就能满足,不必承担自定义 DOM、光标和选区的维护成本。

  • 需要完整富文本排版

    当需求包含表格、复杂样式和文档结构时,更适合评估 Slate、Quill 等成熟方案,而不是在当前实现上不断追加能力。

  • 需要多人实时协同

    当前方案没有覆盖协同编辑,也没有 OT/CRDT 等相应机制,不能直接当作协同编辑器使用。

选择它的前提,是愿意为了特定的混排交互承担编辑细节的维护,而不是认为直接使用 contentEditable 总比现成编辑器简单。

后续改进:先补验证,再整理通用能力

后续打磨主要围绕两件事:让已有行为更容易验证,让重复使用的处理逻辑更容易维护。下面都是计划中的改进,不作为已经完成的能力。

1.补齐内容转换测试

优先验证文字、卡片标记和换行的双向转换,覆盖特殊字符、连续卡片、空行以及零宽字符清理。重点检查合法内容经过"文本 → HTML → 文本"后,业务文字与卡片引用是否按约定保留。

2.补齐真实编辑交互测试

为删除、拖拽、菜单选择和新建指令补集成测试,并覆盖中文输入与不同浏览器。位置计算中不依赖 DOM 的部分可以单独测试;光标矩形、选区和拖拽落点等依赖浏览器的行为,需要在真实浏览器环境中验证。

测试应重点覆盖组合场景,例如卡片紧邻换行时删除、输入法确认后选择菜单、创建指令过程中失焦。单个操作正常,不代表这些操作连续发生时仍然正确。

3.进一步抽取转换与光标处理

将文本与 HTML 的转换逻辑拆成独立模块,把光标和选区操作整理为 Hook 或工具层,让菜单、拖拽和键盘处理复用相同的位置规则。抽取的目标是减少重复实现,并让这些规则能够被单独验证。


回到最初的需求,我们希望用户在一个输入框里,用自然语言组织任务,用卡片复用已有能力。围绕这个目标,选择了 contentEditable,完成了混排表示和编辑交互,也承担了浏览器原生编辑行为带来的复杂性。

这次实践最值得复用的,不只是卡片的展示方式,而是让文字、结构化引用、光标和输入事件在同一套规则下协同工作的方法。它决定了这个输入框是只能演示,还是能够承接连续、真实的编辑操作。

相关推荐
天竺鼠不该去劝架2 小时前
企业财务系统引入AI后会发生什么变化?从资金管理看财务智能体怎么落地
经验分享
skywalk81632 小时前
介绍一下BEAM 数据和 LoCoMo 数据:评估大模型记忆能力的数据集
人工智能·记忆
MerryLuna2 小时前
专业的EUDR机构
大数据·人工智能·python
zyseo82 小时前
谷歌SEO 站点地图优化实战:sitemap 是索引候选清单的运营工具
网络·数据库·经验分享·笔记·搜索引擎
wespaul2 小时前
VeapAI 自然语言操作实战|第 04 篇:系统数据加工——把系统自身的元数据吃成知识
java·大数据·运维·数据库·人工智能
墨云安全2 小时前
自动化安全测试工具的技术架构、集成方式与任务编排(2026 年实践)
人工智能·网络安全·渗透测试
KKKlucifer2 小时前
从人工堆标到模型驱动:数据分类分级平台的现实分化
大数据·人工智能·分类
Data analyse4562 小时前
rrweb回放事件资源加载顺序重放校验
前端·人工智能·算法·机器学习·数据分析
kida_yuan2 小时前
【岁月留声】知识库检索能力演进史
人工智能·elasticsearch