Fraqv0.14.0

@fraqjs/plugin-ai

@fraqjs/plugin-ai npm version

@fraqjs/plugin-ai 提供了配置模型提供商和 API Key 的统一入口,方便基于 Vercel AI SDK 的 AI 插件开发者访问用户开发者配置的语言模型和生图模型。

安装与配置

将插件添加到 fraq.ymlplugins 字段下:

plugins:
  fraqjs/ai:
    # 在这里声明多个 AI SDK 提供商,每个提供商下配置多个模型
    providers:
      # 提供商的名称可以自定义,建议使用 SDK 包名中的关键词以示区分
      deepseek:
        # 目前支持自动配置的 SDK 包包括:
        # - `@ai-sdk/anthropic`
        # - `@ai-sdk/deepseek`
        # - `@ai-sdk/google`
        # - `@ai-sdk/openai`
        # - `@ai-sdk/openai-compatible`
        # 在这里声明 SDK 之后,你需要手动将对应的 SDK 添加到项目的 `additionalDependencies` 中
        sdk: '@ai-sdk/deepseek'
        # 这里用于提供传入 SDK 的配置项
        # 基本的配置项有:
        # - `apiKey`:必需,用户的 API Key
        # - `baseURL`:可选,API 的基础 URL,默认为 SDK 内置的 URL
        # 例如,DeepSeek 的默认 URL 是 `https://api.deepseek.com/v1`
        # 不同 SDK 能接受的配置项不同,具体请参考对应 SDK 的文档
        options:
          apiKey: 'sk-xxx'
        # models 是一个字符串数组,用于声明用户想要使用的语言模型名称
        # 具体模型名称需要参考对应提供商 / SDK 的文档
        # 例如 DeepSeek 的模型名称包括 `deepseek-v4-flash`、`deepseek-v4-pro` 等
        models: [deepseek-v4-flash, deepseek-v4-pro]
      openai-custom:
        sdk: '@ai-sdk/openai'
        options:
          apiKey: 'sk-xxx'
          # 可以通过 `baseURL` 配置自定义的 API URL
          # OpenAI 风格的 API 通常以 `/v1` 结尾
          baseURL: 'https://my-openai-api.com/v1'
        models: [gpt-5.5, gpt-5.5-mini]
        # imageModels 是一个可选的字符串数组,用于声明生图模型名称
        # 例如 OpenAI 的生图模型包括 `gpt-image-2` 等
        imageModels: [gpt-image-2]
    # 在这里为模型配置别名,方便在插件中使用更友好的名称访问模型
    # 键 (key) 为别名,值 (value) 为 providers 中声明的模型名称,格式为 `提供商名称/模型名称`
    # 别名可以同时用于语言模型和生图模型
    # 例如 `fast` 对应 `deepseek` 提供商下的 `deepseek-v4-flash` 模型
    # `art` 对应 `openai-custom` 提供商下的 `gpt-image-2` 生图模型
    aliases:
      fast: deepseek/deepseek-v4-flash
      'fast-gpt': openai-custom/gpt-5.5-mini
      balanced: deepseek/deepseek-v4-pro
      expert: openai-custom/gpt-5.5
      art: openai-custom/gpt-image-2
    # 默认语言模型,插件中通过 `ctx.ai.model()` 访问时,如果没有指定模型名称,就会使用这个默认模型
    # 可以使用别名或者 `提供商名称/模型名称` 的格式指定默认模型
    # 如果没有配置这一项,默认使用 providers 中第一个语言模型作为默认模型
    defaultModel: fast
    # 默认生图模型,插件中通过 `ctx.ai.image()` 访问时,如果没有指定模型名称,就会使用这个默认模型
    # 如果没有配置这一项,默认使用 providers 中第一个生图模型作为默认模型
    defaultImageModel: art

你需要将配置中声明的 SDK 包以及 ai 包添加到 additionalDependencies 中。例如,对于上面的配置,应该在 fraq.yml 中添加如下内容:

additionalDependencies:
  '@ai-sdk/deepseek': ^3
  '@ai-sdk/openai': ^4
  ai: ^7

