Skip to content

创建插件

插件根目录至少需要包含两个文件:

  • plugin.json: 描述插件信息的 json 文件
  • index.js: 实现功能的 JavaScript 脚本

插件的 bundle 就是这两个文件的 zip 压缩包。

配置 plugin.json

字段类型是否必须说明
idstring插件的唯一标识符,必须由数字、小写字母和 . 组成。
namestring插件名称,无限制,建议别太长。
versionstring插件版本号,必须由数字、小写字母和 . 组成。
descriptionstring-插件描述信息。
configurationobject-插件选项数组,该字段用于提供一些选项供用户选择或填写,详情见 config object

config object

字段类型是否必须说明
titlestring插件在配置页面显示的分类
propertiesRecord<string, object>配置 JSON Schema,示例如下
json
{
  "title": "OpenAI",
  "properties": {
    "openai.apiKey": {
      "type": "string",
      "description": "The API key to use for OpenAI",
      "default": "",
      "format": "password"
    }
  }
}

实现 index.js

前文提到,index.js 是一个 JavaScript 脚本,需要导出一个 activate 函数供 NovaChat 初始化,而所有插件相关的 API 都从 @novachat/plugin 中导入。

下面是一个不做任何事情的插件:

ts
export async function activate() {
  console.log('demo plugin activated')
}

添加 LLM 支持

要添加新的 LLM 支持,需要使用 novachat.model.registerProvider 注册新的 LLM Provider,一般会提供多个模型。

下面是 OpenAI Provider 的实现:

ts
import * as novachat from '@novachat/plugin'
import OpenAI from 'openai'

function convertReq(
  req: novachat.QueryRequest,
  stream?: boolean,
): OpenAI.ChatCompletionCreateParams {
  return {
    model: req.model,
    messages: req.messages.map((it) => {
      if (it.from === 'user' && it.attachments) {
        return {
          role: 'user',
          content: [
            {
              type: 'text',
              text: it.content,
            },
            ...it.attachments.map(
              (atta) =>
                ({
                  type: 'image_url',
                  image_url: { url: atta.url },
                } as OpenAI.ChatCompletionContentPartImage),
            ),
          ],
        }
      }
      return {
        content: it.content,
        role: it.from,
      }
    }),
    stream,
  }
}

export async function activate(context: novachat.PluginContext) {
  const client = async () => {
    return new OpenAI({
      apiKey: await novachat.setting.get('openai.apiKey'),
      dangerouslyAllowBrowser: true,
    })
  }
  await novachat.model.registerProvider({
    name: 'OpenAI',
    models: [
      { id: 'o1-preview', name: 'o1-preview' },
      { id: 'o1-mini', name: 'o1-mini' },
      { id: 'gpt-4o', name: 'GPT-4o' },
      { id: 'gpt-4o-mini', name: 'GPT-4o-mini' },
      { id: 'gpt-4', name: 'GPT-4' },
      { id: 'gpt-4-turbo', name: 'GPT-4o-turbo' },
    ],
    async invoke(query) {
      const response = await (
        await client()
      ).chat.completions.create(
        convertReq(query) as OpenAI.ChatCompletionCreateParamsNonStreaming,
      )
      return {
        content: response.choices[0].message.content ?? '',
      }
    },
    async *stream(query) {
      const response = await (
        await client()
      ).chat.completions.create(
        convertReq(query, true) as OpenAI.ChatCompletionCreateParamsStreaming,
      )
      for await (const it of response) {
        yield {
          content: it.choices[0].delta.content ?? '',
        }
      }
    },
  })
}

上面使用 novachat.setting.get('openai.apiKey') 获取用户配置的 API Key,所以还需要在 plugin.json 中添加配置选项。

json
{
  "configuration": {
    "title": "OpenAI",
    "properties": {
      "openai.apiKey": {
        "type": "string",
        "description": "The API key to use for OpenAI",
        "default": "",
        "format": "password"
      }
    }
  }
}

参考: https://github.com/rxliuli/novachat/tree/main/packages/plugin-openai

实现 Prompt Bot

有时候只想使用提示词来进行角色扮演或其他任何事情,希望使用已经配置的 LLM Provider。这种情况下可以实现一个 Prompt Bot,下面是一个翻译插件,它会检测输入内容的语言,如果与用户配置的默认语言相同,就会翻译为英语,否则翻译为用户配置的语言。

ts
import * as novachat from '@novachat/plugin'
import { last } from 'lodash-es'
import { franc } from 'franc-min'

const SYSTEM_MESSAGE =
  'You are a professional, authentic machine translation engine.'
const USER_MESSAGE = `
Translate the following source text to {{to}}, Output translation directly without any additional text.
Source Text: {{text}}

Translated Text:
`.trim()

export async function activate() {
  await novachat.model.registerBot({
    id: 'translator',
    name: 'Translator',
    async *stream(
      query: novachat.QueryRequest,
    ): AsyncGenerator<novachat.QueryChunkResponse> {
      const lastMessage = last(query.messages)
      if (!lastMessage) {
        throw new Error('No last message')
      }
      const defaultModel = await novachat.model.getDefault()
      if (!defaultModel) {
        throw new Error('No default model')
      }
      const localLanguage =
        (await novachat.setting.get('translator.localLanguage')) ?? 'eng'
      const language = franc(lastMessage.content)
      const toLanguage = language === localLanguage ? 'eng' : localLanguage
      const stream = novachat.model.stream({
        messages: [
          { from: 'system', content: SYSTEM_MESSAGE },
          {
            from: 'user',
            content: USER_MESSAGE.replace('{{to}}', toLanguage).replace(
              '{{text}}',
              lastMessage.content,
            ),
          },
        ],
        model: defaultModel.id,
      })
      for await (const it of stream) {
        yield it
      }
    },
  })
}

参考: https://github.com/rxliuli/novachat/tree/main/packages/plugin-translator

Released under the MIT License.