Skip to content

AI 组件 ​

概述 ​

AI 组件提供选区 / 整块的 AI 交互面板,内置 11 种动作(续写、改写、扩写、精简、摘要、翻译、校对、正式语气、友好语气、生成标题、生成大纲)。默认由内置 Mock 流式输出驱动,可完整走通交互;接入真实模型只改 ai 一项配置,不需要写传输层、不需要写渲染逻辑。

接入分三档,按宿主的能力和诉求选一档即可:

档次写法谁负责发请求谁负责显示适合
L1 只给地址ai: '/api/ai'组件内置(XHR + SSE 解析)组件内置后端自己封装了 AI 网关
L2 直连模型ai: { protocol: 'openai', baseURL, apiKey, model }组件内置组件内置有 OpenAI 兼容服务,想省掉后端
L3 自管传输ai: { stream(args) { … } }宿主组件内置已有 SDK / 签名 / 加密 / 特殊协议

ai 的三档写法会在内部归一化成同一个 { kind, protocol, contentFormat, stream(args) } 契约,下游只认这一种形态,因此三档可以随时升降级,不用改调用代码。

配置内容 ​

方式一:只给接口地址(推荐,最省事) ​

ts
import { LarkDoc } from 'lark-doc'

new LarkDoc({
  element: '#editor',
  features: { ai: true },
  // 字符串等价于 { endpoint: '…', protocol: 'auto' }
  ai: '/api/ai',
})

需要显式指定协议时写成对象:

ts
new LarkDoc({
  element: '#editor',
  ai: {
    endpoint: '/api/ai',
    protocol: 'auto',        // 'auto' | 'openai' | 'chatStream' | 'json'
    headers: () => ({ Authorization: `Bearer ${getToken()}` }),
    contentFormat: 'markdown', // 结果按 markdown 渲染并落正文
  },
})

后端契约 ​

组件发出的请求体是扁平参数,action 就是宿主区分「续写 / 润色 / 翻译」的依据:

