Lexical AI Agent 示例深度解析:基于 transformers.js 的纯浏览器端 AI 富文本编辑器

发布时间:2026/9/12 5:42:06
Lexical AI Agent 示例深度解析:基于 transformers.js 的纯浏览器端 AI 富文本编辑器 Lexical AI Agent 示例深度解析基于 transformers.js 的纯浏览器端 AI 富文本编辑器【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical导读本文围绕 Lexical 仓库中的 agent-example 示例 展开讲解如何在一个 React Tailwind CSS 的 Lexical 富文本编辑器中接入完全运行在浏览器端WebAssembly / Web Worker的 AI 能力包括基于小型语言模型的流式文本续写AI Generate、基于命名实体识别NER的实体抽取与交互式装饰节点Extract Entities、以及一键中止Abort机制。读完本文你将掌握在 Lexical 扩展体系Extension API下集成 transformers.js、协调 Worker 消息、实现 token 级流式写入与结构化节点替换的完整实战方案。一、示例概览浏览器内的 AI 编辑器agent-example是 Lexical 官方示例集中展示AI Agent 功能的参考实现。它基于同仓库的 website-toolbar 示例 扩展而来在原有富文本编辑能力之上叠加了三条 AI 能力AI GenerateAI 生成点击工具栏 Generate 按钮AI 会基于当前文档上下文续写一个新段落并逐 token 流式写入编辑器Extract Entities实体抽取检测文本中的人名PER、地点LOC、机构ORG将其替换为带颜色编码的交互式装饰节点PlaceNode 链接到 Google MapsPersonNode 与 OrgNode 链接到 Google 搜索Abort中止点击 Stop 按钮或按下Escape键即可取消正在进行的 AI 操作同时保留完整的富文本能力标题、引用等块类型加粗/斜体/下划线格式化对齐方式撤销重做以及暗色模式切换。核心架构决策是AI 完全在浏览器本地运行不依赖任何服务端。两个模型通过huggingface/transformers在 Web Worker 中以 WASM 后端加载执行从而获得包括 iOS Safari 在内的广泛兼容性。二、技术底座两个模型与懒加载策略2.1 模型选型示例使用两个模型见 ai-worker.ts 中的加载逻辑模型用途量化/大小加载配置HuggingFaceTB/SmolLM2-135M-Instruct文本生成q4 量化约 70MBpipeline(text-generation, ..., {device: wasm, dtype: q4})Xenova/bert-base-NER命名实体识别人/地/机构q8 量化pipeline(token-classification, ..., {device: wasm, dtype: q8})两个模型均使用WASM 设备后端执行在 ai-worker.ts 中device: wasm明确指定了推理后端同时通过progress_callback将下载/加载进度回传主线程驱动界面上的加载进度条。2.2 懒加载与进度上报模型在首次使用时才加载getGenerator()/getNERClassifier()均先检查模块级缓存变量为空才创建 pipeline。加载过程中Worker 通过self.postMessage({status: loading-model | model-ready | loading-ner | ner-ready, type: status, progress})向主线程上报状态主线程侧 AIExtension.ts 将状态映射为modelStatus信号取值idle | loading | ready | error与loadProgress信号供 UI 渲染Loading model 42%…及进度条。注意SmolLM2-135M 属于小型语言模型生成结果属于尽力而为本示例的定位是演示浏览器内 AI 与 Lexical 的集成方式而非生产级 AI 写作质量。三、核心机制AI 与 Lexical 的扩展式集成本示例全部 AI 逻辑被封装为一个 Lexical 扩展AIExtension定义见 AIExtension.ts并通过defineExtension挂载自定义节点依赖AICaretNodeExtension、EntityNodeExtension。这是 Lexical 0.50 版本引入的 Extension API 的典型用法。3.1 编辑器的扩展组合在 Editor.tsx 中编辑器由一组扩展声明式组合而成function createEditorExtension(createWorker: () Worker) { return defineExtension({ $initialEditorState: () { $getRoot().append( $createParagraphNode().append($createTextNode(SAMPLE_TEXT)), ); }, dependencies: [ RichTextExtension, HistoryExtension, TabIndentationExtension, configExtension(AIExtension, {createWorker}), ToolbarExtension, ], name: lexical/agent-example/editor, namespace: lexical/agent-example/editor, theme, }); }configExtension(AIExtension, {createWorker})用于向 AI 扩展注入 Worker 工厂——这是可测试性的关键设计测试时可替换为 mock Worker$initialEditorState预置了一段包含大量人名、城市、机构的示例文本SAMPLE_TEXT方便直接体验实体抽取效果LexicalExtensionComposer负责装配整个扩展树见 Editor.tsx。3.2 Worker 消息协议主线程与 Worker 之间通过结构化消息通信协议包含以下类型见 AIExtension.ts 与 ai-worker.ts请求主线程 → Workergenerate携带id、messages、maxTokens、stopAt、type、extract-entities携带id、text、entityTypes、abort响应Worker → 主线程status模型加载/生成状态、token流式生成的一个 token、done生成完成携带fullText、aborted、entities携带实体数组、error。主线程用pending与entityPending两个 Map 按请求id关联 Promise实现请求-响应的异步编排AIExtension.ts。3.3 从 React 组件消费 AI 能力React 侧通过自定义 Hook useAI.ts 读取扩展输出export function useAI(): UseAIReturn { const ai useExtensionDependency(AIExtension).output; const isGenerating useSignalValue(ai.isGenerating); const modelStatus useSignalValue(ai.modelStatus); const loadProgress useSignalValue(ai.loadProgress); return { abort: ai.abort, handleExtractEntities: ai.handleExtractEntities, handleGenerate: ai.handleGenerate, isGenerating, loadProgress, modelStatus, }; }useExtensionDependency获取扩展输出useSignalValue订阅响应式信号。工具栏 ToolbarExtension.tsx 中的 AI 按钮区据此渲染Generate、Extract Entities琥珀色与生成中的红色 Stop 按钮当isGenerating或modelStatus loading时按钮被禁用aiDisabled。四、AI Generate流式续写的完整链路4.1 Prompt 构造buildGenerateMessagesAIExtension.ts根据文档当前内容决定 Prompt文档非空时system 消息为You are a writing assistant. Continue the text naturally with one new paragraph...user 消息携带Continue this text with one paragraph:\n\n${context}文档为空时退化为Write an opening paragraph的引导场景。上下文context来自editor.read(() $getRoot().getTextContent())。4.2 AI 光标AICaretNode生成开始前handleGenerate在文档末尾追加一个段落并插入AICaretNode作为流式写入的锚点AIExtension.ts。该节点定义见 AICaretNode.ts是一个DecoratorNodeisInline()返回true可内联于段落中getTextContent()返回空字符串不污染文本内容createDOM()渲染一个animate-pulse的竖条h-[1em] w-0.5 rounded-full bg-indigo-500模拟闪烁光标exportDOM()返回{element: null}导出时被丢弃通过data-ai-caret-node属性标记便于定位。4.3 token 级流式写入Worker 端使用TextStreamer逐 token 回调ai-worker.ts配置skip_prompt: true与skip_special_tokens: true过滤 prompt 与特殊 token同时设置stopAt: \n\n段落结束标志实现生成一段即停const streamer new TextStreamer(gen.tokenizer, { callback_function: (token: string) { if (abortController.signal.aborted) return; accumulated token; if (stopAt accumulated.includes(stopAt)) { stoppedEarly true; abortController.abort(); // 提前停止保存已生成的段落 return; } self.postMessage({id, token, type: token}); }, skip_prompt: true, skip_special_tokens: true, }); const output await gen(messages, { do_sample: true, max_new_tokens: maxTokens || 256, streamer, temperature: 0.7, });生成参数固定为do_sample: true、temperature: 0.7、max_new_tokens: 256。每个token消息到达主线程后$appendTokenBeforeCaretAIExtension.ts在 AI 光标之前插入文本节点普通文本用$createTextNode换行符转为$createLineBreakNode制表符转为$createTabNode实现真正的边生成边显示效果。所有写入更新都带tag: AI_STREAM_TAG结束/清理更新带AI_GENERATE_END_TAG便于历史记录按语义分组。生成结束后finally块$removeAICaret会先选中光标前一个位置再移除 AI 光标节点避免留下孤儿节点AIExtension.ts。4.4 中止机制Abort中止存在两条路径双向配合主线程路径点击 Stop 按钮调用abort()向 Worker 发送{type: abort}消息并清理pending、tokenCallback、activeId将isGenerating置为falseAIExtension.ts键盘路径AIExtension.register中用effect监听isGenerating信号生成期间动态注册KEY_ESCAPE_COMMAND处理器COMMAND_PRIORITY_LOWEscape 触发同样的abort()AIExtension.ts。Worker 端维护activeAbortController收到abort消息后调用controller.abort()终止推理ai-worker.ts。另外新请求会自动中止仍在进行的旧请求// Abort any previous in-flight request逻辑保证同一时刻只有一个生成任务。五、Extract Entities从 NER 结果到交互式装饰节点5.1 文本偏移映射$collectTextNodeOffsetsNER 模型返回的是字符级偏移而 Lexical 的内容分布在各 TextNode 中。由于$getRoot().getTextContent()在段落间插入\n\n会导致偏移对不上号示例专门实现了$collectTextNodeOffsetsextractEntityNodes.ts递归遍历元素树将每个 TextNode 的内容拼接为平铺字符串块级边界之间仅以单个空格分隔同时记录每个 TextNode 在平铺串中的start、length、key——这样 NER 的字符偏移可以直接映射回 TextNode无需脆弱的换行算术。5.2 偏移重建与 BIO 合并transformers.js 的 token-classification pipeline 目前不提供字符偏移库内 TODO因此 mergeEntities.ts 自行重建computeTokenOffsets按顺序在原文中查找每个 token 的word非子词 token 要求词边界前一个字符非字母数字WordPiece 续接 token##前缀直接从游标处继续从而正确处理被拆分的名字如G、##anna、##way→GannawaymergeEntities将 BIO 标签B-LOC起始、I-LOC延续合并为连续实体 span子词 token 即使被模型误标为B-也会并入前一个实体如 Maksim 被拆成多个B-PER子词的情况最终输出{text, start, end, entity, score}结构score取段内 token 的最小置信度。5.3 批量替换$replaceTextWithEntityNodes得到实体 span 后handleExtractEntitiesAIExtension.ts请求 Worker 抽取[LOC, PER, ORG]三类实体然后在editor.update中调用$replaceTextWithEntityNodes完成替换按实体所属 TextNode 分组通过偏移区间entity.start tn.start entity.end nodeEnd判断归属对每个 TextNode 一次性计算所有切分点用单次splitText得到稳定片段引用按 start 升序记录partOffsets映射然后逆序遍历实体执行替换保证数组索引始终有效替换工厂replaceWithEntityextractEntityNodes.ts会把源 TextNode 的文本格式加粗/斜体/下划线拷贝给实体节点后再replace。5.4 实体节点EntityNodeEntityNodeEntityNode.tsx继承自DecoratorTextNode通过createState声明entityText与entityType两个扁平 state实现可序列化文本与类型exportDOM()将其导出为带data-entity-type属性的spanimportDOM则通过buildImportMap在粘贴/导入时反向还原。每种实体类型在ENTITY_STYLES表中配置了独立样式与链接策略EntityNode.tsx类型视觉样式链接目标LOC翠绿色系Google Maps 搜索https://www.google.com/maps/search/?api1query...ORG紫色系Google 搜索PER蓝色系Google 搜索渲染层EntityDecorator输出a标签包含对应 SVG 图标、title提示、虚线边框并自动叠加主题中定义的加粗/斜体/下划线 class从editor._config.theme.text读取保证实体节点与普通文本的格式视觉一致。替换操作统一标记tag: AI_ENTITIES_TAG。5.5 单元测试保障实体替换逻辑有完整的单元测试支撑extractEntityNodes.test.ts617 行含多节点偏移收集、跨节点实体归属、切分与替换断言与 mergeEntities.test.ts。前者甚至定义了最小化的TestEntityNode继承DecoratorTextNode用于在无 React 环境下验证替换行为。测试通过pnpm testvitest运行。六、本地运行与工程配置6.1 启动命令pnpm i pnpm run dev依赖安装与开发服务器启动后Vite 会加载src/main.tsx入口浏览器打开即可体验。示例还支持pnpm build生产构建、pnpm preview预览构建产物、pnpm testvitest 单元测试与pnpm typechecktsc 类型检查见 package.json 的 scripts 字段。6.2 依赖构成运行时依赖package.jsonhuggingface/transformers^4.0.1模型加载与推理lexicallexical/extensionlexical/reactlexical/rich-textlexical/historylexical/selectionlexical/utils均 0.50.0编辑器内核与扩展体系react/react-dom^19.2.5。开发依赖方面Tailwind CSS v4 通过tailwindcss/vite插件集成onnxruntime-node与sharp被指向 stubs/empty 空桩包避免在纯浏览器场景下误装原生依赖。6.3 Worker 的创建与注入默认 Worker 工厂defaultCreateWorkerAIExtension.ts使用 Vite 的new URL语法创建模块 Workerexport function defaultCreateWorker(): Worker { return new Worker(new URL(./ai-worker.ts, import.meta.url), { type: module, }); }AIExtension的config默认值即{createWorker: defaultCreateWorker}编辑器层可通过configExtension(AIExtension, {createWorker})覆盖这是测试与定制 Worker 的入口。Worker 生命周期由扩展的dispose()管理——register中通过mergeRegister注册output.dispose.bind(output)编辑器销毁时调用worker.terminate()释放资源。七、示例文本与体验路径编辑器预置的 SAMPLE_TEXTEditor.tsx是一段精心构造的富实体文本涵盖伦敦、旧金山、纽约、墨尔本、温哥华等地点Dominic Gannaway、Bob Ippolito、Ivaylo Pavlov 等人名以及 Meta、Bloomberg、Figma、Atticus 等机构。推荐的体验路径直接点击Extract Entities观察人名/地名/机构被替换为三种颜色的可点击链接节点清空或保留文本后点击Generate观察新段落以光标动画逐字流入编辑器生成过程中点击Stop或按Escape验证即时中止点击实体链接验证 Google Maps / Google 搜索的跳转行为切换暗色模式确认实体配色在深色背景下同样可读。八、可复用的设计要点从源码结构看这个示例提供了几条可直接迁移到自有 Lexical 项目的架构经验AI 与编辑器解耦所有推理逻辑收敛在 Worker 与AIExtension内部React 层只通过useAI暴露的四个动作abort、handleExtractEntities、handleGenerate、isGenerating交互流式渲染用装饰节点做锚点AICaretNode既是视觉光标又是文本插入的稳定参照物避免流式 token 更新与用户光标互相干扰偏移映射是 NER 与编辑器对接的关键$collectTextNodeOffsets用单空格拼接解决了\n\n偏移错位问题splitText单次切分 逆序替换保证了索引稳定协议化消息 Promise 化编排Worker 消息按id关联 pending Promise天然支持并发请求、超时与中止请求级 AbortController新请求自动取消旧请求、stopAt早停与用户主动中止共享同一套取消机制。这些模式在 website-toolbar、website-chat 等其他示例以及 packages/lexical-extension 的扩展机制中都能找到呼应可作为构建浏览器内 AI 编辑器的起点参考。【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考