AI Assistant 是 TinyMCE 首次将 AI 功能集成到编辑器中的迭代版本。TinyMCE AI 是当前的 AI 解决方案,通过 Chat、Quick Actions 和 Review 模式提供深度文档感知的 AI 功能。TinyMCE AI 可与 Tiny Cloud 和自托管的 TinyMCE 编辑器配合使用,并能连接到托管的 Tiny Cloud AI 服务或自托管 AI 服务。

介绍

本指南提供了在 TinyMCE 中使用 Azure OpenAI Service 集成 AI Assistant 插件的说明。

Azure OpenAI Service 提供对 OpenAI 语言模型的 REST API 访问,并具备 Azure 特有的功能,如私有网络、区域可用性和负责任的 AI 内容过滤。

若要了解字符串响应和流式响应之间的区别,请参阅插件页面上的 respondWith 对象。

前提条件

开始之前,请确保已在 Azure 中设置 OpenAI 模型。集成需要模型的 endpoint URL 和 API 密钥。有关详细信息,请参阅 Azure 提供的快速入门指南。

以下示例旨在展示如何在客户端集成中使用身份验证凭据调用 API。这不建议用于生产环境。建议仅通过 代理服务器或实施服务器端集成来访问 API,以防止未经授权的 API 访问。

字符串响应

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

// 此示例在客户端集成中存储了 API 密钥。不推荐任何情况下使用此方法。
// 应使用其他方法来检索 API 密钥。
const AZURE_OPENAI_API_KEY = '<INSERT_API_KEY_HERE>';
const AZURE_OPENAI_ENDPOINT = '<INSERT_ENDPOINT_URL_HERE>'; // e.g. https://<INSERT_RESOURCE_NAME_HERE>.openai.azure.com/openai/deployments/<INSERT_DEPLOYMENT_ID_HERE>/chat/completions?api-version=<INSERT_API_VERSION_HERE>

const ai_request = (request, respondWith) => {
  const azureOpenAiOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'api-key': AZURE_OPENAI_API_KEY
    },
    body: JSON.stringify({
      temperature: 0.7,
      max_tokens: 800,
      messages: [{ role: 'user', content: request.prompt }],
    })
  };
  respondWith.string((signal) => window.fetch(AZURE_OPENAI_ENDPOINT, { signal, ...azureOpenAiOptions })
    .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 Azure OpenAI API');
      }
    })
  );
};

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

流式响应

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

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

// 此示例将 API 密钥存储在客户端集成中。不建议出于任何目的这样做。
// 相反,应使用其他方法来获取 API 密钥。
const AZURE_OPENAI_API_KEY = '<INSERT_API_KEY_HERE>';
const AZURE_OPENAI_ENDPOINT = '<INSERT_ENDPOINT_URL_HERE>'; // 例如 https://<INSERT_RESOURCE_NAME_HERE>.openai.azure.com/openai/deployments/<INSERT_DEPLOYMENT_ID_HERE>/chat/completions?api-version=<INSERT_API_VERSION_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: 'Remove lines with ``` from the response start and response end.' }
    ]

    // 构建发送到 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 = {
      temperature: 0.7,
      max_tokens: 800,
      messages,
      stream: true
    };

    const azureOpenAiOptions = {
      signal,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'api-key': AZURE_OPENAI_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 Azure OpenAI 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(AZURE_OPENAI_ENDPOINT, {
          ...azureOpenAiOptions,
          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
});