AI Assistant 是 TinyMCE 首次尝试将 AI 功能集成到编辑器中的产品。当前的 AI 解决方案是 TinyMCE AI,它通过聊天、快捷操作和审阅模式提供深度文档感知的 AI 功能。TinyMCE AI 可同时配合 Tiny Cloud 和自托管的 TinyMCE 编辑器使用,并能连接托管版 Tiny Cloud AI 服务或 自托管 AI 服务。

简介

本指南介绍如何在 TinyMCE 中集成 AI Assistant 插件,并使用 OpenAI ChatGPT。

如需了解字符串响应与流式响应的区别,请参阅插件页面中的 respondWith 对象。

前提条件

开始前,请准备以下内容:

  1. OpenAI API 密钥。获取 API 密钥需在 OpenAI Platform 注册账户。

以下示例旨在演示如何在客户端集成中使用 API 认证凭据。严禁用于生产环境。建议仅通过 代理服务器 或实现服务端集成来访问 API,以防止未经授权的访问。

字符串响应

本示例演示如何将 AI Assistant 插件与 OpenAI API 集成以生成字符串响应。

// 此示例将 API 密钥存储在客户端集成中。任何用途下均不建议此做法。
// 应改用其他方法获取 API 密钥。
const api_key = '<INSERT_API_KEY_HERE>';

const ai_request = (request, respondWith) => {
  const openAiOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${api_key}`
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      temperature: 0.7,
      max_tokens: 800,
      messages: [{ role: 'user', content: request.prompt }],
    })
  };
  respondWith.string((signal) => window.fetch('https://api.openai.com/v1/chat/completions', { signal, ...openAiOptions })
    .then(async (response) => {
      if (response) {
        const data = await response.json();
        if (data.error) {
          throw new Error(`${data.error.type}: ${data.error.message}`);
        } else if (response.ok) {
          // 从 API 返回的数据中提取响应内容
          return data?.choices[0]?.message?.content?.trim();
        }
      } else {
        throw new Error('Failed to communicate with the ChatGPT API');
      }
    })
  );
};

tinymce.init({
  selector: 'textarea',  // 根据 HTML 修改此值
  plugins: 'ai',
  toolbar: 'aidialog aishortcuts',
  ai_request
});

流式响应

本示例演示如何将 AI Assistant 插件与 OpenAI API 集成以生成流式响应。

const fetchApi = import("https://cdn.skypack.dev/@microsoft/fetch-event-source@2.0.1").then(module => module.fetchEventSource);

// 此示例将 API 密钥存储在客户端集成中。不建议出于任何目的这样做。
// 相反,应使用其他方法来检索 API 密钥。
const api_key = '<INSERT_API_KEY_HERE>';

const ai_request = (request, respondWith) => {
  respondWith.stream((signal, streamMessage) => {
    // 将之前的每个查询和响应作为单独的消息添加
    const conversation = request.thread.flatMap((event) => {
      if (event.response) {
        return [
          { role: 'user', content: event.request.query },
          { role: 'assistant', content: event.response.data }
        ];
      } else {
        return [];
      }
    });

    // 插件提供的系统消息,用于将输出格式化为 HTML 内容。
    const pluginSystemMessages = request.system.map((content) => ({
      role: 'system',
      content
    }));

    const systemMessages = [
      ...pluginSystemMessages,
      // 用于控制 AI 输出的额外系统消息
      { role: 'system', content: '从响应的开头和结尾移除带有 ``` 的行。' }
    ]

    // 构建发送到 API 的新查询
    const content = request.context.length === 0 || conversation.length > 0
      ? request.query
      : `Question: ${request.query} Context: """${request.context}"""`;

    const messages = [
      ...conversation,
      ...systemMessages,
      { role: 'user', content }
    ];

    const requestBody = {
      model: 'gpt-4o',
      temperature: 0.7,
      max_tokens: 800,
      messages,
      stream: true
    };

    const openAiOptions = {
      signal,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${api_key}`
      },
      body: JSON.stringify(requestBody)
    };

    const onopen = async (response) => {
      if (response) {
        const contentType = response.headers.get('content-type');
        if (response.ok && contentType?.includes('text/event-stream')) {
          return;
        } else if (contentType?.includes('application/json')) {
          const data = await response.json();
          if (data.error) {
            throw new Error(`${data.error.type}: ${data.error.message}`);
          }
        }
      } else {
        throw new Error('Failed to communicate with the ChatGPT API');
      }
    };

    // 此函数通过 `streamMessage` 回调将每条新消息传递给插件。
    const onmessage = (ev) => {
      const data = ev.data;
      if (data !== '[DONE]') {
        const parsedData = JSON.parse(data);
        const firstChoice = parsedData?.choices[0];
        const message = firstChoice?.delta?.content;
        if (message) {
          streamMessage(message);
        }
      }
    };

    const onerror = (error) => {
      // 停止操作并且不通过 fetch-event-source 重试
      throw error;
    };

    // 使用 Microsoft 的 fetch-event-source 库来绕过浏览器 `EventSource` API 的 2000 字符限制,该限制需要查询字符串
    return fetchApi
    .then(fetchEventSource =>
      fetchEventSource('https://api.openai.com/v1/chat/completions', {
        ...openAiOptions,
        openWhenHidden: true,
        onopen,
        onmessage,
        onerror
      })
    )
    .then(async (response) => {
      if (response && !response.ok) {
        const data = await response.json();
        if (data.error) {
          throw new Error(`${data.error.type}: ${data.error.message}`);
        }
      }
    })
    .catch(onerror);
  });
};

tinymce.init({
  selector: 'textarea',  // 根据你的 HTML 更改此值
  plugins: 'ai',
  toolbar: 'aidialog aishortcuts',
  ai_request
});