欢迎光临
我们一直在努力

个人AI助手插件开发实战:从零打造天气/翻译/待办三个实用插件

本文是\”个人AI助手平台\”系列第五篇。建议先阅读前四篇了解基础架构后再看本文。


一、为什么需要插件

前面的文章里我们搭好了架构、部署上了云、打通了四个IM通道。但一个只会聊天的 AI 助手太单薄了——用户问\”明天会下雨吗\”,你需要查询天气;用户说\”帮我记下这条待办\”,你需要持久化存储。

插件的价值:

  • 能力扩展:让 AI 能查天气、做翻译、管理待办——不再是一个只会聊天的花瓶
  • 热加载:新增能力不用重启服务,开发者写好插件、丢进插件目录就生效
  • 社区生态:参照 VS Code 插件市场模式,任何人都可以贡献插件

本文带你从零写出三个完整可运行的插件,并掌握插件开发范式。


二、插件规范(Plugin Specification)

先定义插件接口——所有插件必须遵守的契约:

// plugin-spec.ts
export interface PluginManifest {


/** 插件唯一标识,如 \”weather\” */
id: string;
/** 显示名称 */
name: string;
/** 版本号 */
version: string;
/** 作者 */
author: string;
/** 一句话描述 */
description: string;
/** 所需权限 */
permissions: string[];
/** 触发关键词,如 [\”天气\”, \”weather\”] */
triggers: string[];
}

export interface PluginContext {


/** 用户ID */
userId: string;
/** 所属群聊ID */
channelId?: string;
/** 插件数据存储路径(持久化用) */
dataDir: string;
/** 日志函数 */
logger: PluginLogger;
/** 发送消息到当前通道 */
sendMessage: (text: string) => Promise<void>;
}

export interface PluginLogger {


info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string, err?: Error) => void;
}

/**
* 插件基类——所有插件必须继承
*/

export abstract class BasePlugin {


abstract readonly manifest: PluginManifest;

/**
* 初始化(插件加载时调用一次)
*/

abstract onInit(ctx: PluginContext): Promise<void>;

/**
* 收到消息时调用
* @returns true 表示已处理该消息(不再向下传递)
*/

abstract onMessage(
message: string,
ctx: PluginContext
): Promise<{

handled: boolean; reply?: string }>;

/**
* 卸载插件时调用(清理资源)
*/

abstract onDestroy(): Promise<void>;
}


三、插件管理器(动态加载)

插件管理器负责扫描插件目录、加载、卸载、热更新:

// plugin-manager.ts
import * as fs from \’fs\’;
import * as path from \’path\’;
import {

BasePlugin, PluginContext, PluginManifest } from \’./plugin-spec\’;

interface LoadedPlugin {


instance: BasePlugin;
manifest: PluginManifest;
filePath: string;
lastModified: number;
}

export class PluginManager {


private plugins: Map<string, LoadedPlugin> = new Map();
private ctx: PluginContext;
private hotWatchInterval: NodeJS.Timeout | null = null;

constructor(ctx: PluginContext, private pluginsDir: string) {


this.ctx = ctx;
}

/**
* 启动:扫描并加载所有插件、启动热监听
*/

async start(): Promise<void> {


await this.loadAll();
this.startHotWatch();
this.ctx.logger.info(`插件管理器已启动,加载 ${

this.plugins.size} 个插件`);
}

/**
* 停止:卸载所有插件
*/

async stop(): Promise<void> {


if (this.hotWatchInterval) clearInterval(this.hotWatchInterval);
for (const [id, loaded] of this.plugins) {


await loaded.instance.onDestroy();
this.ctx.logger.info(`插件 ${

id} 已卸载`);
}
this.plugins.clear();
}

/**
* 处理消息——遍历所有插件,第一个处理的生效
*/

async handleMessage(message: string): Promise<string | null> {


for (const [id, loaded] of this.plugins) {


try {


const result = await loaded.instance.onMessage(message, this.ctx);
if (result.handled && result.reply) {


this.ctx.logger.info(`插件 [${

id}] 处理了消息`);
return result.reply;
}
} catch (err) {


this.ctx.logger.error(`插件 [${

id}] 处理消息出错`, err as Error);
}
}
return null; // 无插件处理
}

/**
* 扫描并加载所有插件
*/

private async loadAll(): Promise<void> {


if (!fs.existsSync(this.pluginsDir)) {


fs.mkdirSync(this.pluginsDir, {

recursive: true });
return;
}

const files = fs.readdirSync(this.pluginsDir);
for (const file of files) {


if (file.endsWith(\’.js\’) || file.endsWith(\’.ts\’)) {


await this.loadPlugin(path.join(this.pluginsDir, file));
}
}
}

/**
* 加载单个插件
*/

private async loadPlugin(filePath: string): Promise<void> {


try {


// 清除 require 缓存以支持热更新
const resolvedPath = require.resolve(filePath);
delete require.cache[resolvedPath];

const PluginClass = require(filePath).default;
const instance: BasePlugin = new PluginClass();
const manifest = instance.manifest;

if (this.plugins.has(manifest.id)) {


this.ctx.logger.warn(`插件 ${

manifest.id} 已存在,跳过重复加载`);
return;
}

await instance.onInit(this.ctx);
const stats = fs.statSync(filePath);

this.plugins.set(manifest.id, {


instance,
manifest,
filePath,
lastModified: stats.mtimeMs,
});

this.ctx.logger.info(`插件 [${

manifest.id}] v${

manifest.version} 加载成功`);
} catch (err) {


this.ctx.logger.error(`加载插件失败: ${

filePath}`, err as Error);
}
}

/**
* 启动热监听:每 3 秒检查插件文件是否变化
*/

private startHotWatch(): void {


this.hotWatchInterval = setInterval(async () => {


for (const [id, loaded] of this.plugins) {


try {


const stats = fs.statSync(loaded.filePath);
if (stats.mtimeMs > loaded.lastModified) {


this.ctx.logger.info(`检测到插件 [${

id}] 文件变化,热更新中…`);
await loaded.instance.onDestroy();
this.plugins.delete(id);
await this.loadPlugin(loaded.filePath);
}
} catch (err) {


// 文件可能被删除,忽略
}
}
}, 3000);
}

/**
* 获取已加载插件列表
*/

getLoadedPlugins(): PluginManifest[] {


return Array.from(this.plugins.values()).map(p => p.manifest);
}
}


四、实战一:天气查询插件

4.1 设计思路

  • 触发词:天气、weather、会下雨、多少度
  • 依赖:调用公开天气 API(Open-Meteo,免费无需 Key)
  • 返回格式:城市名 + 温度 + 天气状况 + 穿衣建议

4.2 完整代码

赞(0)
未经允许不得转载:171主机测评 » 个人AI助手插件开发实战:从零打造天气/翻译/待办三个实用插件
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址