@fraqjs/plugin-ai
@fraqjs/plugin-ai 提供了配置模型提供商和 API Key 的统一入口,方便基于 Vercel AI SDK 的 AI 插件开发者访问用户开发者配置的语言模型和生图模型。
安装与配置
将插件添加到 fraq.yml 的 plugins 字段下:
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 中。语言模型和生图模型分别通过 model 和 image 方法获取:
语言模型
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);
}生成结构化数据
generateText 和 streamText 都支持通过 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 会调用生图模型并返回生成的图片,你可以通过 base64 或 uint8Array 获取图片数据:
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}`));工具调用
generateText 和 streamText 都支持工具调用(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://...' } }。其中image、record、video类型的消息段会分别以image1、record1、video1等递增编号作为键。files:文件名到文件数据的映射,对应file类型的消息段。
<message> 根元素上会附带 scene、peer_id、seq、sender_id、time 等属性;根据消息场景,还会包含 <friend>、<group>、<group_member> 等子元素,描述发送者与会话的上下文信息。
转换多条消息
xmlifyThread 用于将多条消息合并为一个 <thread> 元素,适合将一段对话历史整体提供给模型。它会复用同一个资源编号空间,确保所有消息中的 image、record、video 键互不冲突:
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> 元素只保留 seq、sender_id、time 属性,场景与对端信息统一由外层 <thread> 携带。
配置项
xmlify 和 xmlifyThread 的第三个参数是一个可选的 XmlifyOptions 对象,支持以下配置项:
maxForwardDepth:合并转发消息的展开深度,默认为0。为0时,合并转发不会被展开,仅输出占位文本;大于0时,会通过ctx.client.get_forwarded_messages拉取转发内容,并递归展开到指定深度。超出maxForwardDepth的嵌套层级会以(Too deeply nested)占位,避免无限制地拉取深层转发内容。serialize:传递给dom-serializer的序列化选项,默认使用encodeEntities: 'utf8'。format:传递给xml-formatter的格式化选项,默认使用两个空格缩进。