http
POST /api/ai
Content-Type: application/json;charset=UTF-8
Accept: text/event-stream, application/json, text/plain, */*
json
{
  "action": "rewrite",
  "text": "待处理的正文",
  "prompt": "用户在面板里补的要求,可空",
  "language": "zh",
  "range": { "from": 12, "to": 48, "source": "selection" }
}

响应支持三种形态,组件会自动识别,后端改成哪种都不用改前端:

形态响应体识别方式
SSE 流式data: {"content":"你"} 逐帧,空行分隔,末尾 data: [DONE]首块出现 data: / event: / id: 行
整包 JSON{"content":"完整结果"}首块是 { 或 [ 且能整体 JSON.parse
纯文本直接吐字首块含换行,或长度超过 4KB

SSE 帧里的文本字段名不固定,以下都能取到(按顺序探测,最多 4 层):

choices[0].delta.content / choices[0].message.content / choices[0].text
content / text / delta / answer / output / reply / chunk / token
message.{…} / data.{…} / result.{…}

结束信号认 [DONE]、{"done":true}、{"event":"finish"}、choices[0].finish_reason 以及事件名 done|end|finish|complete|message_end。错误给 {"error":"…"} 或 {"success":false,"message":"…"} 即可,组件会把文案抛到面板错误态。

方式二:直连模型服务 ​

ts
new LarkDoc({
  element: '#editor',
  ai: {
    protocol: 'openai',
    baseURL: process.env.VUE_APP_AI_BASE_URL, // 如 https://api.openai.com/v1
    apiKey: process.env.VUE_APP_AI_KEY,       // 自动补 Authorization: Bearer …
    model: process.env.VUE_APP_AI_MODEL,
    contentFormat: 'markdown',
  },
})
  • 实际地址 = baseURL 去掉末尾斜杠 + /chat/completions,可用 urlPath 覆盖。
  • 请求体 = { model, stream, messages },messages 是 [{role:'system'},{role:'user'}] 两条,指令由组件按动作生成(11 个动作的指令内置在 prompt.js,翻译会把目标语言拼进去)。
  • 需要自己控制 messages 时传 messages: (args) => [...]。

密钥不要进仓库

apiKey 支持传函数,渲染时再取值;也可以整体走宿主后端代理,用方式一。前端直连模型服务意味着密钥会进浏览器,仅建议内网或自用场景。

方式三:自管传输(兼容旧写法) ​

ts
new LarkDoc({
  element: '#editor',
  ai: {
    contentFormat: 'markdown',
    stream(args) {
      const { action, text, prompt, language, onStart, onChunk, onDone, onCancelled, onError } = args
      const controller = mySdk.chat({
        prompt: buildPrompt(action, text, prompt),
        onOpen: onStart,
        onDelta: onChunk,      // 逐字回调,只传增量文本
        onFinish: onDone,
        onError,
      })
      return { cancel: () => controller.abort() }
    },
  },
})

这一档的入参与返回值与历史版本完全一致,老接入不需要任何改动。stream 也可以写成 generate(别名),或者直接把函数当 ai 传:

ts
ai: (args) => ({ cancel() {} })

stream(args) 契约 ​

入参是一个对象(不是位置参数):

字段类型说明
actionstring动作值与 AI_ACTIONS 一致,如 rewrite、translate;面板输入框提交时为 custom,问答为 chat。
textstring上下文正文(按 AI_CONTEXTS 取选区 / 当前块 / 全文)。
promptstring用户在面板里补的要求,未填为空串。
languagestring目标语言,translate 用;未设置为空串。
range{ from, to, source }生成时的文档位置快照,用于结果回落。
contextobject上下文快照(含上下文来源、块信息等),排查问题用。

回调(全部可选,但建议至少实现 onChunk 与 onError):

回调说明
onStart()首块开始输出时触发一次,用于切换「生成中」。
onChunk(text)增量文本,只传新增部分,组件自己累加。
onDone()正常结束,触发一次。
onCancelled()用户点了停止。
onError(error)出错,传 Error 对象,message 会显示在面板上。

返回值需要提供 cancel();返回一个函数时组件会自动包成 { cancel }。

配置说明 ​

配置项类型默认值说明
features.aibooleantrue关闭后 AI 面板与工具栏入口隐藏。
aistring | object | function内置 Mock三档接入任选其一;传 null / false 回落 Mock。
ai.enabledbooleantrue显式置 false 关闭接入(同时回落 Mock),便于按环境开关。
ai.endpointstring—自家 AI 网关地址;别名 url、api。
ai.baseURLstring—OpenAI 兼容服务基址;别名 baseUrl。配了它且没配 endpoint 时协议默认 openai。
ai.urlPathstring/chat/completionsopenai 协议拼接的路径。
ai.apiKeystring | () => string—自动补 Authorization: Bearer …;已有同名头则跳过。
ai.modelstring—仅 openai 协议,写进请求体。
ai.protocol'auto' | 'openai' | 'chatStream' | 'json'auto决定请求体形状与帧解析方式。
ai.contentFormat'auto' | 'markdown' | 'html' | 'text'auto结果按什么格式渲染预览并落正文。
ai.headersobject | () => object{}额外请求头,函数形式每次请求实时求值。
ai.paramsobject—合并进请求体的额外字段,如 { temperature: 0.2 }。
ai.messages(args) => Message[]组件生成仅 openai 协议,覆盖默认 system + user。
ai.transformRequest(request, args) => request—发请求前整体改写 { url, headers, body }。
ai.stream(布尔)booleantrue置 false 时不做流式,一次性接收整包结果。
ai.timeoutnumber0毫秒;流式建议保持 0。
ai.withCredentialsbooleanfalse跨域带 cookie。
ai.stream(函数)(args) => { cancel }—方式三的自管传输钩子。与上面的流式开关同名,按类型区分。

显示控制:contentFormat ​

AI 返回的多半是 markdown,直接塞进文档会露出 ** 和 #。contentFormat 决定这些标记怎么处理:

值预览落正文
auto(默认)内容含 markdown 语法时按富文本渲染,否则显示原文富文本时解析成标题 / 列表 / 粗体;纯文本保持原样
markdown一律按 markdown 渲染解析成真实格式
html按宿主给的 HTML 渲染直接插入(仍需符合编辑器 schema)
text纯文本 <pre>insertText,不解析任何标记

额外做了两件防坑的事:

  • 单段落结果按行内插入:「把这句话改正式」这类只产出一段的动作,替换时只换段内文字,不会把光标所在段落切成两段。
  • 解析失败回落纯文本:结果结构超出编辑器 schema 时退回 insertText,保证内容不丢。

行内渲染对 <、& 一律逃生,链接只放行 http(s):、mailto:、tel: 与站内相对路径,javascript: 之类的协议会被剥掉 href 只留文字。

内置动作(AI_ACTIONS) ​

valuelabelvaluelabelvaluelabel
continue续写rewrite改写expand扩写
shorten精简summarize摘要translate翻译
proofread校对formal正式语气friendly友好语气
title生成标题outline生成大纲

除这 11 个外,面板输入框提交会带 action: 'custom'(prompt 是要求、text 是内容),问答会带 action: 'chat'。

上下文来源(AI_CONTEXTS) ​

valuelabel
none无上下文
selection当前选区
block当前块
document全文

交互流程 ​

入口行为
工具栏 AI 按钮 / 气泡菜单打开 AI 面板,选择动作与上下文。
流式输出预览区逐字显示,可随时停止。
确认 / 放弃确认后写入文档(并入撤销历史);放弃不改动正文。
校对额外输出问题清单(Mock 内置规则审查)。
生成标题只写实例的 title,不改正文。

错误与取消 ​

  • HTTP 状态非 2xx 时,错误文案会拼上响应体里的 message / error.message / error / msg,直接显示在面板上;非 JSON 响应体截取前 200 字。
  • 出错后的后续帧会被丢弃,不会再触发 onDone,避免「报错了却又写入半截文本」。
  • 同一轮生成里 onDone / onCancelled / onError 只会有一个生效,重复调用被忽略。
  • 取消会 abort 底层请求并停止轮询,不会留下后台流量。

生命周期

destroy() 与组件卸载都会先调用 stopAI(),进行中的流会被取消,之后到达的 onChunk / onDone 一律丢弃,不会出现「销毁后还往正文写」的回调。宿主自己发起的请求仍需在 cancel() 里真正中断底层连接。

调试与自检 ​

ts
import { ai as aiAdapter } from 'lark-doc'

// 看一眼配置会被归一化成什么
aiAdapter.normalizeAIProvider({ endpoint: '/api/ai', contentFormat: 'markdown' })

// 验证 markdown 渲染结果,确认落正文的 HTML 符合预期
aiAdapter.previewToHtml('## 标题\n\n- 要点', 'markdown')

// 单独试一个动作的指令
aiAdapter.buildMessages('translate', 'hello', '', 'en')

ai 命名空间还导出 markdownToHtml / looksLikeMarkdown / plainTextToHtml / singleParagraphInner / createSseDecoder / createFrameParser / pickText / extractWholeBody / ACTION_PROMPTS,可以在宿主侧复用同一套解析与渲染,避免出现「组件里显示对了、自己拼的请求却对不上」。