如果你是插件开发者,请将本插件添加到项目的 peerDependencies 中,并在自己的插件中声明依赖:

import { definePlugin } from '@fraqjs/fraq';
import { AiService } from '@fraqjs/plugin-ai';

definePlugin({
  name: 'my-plugin',
  inject: {
    ai: AiService,
  },
  apply(ctx) {
    // 通过 ctx.ai.model 访问配置好的语言模型
    // 通过 ctx.ai.image 访问配置好的生图模型
  },
});

此外,你还需要将 ai 添加到 peerDependencies 中,以便使用其中的函数。

获取模型实例

插件会将用户配置的模型实例注入到 ctx.ai 中。语言模型和生图模型分别通过 modelimage 方法获取:

语言模型

const model = ctx.ai.model(); // 获取默认语言模型
const fastModel = ctx.ai.model('fast'); // 通过别名获取
const kimiModel = ctx.ai.model('moonshot/kimi-k2.5'); // 通过 提供商/模型名 获取

ctx.ai.hasModel('fast'); // true
ctx.ai.hasModel('nonexistent'); // false

ctx.ai.models(); // 获取所有语言模型的名称列表

生图模型

const image = ctx.ai.image(); // 获取默认生图模型
const artModel = ctx.ai.image('art'); // 通过别名获取
const dalleModel = ctx.ai.image('openai-custom/gpt-image-2'); // 通过 提供商/模型名 获取

ctx.ai.hasImage('art'); // true
ctx.ai.hasImage('nonexistent'); // false

ctx.ai.images(); // 获取所有生图模型的名称列表

调用 AI 功能

Vercel 的 ai 包提供了丰富的函数来调用模型,只需要传入模型实例和相应的参数即可。下面是一些简单的例子,更多用法请参考 Vercel AI SDK 的文档

生成文本

import { generateText } from 'ai';

const { text } = await generateText({
  model: ctx.ai.model(),
  prompt: '用一句话介绍一下 Fraq。',
});

console.log(text);

流式生成文本

streamText 会返回一个流式结果,你可以通过 textStream 异步迭代器逐段读取生成的文本:

import { streamText } from 'ai';

const result = streamText({
  model: ctx.ai.model(),
  prompt: '写一首关于海的短诗。',
});

for await (const delta of result.textStream) {
  process.stdout.write(delta);
}

生成结构化数据

generateTextstreamText 都支持通过 output 选项来指定生成结果的格式:

import { generateText, Output } from 'ai';
import z from 'zod';

const { output } = await generateText({
  model: ctx.ai.model(),
  output: Output.object({
    schema: z.object({
      recipe: z.object({
        name: z.string(),
        ingredients: z.array(
          z.object({ name: z.string(), amount: z.string() }),
        ),
        steps: z.array(z.string()),
      }),
    }),
  }),
  prompt: '请生成一份阳春面的食谱。',
});

这里输出的 output 将会是一个符合指定 Zod schema 的对象。

同样的,如果你在插件中使用 zod,也需要将 zod 添加到 peerDependencies 中。

生成图片

generateImage 会调用生图模型并返回生成的图片,你可以通过 base64uint8Array 获取图片数据:

import { generateImage } from 'ai';
import { seg } from '@fraqjs/fraq';

const { image } = await generateImage({
  model: ctx.ai.image(),
  prompt: '画一只猫',
});

// 将生成的图片转换为 base64 URI 并发送
await session.reply(seg.image(`base64://${image.base64}`));

工具调用

generateTextstreamText 都支持工具调用(Tool Calling),你可以通过 tools 选项让模型在生成过程中调用你定义的函数:

import { generateText, tool } from 'ai';
import z from 'zod';

const { toolResults } = await generateText({
  model: ctx.ai.model(),
  prompt: '东京现在的天气怎么样?',
  tools: {
    weather: tool({
      description: '查询某个城市的天气',
      inputSchema: z.object({
        city: z.string().describe('城市名称'),
      }),
      execute: async ({ city }) => `${city}今天是晴天`,
    }),
  },
});

