在插件中使用 AI
声明服务
先在插件的 package.json 里声明依赖
{
"mioku": {
"services": ["ai"]
}
}然后在 index.ts 里读取服务
import { definePlugin } from "mioki";
import type { AIService } from "../../src/services/ai/types";
export default definePlugin({
name: "release-note",
async setup(ctx) {
const aiService = ctx.services?.ai as AIService | undefined;
},
});TIP
大部分插件不需要自己创建 AI 实例
正常情况下,直接拿默认实例即可。默认实例通常由 chat 插件在启动时创建并设置
获取默认 AI 实例
import { definePlugin } from "mioki";
import type { AIService } from "../../src/services/ai/types";
export default definePlugin({
name: "release-note",
async setup(ctx) {
const aiService = ctx.services?.ai as AIService | undefined;
const ai = aiService?.getDefault();
},
});使用默认实例生成文本
最常见的用法就是:给一段明确提示词,让 AI 直接返回可发送的文本
import { definePlugin } from "mioki";
import type { AIService } from "../../src/services/ai/types";
export default definePlugin({
name: "release-note",
async setup(ctx) {
const aiService = ctx.services?.ai as AIService | undefined;
const ai = aiService?.getDefault();
if (!ai) return;
ctx.handle("message", async (event) => {
const text = ctx.text(event).trim();
if (!text.startsWith("/润色公告 ")) {
return;
}
const draft = text.slice("/润色公告 ".length).trim();
if (!draft) {
await event.reply("请先给我一段公告草稿");
return;
}
const polished = await ai.generateText({
prompt: [
"你是群公告编辑助手。",
"保留原意,不要编造新事实",
"输出 2 到 4 行,语气明确",
].join("\n"),
messages: [{ role: "user", content: draft }],
temperature: 0.4,
max_tokens: 180,
});
await event.reply(polished.trim());
});
},
});当你省略 model 时,ai-service 会自动读取 config/chat/base.json 里的 model 作为默认模型
使用多模态生成内容
如果你的插件要同时给模型传文字和图片,使用 generateMultimodal()
import { definePlugin } from "mioki";
import type { AIService } from "../../src/services/ai/types";
export default definePlugin({
name: "poster-review",
async setup(ctx) {
const aiService = ctx.services?.ai as AIService | undefined;
const ai = aiService?.getDefault();
if (!ai) return;
ctx.handle("message", async (event) => {
const text = ctx.text(event).trim();
const match = text.match(/^\/检查海报\s+(https?:\/\/\S+)$/);
if (!match) {
return;
}
const imageUrl = match[1];
const result = await ai.generateMultimodal({
prompt: [
"你是活动运营助手。",
"请从海报中提取活动名称、时间、地点。",
"如果文案存在明显问题,也顺手指出来。",
].join("\n"),
messages: [
{
role: "user",
content: [
{ type: "text", text: "请检查这张活动海报" },
{
type: "image_url",
image_url: {
url: imageUrl,
detail: "high",
},
},
],
},
],
temperature: 0.2,
});
await event.reply(result.trim() || "没有识别到有效内容");
});
},
});带工具调用的内容生成
如果你希望模型不只是写字,而是决定什么时候调用插件能力,用 complete(),并传入 executableTools
import { definePlugin } from "mioki";
import type { AIService } from "../../src/services/ai/types";
export default definePlugin({
name: "web-ask",
async setup(ctx) {
const aiService = ctx.services?.ai as AIService | undefined;
const ai = aiService?.getDefault();
if (!ai) return;
ctx.handle("message", async (event) => {
const text = ctx.text(event).trim();
const [, url, question] = match;
const response = await ai.complete({
temperature: 0.2,
maxIterations: 4,
messages: [
{
role: "system",
content: [
"你是网页信息整理助手。",
"先调用工具读取网页,再根据网页内容回答。",
"如果网页信息不足,就明确说明。",
].join("\n"),
},
{
role: "user",
content: `${text}`,
},
],
executableTools: [
{
name: "read_webpage",
tool: {
name: "read_webpage",
description: "下载网页并提取标题和正文文本",
parameters: {
type: "object",
properties: {
url: {
type: "string",
description: "要读取的网页地址",
},
},
required: ["url"],
},
handler: async (args) => {
const targetUrl = String(args?.url || "").trim();
if (!targetUrl) {
return { success: false, error: "missing url" };
}
const resp = await fetch(targetUrl);
const html = await resp.text();
const title =
html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]?.trim() ||
"";
const plainText = html
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 6000);
return {
success: true,
url: targetUrl,
title,
content: plainText,
};
},
},
},
],
});
await event.reply(response.content || "没有拿到可用结果");
});
},
});使用chat运行时
chat-runtime 不是普通文本生成接口。
它是 chat 插件注册到 ai 服务上的一层运行时能力,作用是:
- 复用
chat插件当前的人设 - 复用最近对话上下文
- 复用
chat插件自己的发送逻辑
TIP
换句话讲,你的插件可以通过 chat 运行时通过 chat 插件和用户自然地对话
const aiService = ctx.services?.ai as AIService | undefined;
const chatRuntime = aiService?.getChatRuntime();
if (!chatRuntime) {
ctx.logger.warn("chat-runtime 不可用,请先启用 chat 插件");
return;
}IMPORTANT
chat-runtime 由 chat 插件注册。
如果没启用 chat 插件,或者聊天插件初始化失败,getChatRuntime() 会返回 undefined。
用聊天人设发通知
一般用 generateNotice(),这个方法的目标很简单,就是让当前人格把这件事自然地说出来
import { definePlugin } from "mioki";
import type { AIService } from "../../src/services/ai/types";
export default definePlugin({
name: "video-jobs",
async setup(ctx) {
const aiService = ctx.services?.ai as AIService | undefined;
const chatRuntime = aiService?.getChatRuntime();
if (!chatRuntime) return;
async function notifyJobFinished(event: any, jobId: string) {
await chatRuntime.generateNotice({
event,
instruction: `告诉用户:视频转码已经完成,现在可以发送 /下载 ${jobId} 获取结果`,
send: true,
});
}
},
});如果你想先预览文本,不立即发送,可以传 send: false,然后读取返回值里的 messages 继续处理
用聊天人设向用户追问缺失信息
询问场景用 requestInformation()。
它内部会额外挂一个提交答案的工具,让模型在信息足够时把结构化结果交回来。如果信息不够,它就继续追问
import { definePlugin } from "mioki";
import type { AIService } from "../../src/services/ai/types";
export default definePlugin({
name: "reminder",
async setup(ctx) {
const aiService = ctx.services?.ai as AIService | undefined;
const chatRuntime = aiService?.getChatRuntime();
if (!chatRuntime) return;
ctx.handle("message", async (event) => {
const text = ctx.text(event).trim();
if (text !== "/提醒我") {
return;
}
const result = await chatRuntime.requestInformation({
event,
task: "帮当前用户补全创建提醒任务所需的信息",
schema: {
type: "object",
properties: {
time: {
type: "string",
description: "提醒时间,例如 今天 23:30、明天早上 8 点",
},
content: {
type: "string",
description: "提醒内容",
},
repeat: {
type: "string",
description: "可选,重复规则,例如 每周一到周五",
},
},
required: ["time", "content"],
},
send: true,
});
const info = result.collectedInfo;
if (!info?.isComplete || !info.data) {
return;
}
await event.reply(
`提醒已创建:${info.data.time} - ${info.data.content}`,
);
});
},
});默认 AI 实例和 chat-runtime 的区别
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 总结文本、改写公告、解析参数 | 默认 AI 实例 | 你完全控制提示词、消息和工具 |
| 让模型调用插件里的本地工具 | 默认 AI 实例 + complete() | 你可以传 executableTools |
想让消息保持 chat 插件的人设和语气 | chat-runtime.generateNotice() | 直接复用聊天人格和上下文 |
| 想让聊天人格代你向用户补齐字段 | chat-runtime.requestInformation() | 自带提交答案工具和追问流程 |
- 默认 AI 实例更像你自己在直接调模型
chat-runtime更像请聊天插件代你开口
编写 skills.ts
如果你想把插件能力暴露给 AI,可以在插件目录下编写 skills.ts 文件
Mioku 会在启动时自动扫描插件目录下的 skills.ts,并注册里面 default-export 出来的 AISkill[]
NOTE
提供skiils可以让 chat 插件中的AI使用插件中的功能
import type { AISkill, AITool } from "../../src";
import type { HelpService } from "../../src/services/help/types";
import { buildHelpInfoText } from "./shared";
const helpSkills: AISkill[] = [
{
name: "help",
description: "帮助系统,获取插件帮助信息和发送帮助图片",
tools: [
{
name: "get_help_info",
description: "获取所有插件的帮助信息文本",
parameters: {
type: "object",
properties: {},
required: [],
},
handler: async (_args: any, runtimeCtx?: any) => {
const ctx = runtimeCtx?.ctx;
const helpService = ctx?.services?.help as HelpService | undefined;
if (!helpService) {
return "help-service 未加载,无法获取帮助信息";
}
return buildHelpInfoText(helpService.getAllHelp());
},
} as AITool,
],
},
];
export default helpSkills;skills.ts默认导出的是AISkill[]- 工具处理函数可以通过
runtimeCtx?.ctx访问当前上下文 - 如果需要读取
setup()创建的可变对象,不要依赖模块局部变量,使用runtime.ts+ Mioku runtime registry
注册后,工具会以 skillName.toolName 的形式被识别
如果 chat 插件启用了外部技能,它就可以把这些工具加载到会话里
使用 runtime.ts 解决 index.ts 闭包
help 这种工具比较简单,直接从 runtimeCtx?.ctx 里拿服务就够了
但更复杂的插件往往会在 setup() 里创建一些只能运行时存在的对象,比如:
- 循环管理器
- 长连接客户端
- 缓存和会话状态
- 由配置拼出来的服务包装层
问题在于:skills.ts 不是在插件 setup() 内执行的,它会被框架单独导入
所以它不能直接引用 setup() 里的局部变量,这时就需要 runtime.ts 做桥接。
IMPORTANT
不要把 runtime.ts 写成模块内局部变量单例,例如 const runtimeState = {}
原因有两个:
skills.ts和插件本体可能通过不同加载路径被导入mioki当前内部使用的jiti明确关闭了moduleCache
这意味着同一个 runtime.ts 文件可能被执行多次,模块级变量不会稳定共享
在 Mioku 里,推荐使用 src/core/plugin-runtime-state.ts 提供的全局 runtime registry
// runtime.ts 示例
import type { QueueManager } from "./queue-manager";
import {
getPluginRuntimeState,
resetPluginRuntimeState,
setPluginRuntimeState,
} from "../../src";
export interface NoticeRuntimeState {
queue?: QueueManager;
webhookUrl?: string;
}
const PLUGIN_NAME = "notice-center";
export function setNoticeRuntimeState(nextState: NoticeRuntimeState) {
return setPluginRuntimeState<NoticeRuntimeState>(PLUGIN_NAME, nextState);
}
export function getNoticeRuntimeState(): NoticeRuntimeState {
return getPluginRuntimeState<NoticeRuntimeState>(PLUGIN_NAME);
}
export function resetNoticeRuntimeState(): void {
resetPluginRuntimeState(PLUGIN_NAME);
}在 index.ts 的 setup() 里,把运行时对象塞进去:
// index.ts
import { definePlugin } from "mioki";
import { QueueManager } from "./queue-manager";
import {
resetNoticeRuntimeState,
setNoticeRuntimeState,
} from "./runtime";
export default definePlugin({
name: "notice-center",
async setup(ctx) {
const queue = new QueueManager(ctx.logger);
const webhookUrl = process.env.NOTICE_WEBHOOK_URL || "";
setNoticeRuntimeState({
queue,
webhookUrl,
});
return () => {
resetNoticeRuntimeState();
};
},
});skills.ts 再去读取这些状态:
// skills.ts
import type { AISkill } from "../../src";
import { getNoticeRuntimeState } from "./runtime";
const noticeSkills: AISkill[] = [
{
name: "notice_center",
description: "通知中心工具",
tools: [
{
name: "push_notice",
description: "推送一条站内通知",
parameters: {
type: "object",
properties: {
title: { type: "string" },
content: { type: "string" },
},
required: ["title", "content"],
},
handler: async (args) => {
const { queue, webhookUrl } = getNoticeRuntimeState();
if (!queue || !webhookUrl) {
return { error: "runtime is not ready" };
}
return queue.push({
title: args.title,
content: args.content,
webhookUrl,
});
},
},
],
},
];这样 skills.ts 既不会依赖 setup() 的局部闭包,又能稳定拿到真正的运行时对象。
