跳转到内容

AI Rich Editor 使用指南

包内只给协议,接入点由宿主任选其一,经顶层 chat 属性传入:

  • 传字符串 —— 已鉴权的 OpenAI Chat Completions 端点,包内负责请求与 SSE 解析
  • 传函数 —— 自定义适配器,接收包内拼装好的协议请求,流式返回增量

请求由包内组装(system 提示词恒为首条,历史轮次的上下文块已剥离):

interface AiRichChatRequest {
messages: Array<{
role: 'system' | 'user' | 'assistant';
content:
| string
| Array<
| { type: 'text'; text: string }
| { type: 'image_url'; image_url: { url: string } }
>;
}>;
}

响应由接入方流式返回:

interface AiRichChatChunk {
content?: string; // 正文增量
reasoning?: string; // 思考增量
finishReason?: string; // 结束原因
}
// 一:已鉴权的 OpenAI 端点在宿主自己的鉴权体系内 —— 直接给 URL
// 请求体固定 { messages, stream: true },不带 model(模型由端点侧决定)
<AiRichEditor chat="/api/ai/chat" />
// 二:自己的 server function / 需要自定义请求头与 model / 非 OpenAI 协议 —— 给函数
<AiRichEditor
chat={async function* ({ messages }, signal) {
const res = await fetch('/my/chat', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ model: 'my-model', messages }),
signal,
});
if (!res.ok || !res.body) throw new Error('请求失败');
for await (const chunk of parseMyProtocol(res.body)) yield chunk;
}}
/>
  • 终止:字符串接入以 [DONE] 或 finish_reason 为准,两者都缺失即判定流被切断并报错;函数接入迭代器正常结束即完成,异常中断请自行抛错
  • 思考:字符串接入按厂商方言尽力识别 reasoning_content / reasoning / thinking,渲染为可折叠思考块
  • 图片:绝对 http(s) 地址且 config.sendImagesAsMultimodal 开启时作为 image_url 内容块发送;其余附件靠正文里的 [已上传附件] 清单传递地址
  • 中断:stop 中止当前生成并静默退出
  • 错误:字符串接入的 HTTP 非 2xx、流内 {"error":…} 与截断都会抛出 Error,函数接入的错误原样上抛,一律走 onNotify + onError 两条通道

每条用户消息都附带 [当前片段](源片段);构造请求时历史轮次的该块会被剥离,只保留最新一份,避免请求体随对话线性膨胀。[文档内容](Word / PDF 解析结果)是持久源材料,不参与剥离。

修改类请求把当前片段一并发送,并明确要求:只做最小化改动,但输出改动后的完整片段。

  • 每条助手回复都是一个完整可应用的版本,按时间线留在对话里;点任意历史代码卡片的「应用到编辑器」即可回退到该版本
  • 代码面板中的内容也可随时手改,AI 下一次整段替换前会读到最新内容
  • 若模型仍违规输出 Search / Replace 差异块,包内只渲染为 diff 卡片并提供手动「应用修改」,不自动应用,并提示重新索要完整片段

媒体能力经顶层 media 属性注入(不在 config 里:上传与列表都是函数,设置面板无法编辑)。

<AiRichEditor
chat="/api/ai/chat"
media={{
image: {
upload: async (file, onProgress) => ({
id: file.name,
url: await myUpload(file, onProgress),
name: file.name,
size: file.size,
}),
getList: async ({ page, pageSize, keyword }) => ({ items, total }),
},
video: { upload, getList },
audio: { upload, getList },
attachment: { upload },
}}
/>

MediaItem 等类型与 @easyx/editor 共用同一套媒体契约,宿主接口可复用。

两条插入路径:

  1. 对话:把文件粘贴 / 拖入输入框,或点回形针从上传与媒体库中选择。点发送时才调用对应类型的 upload,拿到地址后把 [已上传附件] 清单拼进消息,由模型决定插到哪里、如何书写标签。上传失败不发送,输入与附件保留可重试。
  2. 代码面板:行号左侧、跟随光标所在行的「+」打开同一套浮层;也可把文件直接拖进代码区,按落点插入。

插入的是自包含片段:

  • 图片:<img src alt style="max-width:100%;height:auto;">
  • 视频:<video src controls playsinline style="max-width:100%;"></video>
  • 音频:<audio src controls></audio>
  • 附件:<a href download>文件名(尺寸)</a>

未配置 upload 的类型不可上传,添加那一刻即报错。「媒体库」入口由 getList 决定是否出现,列表取自第一个配置了 getList 的类型(媒体库通常是一份共享资源,条目类型按 fileType 或扩展名逐个推断)。

来源是否校验
用户手输(「网络地址」页签)校验
AI 回复里的链接与图片校验
宿主 upload / getList 返回的地址不校验(宿主服务端产物,属可信来源)