console.log(toolResults);

使用内置工具

AiService 提供了一个 milkyToolset 方法,可以构造 tools 选项所需的工具集,方便 AI 直接调用 Milky 协议功能。你需要提供一个 ctx 实例和一个数组,数组中包含你想要开放给 AI 调用的 Milky API 名称:

import { milkyToolset } from '@fraqjs/plugin-ai';

const tools = milkyToolset(ctx, ['get_login_info', 'set_nickname']);

await generateText({
  model: ctx.ai.model(),
  prompt: '获取当前帐户的登录信息。',
  tools,
});

将消息转换为 XML

@fraqjs/plugin-ai 提供了 xmlify 系列函数,将 milky.IncomingMessage 转换为结构化的 XML 文本。在把聊天消息作为上下文提供给语言模型时,XML 的结构化形式比 JSON 更利于模型理解消息的各个组成部分(如图片、@提及、回复引用、合并转发等)。

转换单条消息

xmlify 接收一个 Context 和单条消息,返回一个包含 XML 文本、资源和文件的对象:

import { xmlify } from '@fraqjs/plugin-ai';

ctx.on('message_receive', async ({ data }) => {
  const { xmlContent, resources, files } = await xmlify(ctx, data);
  console.log(xmlContent);
});

对于一条包含文本和图片的好友消息,xmlContent 的输出形如:

<message scene="friend" peer_id="10001" seq="1" sender_id="10001" time="1700000000">
  <content>
    <text>你好,看看这只猫</text>
    <image id="image1" width="800" height="600" sub_type="normal">[图片]</image>
  </content>
  <friend>
    <user_id>10001</user_id>
    <nickname>Alice</nickname>
    <remark></remark>
  </friend>
</message>

返回对象(XmlifyContext)包含以下字段:

  • xmlContent:格式化后的 XML 字符串。
  • resources:资源键到临时 URL 的映射,例如 { image1: { url: 'https://...' } }。其中 imagerecordvideo 类型的消息段会分别以 image1record1video1 等递增编号作为键。
  • files:文件名到文件数据的映射,对应 file 类型的消息段。

<message> 根元素上会附带 scenepeer_idseqsender_idtime 等属性;根据消息场景,还会包含 <friend><group><group_member> 等子元素,描述发送者与会话的上下文信息。

转换多条消息

xmlifyThread 用于将多条消息合并为一个 <thread> 元素,适合将一段对话历史整体提供给模型。它会复用同一个资源编号空间,确保所有消息中的 imagerecordvideo 键互不冲突:

import { xmlifyThread } from '@fraqjs/plugin-ai';

const { messages } = await ctx.client.get_history_messages({
  message_scene: 'group',
  peer_id: 100100,
  limit: 10,
});
const { xmlContent } = await xmlifyThread(ctx, messages);

输出形如:

<thread scene="group" peer_id="100100">
  <message seq="1" sender_id="10001" time="1700000000">
    <content>
      <text>有人在线吗?</text>
    </content>
  </message>
  <message seq="2" sender_id="10002" time="1700000001">
    <content>
      <text>在的</text>
    </content>
  </message>
</thread>

xmlifyThread 中,每个 <message> 元素只保留 seqsender_idtime 属性,场景与对端信息统一由外层 <thread> 携带。

配置项

xmlifyxmlifyThread 的第三个参数是一个可选的 XmlifyOptions 对象,支持以下配置项:

  • maxForwardDepth:合并转发消息的展开深度,默认为 0。为 0 时,合并转发不会被展开,仅输出占位文本;大于 0 时,会通过 ctx.client.get_forwarded_messages 拉取转发内容,并递归展开到指定深度。超出 maxForwardDepth 的嵌套层级会以 (Too deeply nested) 占位,避免无限制地拉取深层转发内容。
  • serialize:传递给 dom-serializer 的序列化选项,默认使用 encodeEntities: 'utf8'
  • format:传递给 xml-formatter 的格式化选项,默认使用两个空格缩进。

On this page