随着所有炒作的进行,人工智能(或者更确切地说是机器学习(ML)和大型语言模型(LLM))无处不在。就个人而言,我可能不会经常使用 ChatGPT(和类似的替代品),但我确实依赖GitHub Copilot之类的东西(用于 VS Code 中的智能自动完成)或Grammarly(用于编辑我的博客文章)每天。
我认为我们距离 AGI 仍有相当多的突破,而目前的技术不足以让我们到达那里(谢天谢地或不谢天谢地)。也就是说,我们已经深入到“AI 增强型”应用程序的时代,顶级应用程序可能没有最好的 AI 系统,但它们以最好的方式集成了它们。
这就是为什么这是一个有趣的过程,探索 OpenAI 的 API 并尝试将其集成到 Vrite 的富文本编辑器 (RTE) — 我的开源无头 CMS。
扩展所见即所得编辑器
对于那些不熟悉的人来说,简而言之,Vrite 是一个用于技术内容的无头 CMS,例如编程博客或软件文档。它可以看作是两个应用程序合二为一——用于内容管理的看板仪表板和用于编写的所见即所得编辑器,以及嵌入式代码片段编辑器和格式化程序等附加的开发友好功能。
Vrite 的最新重要补充是早期扩展系统,可以轻松构建集成并扩展 Vrite 的功能。对我来说,这似乎是将 ChatGPT 作为扩展引入编辑器的完美方式。
阻止操作
为了能够使用扩展系统将 ChatGPT 集成到编辑器中,必须引入一个新的 API。我称它为Block Action API,因为它专门用于向编辑器添加快速操作,对顶级内容块(如段落、标题或图像)进行操作,如下所示:
使用 Block Actions API,扩展可以读取活动块的 JSON 内容并使用 HTML 格式的内容更新它,就像在 Vrite API 中所做的一样(一方面,解析 JSON 输出更容易,另一方面,HTML 更容易)适合将内容转换成)。
从 UI 端来看,块操作显示为主动选择的块一侧的按钮。他们可以直接在点击时调用一个动作,或者——就像 ChatGPT 一样——打开一个下拉菜单来提示用户提供更多详细信息。
按钮必须绝对定位,这需要自定义TipTap扩展和更深入地挖掘底层ProseMirror(这两个库都支持 Vrite 编辑器)。
该过程基本上归结为确定块节点的位置和大小,给定整个顶级节点或仅其子节点的选择(源代码):
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-comment-color)">// ...</span>
<span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">BlockActionMenuPlugin</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">Extension</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">create</span><span style="color:var(--syntax-text-color)">({</span><span style="color:var(--syntax-comment-color)">// ...</span><span style="color:var(--syntax-name-color)">onSelectionUpdate</span><span style="color:var(--syntax-text-color)">()</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-name-color)">selection</span> <span style="color:var(--syntax-text-color)">}</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">editor</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">state</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">isTextSelection</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">selection</span> <span style="color:var(--syntax-declaration-color)">instanceof</span> <span style="color:var(--syntax-name-color)">TextSelection</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">selectedNode</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$from</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">node</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-literal-color)">1</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-error-color)">||</span> <span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$from</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">nodeAfter</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-error-color)">!</span><span style="color:var(--syntax-name-color)">selectedNode</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">box</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">style</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">display</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">none</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">return</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-name-color)">view</span> <span style="color:var(--syntax-text-color)">}</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">editor</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">node</span> <span style="color:var(--syntax-error-color)">=</span><span style="color:var(--syntax-name-color)">view</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">nodeDOM</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$from</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">pos</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-error-color)">||</span><span style="color:var(--syntax-name-color)">view</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">nodeDOM</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$from</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">pos</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$from</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">parentOffset</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-error-color)">||</span><span style="color:var(--syntax-name-color)">view</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">domAtPos</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$from</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">pos</span><span style="color:var(--syntax-text-color)">)?.</span><span style="color:var(--syntax-name-color)">node</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-error-color)">!</span><span style="color:var(--syntax-name-color)">node</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-declaration-color)">return</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">blockParent</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">getBlockParent</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">node</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">parentPos</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-text-color)">document</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">getElementById</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">pm-container</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">)</span><span style="color:var(--syntax-text-color)">?.</span><span style="color:var(--syntax-name-color)">getBoundingClientRect</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">childPos</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">blockParent</span><span style="color:var(--syntax-text-color)">?.</span><span style="color:var(--syntax-name-color)">getBoundingClientRect</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-error-color)">!</span><span style="color:var(--syntax-name-color)">parentPos</span> <span style="color:var(--syntax-error-color)">||</span> <span style="color:var(--syntax-error-color)">!</span><span style="color:var(--syntax-name-color)">childPos</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-declaration-color)">return</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">relativePos</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">top</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">childPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">top</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-name-color)">parentPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">top</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">right</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">childPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">right</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-name-color)">parentPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">right</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">bottom</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">childPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">bottom</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-name-color)">parentPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">bottom</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">left</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">childPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">left</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-name-color)">parentPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">left</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-text-color)">};</span><span style="color:var(--syntax-declaration-color)">let</span> <span style="color:var(--syntax-name-color)">rangeFrom</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$from</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">pos</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">let</span> <span style="color:var(--syntax-name-color)">rangeTo</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$to</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">pos</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-name-color)">box</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">style</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">top</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-string-color)">`</span><span style="color:var(--syntax-text-color)">${</span><span style="color:var(--syntax-name-color)">relativePos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">top</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-string-color)">px`</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-name-color)">box</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">style</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">left</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-string-color)">`</span><span style="color:var(--syntax-text-color)">${</span><span style="color:var(--syntax-name-color)">relativePos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">left</span> <span style="color:var(--syntax-error-color)">+</span> <span style="color:var(--syntax-name-color)">parentPos</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">width</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-string-color)">px`</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-name-color)">box</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">style</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">display</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">block</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">isTextSelection</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">try</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">p</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">findParentAtDepth</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">selection</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">$from</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-literal-color)">1</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-name-color)">rangeFrom</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">p</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">start</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-literal-color)">1</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-name-color)">rangeTo</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">p</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">start</span> <span style="color:var(--syntax-error-color)">+</span> <span style="color:var(--syntax-name-color)">p</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">node</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">nodeSize</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-literal-color)">1</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">}</span> <span style="color:var(--syntax-declaration-color)">catch</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">e</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">box</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">style</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">display</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">none</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-comment-color)">// ...</span><span style="color:var(--syntax-text-color)">},</span>
<span style="color:var(--syntax-text-color)">});</span></code></span></span>
替换编辑器内容
第二部分涉及处理用新提供的内容替换块内容的实际过程。这里最棘手的事情是获取块节点的正确范围(ProseMirror 中的开始和结束位置)。这是使用 TipTap 的命令正确替换范围所必需的。
如果您仔细查看最后的代码片段 — 其代码已经存在。在每次选择更新时,块的范围以及块操作 UI 定位都会更新。
用新内容实际替换范围要容易得多。它所要做的就是将 HTML 转换为遵循模式的 JSON 并涉及适当的命令(源代码):
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-comment-color)">// ...</span>
<span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">replaceContent</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">unlock</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">clear</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-name-color)">setLocked</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-declaration-color)">true</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">range</span><span style="color:var(--syntax-text-color)">())</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">let</span> <span style="color:var(--syntax-name-color)">size</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-literal-color)">0</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">nodeOrFragment</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">createNodeFromContent</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">props</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">state</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">editor</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">schema</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">nodeOrFragment</span> <span style="color:var(--syntax-declaration-color)">instanceof</span> <span style="color:var(--syntax-name-color)">PMNode</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">size</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">nodeOrFragment</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">nodeSize</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">}</span> <span style="color:var(--syntax-declaration-color)">else</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">size</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">nodeOrFragment</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">size</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-name-color)">props</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">state</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">editor</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">chain</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">focus</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">insertContentAt</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">range</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-error-color)">!</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">generateJSON</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">props</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">state</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">editor</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">extensionManager</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">extensions</span><span style="color:var(--syntax-text-color)">)</span><span style="color:var(--syntax-text-color)">)</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">scrollIntoView</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">focus</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">run</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-name-color)">setRange</span><span style="color:var(--syntax-text-color)">({</span> <span style="color:var(--syntax-name-color)">from</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">range</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-error-color)">!</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-declaration-color)">from</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">to</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">range</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-error-color)">!</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-declaration-color)">from</span> <span style="color:var(--syntax-error-color)">+</span> <span style="color:var(--syntax-name-color)">size</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-literal-color)">1</span> <span style="color:var(--syntax-text-color)">});</span><span style="color:var(--syntax-name-color)">computeDropdownPosition</span><span style="color:var(--syntax-text-color)">()();</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-name-color)">unlock</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-text-color)">};</span>
<span style="color:var(--syntax-comment-color)">// ...</span></code></span></span>
replaceContent()
然后可以通过向主框架发送适当的消息,从扩展的沙箱远程调用该函数。
为了启用像 ChatGPT 集成这样的用例,其中内容将在过程完成之前连续多次更新(即替换),该函数还会在调用函数的短时间内锁定编辑器并更新范围,和每次调用的 UI 定位。但为什么需要这样做呢?
与 OpenAI 的 API 集成
集成 OpenAI 的 API 的过程在其官方文档中有很好的记录。鉴于官方提供了SDK,整个过程只需要几行代码就可以搞定:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">async</span> <span style="color:var(--syntax-text-color)">({</span> <span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">input</span> <span style="color:var(--syntax-text-color)">})</span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">configuration</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-name-color)">Configuration</span><span style="color:var(--syntax-text-color)">({</span><span style="color:var(--syntax-name-color)">apiKey</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">fastify</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">config</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">OPENAI_API_KEY</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">organization</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">fastify</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">config</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">OPENAI_ORGANIZATION</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-text-color)">});</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">openai</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-name-color)">OpenAIApi</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">configuration</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">response</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">await</span> <span style="color:var(--syntax-name-color)">openai</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">createChatCompletion</span><span style="color:var(--syntax-text-color)">({</span><span style="color:var(--syntax-name-color)">model</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">gpt-3.5-turbo</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">messages</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">[{</span> <span style="color:var(--syntax-name-color)">role</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">user</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">input</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">prompt</span> <span style="color:var(--syntax-text-color)">}],</span><span style="color:var(--syntax-text-color)">});</span>
<span style="color:var(--syntax-text-color)">};</span></code></span></span>
现在,所有这些都是真的,但前提是您愿意等待通常为+20 秒的单个响应!这对于单个请求来说已经很多了。max_tokens
从更改服务器位置到通过限制或自定义其他参数来优化请求,都没有任何效果。这一切都归结为当前的 LLM(至少是 GPT-3 级别的)仍然相当缓慢。
话虽如此,ChatGPT 应用程序仍然设法被认为相当快速和响应迅速。这要归功于流媒体和服务器发送事件(SSE)的使用。
流式聊天 GPT 响应
OpenAI 的 API 的聊天完成和其他端点支持通过服务器发送的事件进行流式传输,本质上是保持一个开放的连接,新令牌一旦可用就会通过该连接发送。
不幸的是,官方 Node.js SDK不支持流式传输,需要您使用变通方法才能使其正常工作,从而导致需要更多代码,仅用于连接 API(源代码):
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">async</span> <span style="color:var(--syntax-text-color)">({</span> <span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">input</span> <span style="color:var(--syntax-text-color)">})</span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">configuration</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-name-color)">Configuration</span><span style="color:var(--syntax-text-color)">({</span><span style="color:var(--syntax-name-color)">apiKey</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">fastify</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">config</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">OPENAI_API_KEY</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">organization</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">fastify</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">config</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">OPENAI_ORGANIZATION</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-text-color)">});</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">openai</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-name-color)">OpenAIApi</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">configuration</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">response</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">await</span> <span style="color:var(--syntax-name-color)">openai</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">createChatCompletion</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">model</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">gpt-3.5-turbo</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">stream</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-declaration-color)">true</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">messages</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">[{</span> <span style="color:var(--syntax-name-color)">role</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">user</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">input</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">prompt</span> <span style="color:var(--syntax-text-color)">}],</span><span style="color:var(--syntax-text-color)">},</span><span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-name-color)">responseType</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">stream</span><span style="color:var(--syntax-string-color)">'</span> <span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">res</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">raw</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">writeHead</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-literal-color)">200</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-text-color)">...</span><span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">res</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">getHeaders</span><span style="color:var(--syntax-text-color)">(),</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">content-type</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">text/event-stream</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">cache-control</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">no-cache</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">connection</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">keep-alive</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-text-color)">});</span><span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-text-color)">Promise</span><span style="color:var(--syntax-error-color)"><</span><span style="color:var(--syntax-declaration-color)">void</span><span style="color:var(--syntax-error-color)">></span><span style="color:var(--syntax-text-color)">((</span><span style="color:var(--syntax-name-color)">resolve</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">responseData</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">response</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">data</span> <span style="color:var(--syntax-declaration-color)">as</span> <span style="color:var(--syntax-name-color)">unknown</span> <span style="color:var(--syntax-declaration-color)">as</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">on</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">event</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-declaration-color)">string</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">data</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">data</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-declaration-color)">string</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-declaration-color)">void</span><span style="color:var(--syntax-text-color)">):</span> <span style="color:var(--syntax-declaration-color)">void</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">};</span><span style="color:var(--syntax-name-color)">responseData</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">on</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">data</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">data</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">lines</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">data</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">toString</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">split</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-literal-color)">\n</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">)</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">filter</span><span style="color:var(--syntax-text-color)">((</span><span style="color:var(--syntax-name-color)">line</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-name-color)">line</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">trim</span><span style="color:var(--syntax-text-color)">()</span> <span style="color:var(--syntax-error-color)">!==</span> <span style="color:var(--syntax-string-color)">''</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-declaration-color)">for</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">line</span> <span style="color:var(--syntax-declaration-color)">of</span> <span style="color:var(--syntax-name-color)">lines</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">message</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">line</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">replace</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">/^data: /</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-string-color)">''</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">message</span> <span style="color:var(--syntax-error-color)">===</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">[DONE]</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">res</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">raw</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">end</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-name-color)">resolve</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-declaration-color)">continue</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-declaration-color)">try</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">parsed</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">JSON</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">parse</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">message</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">content</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">parsed</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">choices</span><span style="color:var(--syntax-text-color)">[</span><span style="color:var(--syntax-literal-color)">0</span><span style="color:var(--syntax-text-color)">].</span><span style="color:var(--syntax-name-color)">delta</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">content</span> <span style="color:var(--syntax-error-color)">||</span> <span style="color:var(--syntax-string-color)">''</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">res</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">raw</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">write</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">`data: </span><span style="color:var(--syntax-text-color)">${</span><span style="color:var(--syntax-text-color)">encodeURIComponent</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">)}</span><span style="color:var(--syntax-string-color)">`</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-name-color)">ctx</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">res</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">raw</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">write</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-literal-color)">\n\n</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-text-color)">}</span> <span style="color:var(--syntax-declaration-color)">catch</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">error</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">console</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">error</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">Could not JSON parse stream message</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">message</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">error</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-text-color)">});</span><span style="color:var(--syntax-text-color)">});</span>
<span style="color:var(--syntax-text-color)">};</span></code></span></span>
最重要的是,你还必须在你的 API 服务器和 Web 客户端之间支持流式传输,对于 Vrite,这意味着将 SSE 与Fastify和tRPC集成。不是最干净的解决方案,但仍然非常稳定。
从前端(准确地说是扩展沙箱),必须建立与新流端点的连接并正确处理传入数据(源代码):
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">import</span> <span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-name-color)">fetchEventSource</span> <span style="color:var(--syntax-text-color)">}</span> <span style="color:var(--syntax-declaration-color)">from</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">@microsoft/fetch-event-source</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-comment-color)">// ...</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">generate</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">async</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">ExtensionBlockActionViewContext</span><span style="color:var(--syntax-text-color)">):</span> <span style="color:var(--syntax-text-color)">Promise</span><span style="color:var(--syntax-error-color)"><</span><span style="color:var(--syntax-declaration-color)">void</span><span style="color:var(--syntax-error-color)">></span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">includeContext</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">temp</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">includeContext</span> <span style="color:var(--syntax-declaration-color)">as</span> <span style="color:var(--syntax-name-color)">boolean</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">prompt</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">temp</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">prompt</span> <span style="color:var(--syntax-declaration-color)">as</span> <span style="color:var(--syntax-declaration-color)">string</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-declaration-color)">let</span> <span style="color:var(--syntax-name-color)">content</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-string-color)">""</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">setTemp</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">$loading</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-declaration-color)">true</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-text-color)">window</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">currentRequestController</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-name-color)">AbortController</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-text-color)">window</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">currentRequestController</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">signal</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">addEventListener</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">abort</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">()</span> <span style="color:var(--syntax-error-color)">=></span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">setTemp</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">$loading</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-declaration-color)">false</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">refreshContent</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-text-color)">});</span><span style="color:var(--syntax-declaration-color)">await</span> <span style="color:var(--syntax-name-color)">fetchEventSource</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">https://extensions.vrite.io/gpt</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">method</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">POST</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-name-color)">headers</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">Content-Type</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">application/json</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">Accept</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">text/event-stream</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">},</span><span style="color:var(--syntax-name-color)">body</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">JSON</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">stringify</span><span style="color:var(--syntax-text-color)">({</span><span style="color:var(--syntax-name-color)">prompt</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">includeContext</span> <span style="color:var(--syntax-text-color)">?</span> <span style="color:var(--syntax-string-color)">`"</span><span style="color:var(--syntax-text-color)">${</span><span style="color:var(--syntax-name-color)">gfmTransformer</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">)}</span><span style="color:var(--syntax-string-color)">"\n\n</span><span style="color:var(--syntax-text-color)">${</span><span style="color:var(--syntax-name-color)">prompt</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-string-color)">`</span> <span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">prompt</span><span style="color:var(--syntax-text-color)">}),</span><span style="color:var(--syntax-name-color)">signal</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">window</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">currentRequestController</span><span style="color:var(--syntax-text-color)">?.</span><span style="color:var(--syntax-name-color)">signal</span><span style="color:var(--syntax-text-color)">,</span><span style="color:var(--syntax-declaration-color)">async</span> <span style="color:var(--syntax-name-color)">onopen</span><span style="color:var(--syntax-text-color)">()</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">return</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">},</span><span style="color:var(--syntax-name-color)">onerror</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">error</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">setTemp</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">$loading</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-declaration-color)">false</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">refreshContent</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">notify</span><span style="color:var(--syntax-text-color)">({</span> <span style="color:var(--syntax-name-color)">text</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">Error while generating content</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">type</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">error</span><span style="color:var(--syntax-string-color)">"</span> <span style="color:var(--syntax-text-color)">});</span><span style="color:var(--syntax-declaration-color)">throw</span> <span style="color:var(--syntax-name-color)">error</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-text-color)">},</span><span style="color:var(--syntax-name-color)">onmessage</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">event</span><span style="color:var(--syntax-text-color)">)</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">partOfContent</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-text-color)">decodeURIComponent</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">event</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">data</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-name-color)">content</span> <span style="color:var(--syntax-error-color)">+=</span> <span style="color:var(--syntax-name-color)">partOfContent</span><span style="color:var(--syntax-text-color)">;</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">replaceContent</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">marked</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">parse</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">content</span><span style="color:var(--syntax-text-color)">));</span><span style="color:var(--syntax-text-color)">},</span><span style="color:var(--syntax-name-color)">onclose</span><span style="color:var(--syntax-text-color)">()</span> <span style="color:var(--syntax-text-color)">{</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">setTemp</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">$loading</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-declaration-color)">false</span><span style="color:var(--syntax-text-color)">);</span><span style="color:var(--syntax-name-color)">context</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">refreshContent</span><span style="color:var(--syntax-text-color)">();</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-text-color)">});</span>
<span style="color:var(--syntax-text-color)">};</span>
</code></span></span>
不幸的是,用于处理 SSE 的EventSource Web API(内置于大多数现代浏览器中)仅支持GET
请求,这在需要POST
具有较大 JSON 数据的请求时非常有限。body
作为替代方案,您可以使用 Fetch API 或现成的库,例如Microsoft 的 Fetch Event Source。
同样,启用流式传输后,您现在将在可用时立即收到新令牌。鉴于 OpenAI 的 API 在其响应格式中使用 Markdown,因此需要将传入的令牌放在一起并解析为函数接受的 HTML 的完整消息replaceContent
。为此,我使用了Marked.js 解析器。
现在,随着每个新令牌的出现,更大的响应正在建立。每次出现新的标记时,都会解析完整的 Markdown 并更新内容,从而形成一种很好的“类似打字的效果”。
虽然这个过程确实有一些开销,但在使用中并不明显,而 Markdown 只需用每个新标记进行解析,因为它可能包含例如代码块的结束或格式化段的结尾。因此,虽然这个过程可能会被优化,但在大多数情况下它不会导致任何可识别的性能改进。
最后,值得注意的是 的使用AbortController
,它可用于在用户选择的任何时间停止流。这对于较长的响应尤其有用。
底线
总的来说,我对结果很满意。得益于 Markdown 解析,数据流、良好的打字效果以及与编辑器现有内容块的良好集成——所有这些共同创造了引人注目的用户体验。
现在,肯定还有改进的余地。Block Actions API 以及 Vrite Extensions 作为一个整体,在它们可以被其他用户创建之前,还有很多开发工作要做。其他需要考虑的 UI/UX 改进,比如一次在多个块上操作(例如,用于 ChatGPT 的额外上下文)和内联显示 UI(很像 Notion AI)而不是模糊视图,这些只是我正在考虑的几个例子。也就是说,要很好地实施这些想法还需要一些时间。
Vrite 不仅仅是一个 GPT 增强型编辑器。它是一个完整的开源 CMS,专注于编程博客等技术内容,包括代码编辑器、API、看板管理仪表板和简单的发布集成。因此,如果您有兴趣尝试并可能使用它来为您的博客提供动力,请一定要检查一下!