规则是协议白名单:默认放行 http(s) / mailto / tel / blob 与相对路径,拦下 javascript: / data: 等。blob: 放行是因为它只能由同源脚本现场铸造,无法像 data: 那样从字符串直接构造。

宿主可用 allowedUrlSchemes 追加协议(只增不减;危险协议写进去也会被忽略):

<AiRichEditor allowedUrlSchemes={['ipfs:', 'app:']} />

可序列化配置统一收拢到 config,经顶栏「设置」模态框编辑,保存后生效:

配置项类型默认值说明
autoApplybooleantrue回复结束后自动应用到编辑器(仍保留手动按钮)
previewEditMenubooleantrue预览区右键「用 AI 修改」入口
systemPromptstring内置模板自定义 system 提示词,作为 messages[0] 发送
previewHeadstring—预览 <head> 附加代码(原始 HTML)
sendImagesAsMultimodalbooleantrue图片附件以多模态 content parts 发送

函数型注入项(media / tools / onNotify / onError)与 allowedUrlSchemes 一律是顶层属性,不进 config。

设置模态框就地渲染在编辑器容器内(不 portal),高度随内容自适应、上限为容器的 90%,宽度上限 800px,只承载可编辑的可序列化项。

两条通道分开,均为顶层属性;错误会同时走两条 —— 一条给人看,一条给程序看:

通道类型承载未注入时的兜底
onNotifyAiRichNotifyHandler用户可见文案(成功 / 提醒 / 所有错误提示)包内置轻提示
onErrorAiRichErrorHandler错误实例,供日志 / 上报 / 分支console.error(不上浮 UI)
<AiRichEditor
chat="/api/ai/chat"
onNotify={(type, content) => myToast(type, content)}
onError={(error) => myReporter(error)}
/>
  • 错误提示文案取自 error.message,宿主无需自行翻译
  • onError 收到的是 Error,可按类分支:error instanceof MediaNotConfiguredError / InvalidMediaUrlError
  • 兜底刻意做得很轻:通知用包内轻提示,错误只打 console.error,绝不弹原生 alert
  • 会话流错误在对话区保留 Alert(属对话状态),同时也会走两条通道

亮暗判定优先级(与包内样式一致):

  1. 令牌作用域 class easyx-ai-rich-editor-scope-dark
  2. 最近的宿主 [data-theme] 祖先(值以 dark 结尾,如 dark、admin-dark)
  3. 两者都没有时跟随系统 prefers-color-scheme

代码面板的语法高亮与结构配色全部走 --easyx-ai-rich-editor-code-* 令牌(跟随上述判定,无需 JS)。浮层(菜单 / Tooltip / 媒体选择 / 轻提示)以 portal 渲染,包内会把触发元素所处的主题一并带到浮层根上,因此浮层不会脱主题。

所有颜色经 --easyx-ai-rich-editor-* CSS 变量控制,可在宿主覆盖。

在预览里选中文字,或右键某个区块,即可就地描述修改:

  • 右键弹出行内输入框(Enter 发送 / Shift+Enter 换行 / Esc 取消)
  • 目标优先级:有文字选区时以选中文本为焦点,无选区时以右键所在的最内层元素为单位;元素原文在片段中不唯一时沿父链向上扩张到唯一祖先
  • 多选:对话框打开后按 Shift + 右键 增减目标(再次点已选元素即移除),用于「让这两个一致」这类关系型改动
  • 发送的消息为每个目标附一个 [目标区域] 块,并带上该元素的关键计算样式摘要(color / font-size / display / margin 等实际生效值)与 [选中文本];回复仍是改动后的完整片段
  • 预览文档由包内注入 data-easyx-id 编号做命中映射,不向 iframe 注入脚本;父页经 allow-same-origin 读取 DOM,读不到时静默降级
  • 对话框打开期间以覆盖层保持全部目标高亮(原生选区在父页抢焦点后会被浏览器隐藏)
  • 可用 config.previewEditMenu: false 关闭
  • 助手消息的 markdown 走「marked 词法 → React 元素」自研渲染:markdown 里的裸 HTML 一律丢弃,链接协议白名单校验,全程不经 dangerouslySetInnerHTML
  • 完整片段渲染为 HtmlCodeCard(可回退并重新应用);Search / Replace 差异块渲染为 PatchCard(删除行红底 / 新增行绿底 + 手动「应用修改」)
  • 用户消息按上下文块分段渲染:原话 + 附件缩略条 + [当前片段] / [目标区域] 只读代码卡片
  • 流式生成中,出现已闭合的完整片段即同步到编辑器与预览;未闭合的半成品不落地,避免残缺 HTML 污染文档
  • 外部写入(流式同步 / 应用片段 / 手动应用补丁)与用户输入区别对待:按公共前后缀求最小改动区间,只替换变化段;不进撤销栈,也不回吐 onChange