直接下载对应插件目录下的 .dll 文件即可使用:
| 插件名称 | 功能描述 | 下载路径 |
|---|---|---|
| MoneyManagerPlugin | 节日里给萝莉斯包红包! | MoneyManagerPlugin/plugin/MoneyManagerPlugin.dll |
| ReminderPlugin | 设置定时提醒 | ReminderPlugin/plugin/ReminderPlugin.dll |
| SystemInfoPlugin | 获取当前操作系统的版本信息 | SystemInfoPlugin/plugin/SystemInfoPlugin.dll |
| ForegroundAppPlugin | 监视前台应用程序并将其名称提供给 AI | ForegroundAppPlugin/plugin/ForegroundAppPlugin.dll |
| AppLauncherPlugin | 允许 AI 启动应用程序 | AppLauncherPlugin/plugin/AppLauncherPlugin.dll |
| MinecraftVersionPlugin | 监听并识别我的世界版本 | MinecraftVersionPlugin/plugin/MinecraftVersionPlugin.dll |
| MarkdownViewerPlugin | 在独立窗口中渲染 Markdown 文档 | MarkdownViewerPlugin/plugin/MarkdownViewerPlugin.dll |
| WebSearchPlugin | 搜索互联网内容或获取网页内容 | WebSearchPlugin/plugin/WebSearchPlugin.dll |
| ExamplePlugin | 示例插件(开发参考) | ExamplePlugin/plugin/ExamplePlugin.dll |
欢迎来到 VPetLLM 的插件开发!通过插件,您可以扩展 VPetLLM 的功能,让您的桌宠更加智能和强大。
VPetLLM 插件是一个实现了特定接口的 .dll 文件,它允许您:
- 添加新的功能:例如,获取天气信息、查询股价、控制智能家居等。
- 与外部服务交互:通过调用 API,将外部数据集成到与桌宠的互动中。
目前,VPetLLM 支持以下类型的插件接口:
| 接口 | 说明 |
|---|---|
IVPetLLMPlugin |
基础接口,所有插件必须实现 |
IActionPlugin |
标记接口,表示可被 AI 主动调用的动作插件 |
IPluginWithData |
提供插件数据目录支持 |
IDynamicInfoPlugin |
提供动态信息给 AI 的插件 |
- IDE: 推荐使用 Visual Studio
- 项目类型: 创建
.NET 8.0类库项目(启用 WPF) - 添加引用:
VPet-Simulator.Windows.Interface.dll(通过 NuGet)VPetLLM.dll(本地引用,用于访问PluginConfigHelper)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<OutputType>Library</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="VPet-Simulator.Windows.Interface" Version="1.1.0.50" />
</ItemGroup>
<ItemGroup>
<!-- 引用 VPetLLM.dll 以使用 PluginConfigHelper -->
<Reference Include="VPetLLM">
<HintPath>你的VPetLLM.dll路径</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<!-- 可选:自动复制到 plugin 目录 -->
<Target Name="CopyPlugin" AfterTargets="Build">
<ItemGroup>
<PluginFiles Include="$(OutDir)$(TargetName).dll" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="plugin" />
</Target>
</Project>重要说明:
Microsoft.Data.Sqlite包是使用PluginConfigHelper所必需的VPetLLM.dll引用设置为Private=false,避免复制到输出目录- 插件运行时会使用主程序的
VPetLLM.dll
这是所有插件都必须实现的基础接口。
public interface IVPetLLMPlugin
{
// 插件的名称,AI 将通过这个名称来调用你的插件
// 名称应该清晰、简洁,并使用下划线(_)代替空格
string Name { get; }
// 插件的详细描述,帮助 AI 理解插件的功能
// 注意:AI 依靠此信息来生成调用插件的指令
string Description { get; }
// 插件接受的参数说明,例如 "time(int), unit(string), event(string)"
// 注意:AI 依靠此信息来生成调用插件的指令
string Parameters { get; }
// 插件的调用示例
// 注意:AI 依靠此信息来生成调用插件的指令
string Examples { get; }
// 控制插件是否启用
bool Enabled { get; set; }
// 插件文件的路径
string FilePath { get; set; }
// 插件的核心功能实现
// 当 AI 调用此插件时,这个异步方法将被执行
// 'arguments' 参数是 AI 根据 'Parameters' 定义生成的字符串
Task<string> Function(string arguments);
// 初始化方法,在插件被加载时调用
// 你可以在这里进行一些初始化操作,例如保存 VPetLLM 的实例
void Initialize(VPetLLM plugin);
// 卸载方法,在插件被卸载时调用
void Unload();
// 日志记录方法,方便调试
void Log(string message);
}这是一个标记接口,继承自 IVPetLLMPlugin。它表示该插件是一个可以被 AI 调用的"动作"。
public interface IActionPlugin : IVPetLLMPlugin
{
// 这是一个标记接口,表明该插件可以被 AI 作为动作调用
// 它没有额外的方法
}最简单的插件实现,返回固定问候语:
using System.Threading.Tasks;
using VPetLLM;
public class ExamplePlugin : IActionPlugin
{
public string Name => "example_plugin";
public string Author => "ycxom";
public string Description
{
get
{
if (_vpetLLM is null) return "一个简单的示例插件。";
switch (_vpetLLM.Settings.Language)
{
case "ja": return "呼び出されると固定の挨拶を返す簡単なサンプルプラグインです。";
case "zh-hans": return "一个简单的示例插件,在调用时返回固定的问候语。";
case "zh-hant": return "一個簡單的範例插件,在呼叫時返回固定的問候語。";
case "en":
default: return "A simple example plugin that returns a fixed greeting when called.";
}
}
}
public string Parameters => ""; // 此插件不需要任何参数
public string Examples => "Example: `<|plugin_example_plugin_begin|> <|plugin_example_plugin_end|>`";
public bool Enabled { get; set; } = true;
public string FilePath { get; set; } = "";
private VPetLLM.VPetLLM? _vpetLLM;
public void Initialize(VPetLLM.VPetLLM plugin)
{
_vpetLLM = plugin;
FilePath = plugin.PluginPath;
VPetLLM.Utils.Logger.Log("Example Plugin Initialized!");
}
public Task<string> Function(string arguments)
{
if (_vpetLLM is null) return Task.FromResult("VPetLLM instance is not initialized.");
var result = "Hello, I am an example plugin!";
_vpetLLM.Log($"ExamplePlugin: Function called. Returning: {result}");
return Task.FromResult(result);
}
public void Unload()
{
VPetLLM.Utils.Logger.Log("Example Plugin Unloaded!");
}
public void Log(string message)
{
_vpetLLM?.Log(message);
}
}支持参数解析的定时提醒插件:
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using VPetLLM;
public class ReminderPlugin : IActionPlugin
{
public string Name => "reminder";
public string Author => "ycxom";
public string Description
{
get
{
if (_vpetLLM is null) return "设置一个定时提醒。";
switch (_vpetLLM.Settings.Language)
{
case "ja": return "タイマーリマインダーを設定します。";
case "zh-hans": return "设置一个定时提醒。";
case "zh-hant": return "設置一個定時提醒。";
case "en":
default: return "Set a timed reminder.";
}
}
}
// 定义参数格式,AI 会根据此生成调用指令
public string Parameters => "time(int), unit(string, optional: seconds/minutes), event(string)";
// 提供调用示例,帮助 AI 理解如何调用
public string Examples => "Example: `<|plugin_reminder_begin|> time(10), unit(minutes), event(\"study\") <|plugin_reminder_end|>`";
public bool Enabled { get; set; } = true;
public string FilePath { get; set; } = "";
private VPetLLM.VPetLLM? _vpetLLM;
public void Initialize(VPetLLM.VPetLLM plugin)
{
_vpetLLM = plugin;
VPetLLM.Utils.Logger.Log("Reminder Plugin Initialized!");
}
public Task<string> Function(string arguments)
{
try
{
// 使用正则表达式解析参数
var timeMatch = new Regex(@"time\((\d+)\)").Match(arguments);
var unitMatch = new Regex(@"unit\((\w+)\)").Match(arguments);
var eventMatch = new Regex(@"event\(""(.*?)""\)").Match(arguments);
if (!timeMatch.Success || !eventMatch.Success)
{
return Task.FromResult("创建提醒失败:缺少 'time' 或 'event' 参数。");
}
var timeValue = int.Parse(timeMatch.Groups[1].Value);
var unit = unitMatch.Success ? unitMatch.Groups[1].Value.ToLower() : "seconds";
var message = eventMatch.Groups[1].Value;
TimeSpan delay;
switch (unit)
{
case "minute":
case "minutes":
delay = TimeSpan.FromMinutes(timeValue);
break;
case "second":
case "seconds":
default:
delay = TimeSpan.FromSeconds(timeValue);
break;
}
// 启动异步提醒任务
_ = ReminderTask(delay, message);
return Task.FromResult($"好的,我会在 {timeValue} {unit} 后提醒你 '{message}'");
}
catch (Exception e)
{
return Task.FromResult($"创建提醒失败,请检查参数: {e.Message}");
}
}
private async Task ReminderTask(TimeSpan delay, string message)
{
if (_vpetLLM is null) return;
await Task.Delay(delay);
var aiName = _vpetLLM.Settings.AiName;
var notificationTitle = $"{aiName} 提醒你";
var notificationMessage = $"该 "{message}" 了";
// 使用 Dispatcher 在 UI 线程上执行 UI 操作
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
var mainWindow = System.Windows.Application.Current.MainWindow;
if (mainWindow is not null)
{
mainWindow.Activate();
mainWindow.Topmost = true;
_ = Task.Run(async () =>
{
await Task.Delay(3000);
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => mainWindow.Topmost = false);
});
}
System.Windows.MessageBox.Show(notificationMessage, notificationTitle,
System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
});
// 让桌宠说话
var response = $"reminder_finished, Task: \"{message}\"";
await _vpetLLM.ChatCore.Chat(response, true);
}
public void Unload()
{
VPetLLM.Utils.Logger.Log("Reminder Plugin Unloaded!");
}
public void Log(string message)
{
_vpetLLM?.Log(message);
}
}如果你的插件需要保存配置或数据,可以实现此接口:
public interface IPluginWithData
{
// 插件数据目录路径,由 VPetLLM 自动设置
// 注意:从 v2.11.40 开始,推荐使用 PluginConfigHelper 管理配置
string PluginDataDir { get; set; }
}从 v2.0 开始,所有插件配置统一存储在 SQLite 数据库中,使用 PluginConfigHelper 进行管理。
using VPetLLM.Infrastructure.Configuration;
public class MyPlugin : IVPetLLMPlugin, IActionPlugin, IPluginWithData
{
private MySettings _settings = new();
public string PluginDataDir { get; set; } = ""; // 保留以兼容接口,但不再使用
public void Initialize(VPetLLM.VPetLLM plugin)
{
_vpetLLM = plugin;
// 从数据库加载配置
LoadSettings();
}
private void LoadSettings()
{
// 一行代码从数据库加载配置
_settings = PluginConfigHelper.Load<MySettings>("MyPlugin");
}
private void SaveSettings()
{
// 一行代码保存配置到数据库
PluginConfigHelper.Save("MyPlugin", _settings);
}
public void Unload()
{
// 卸载时保存配置
SaveSettings();
}
// 配置类定义
public class MySettings
{
public string ApiKey { get; set; } = "";
public int Timeout { get; set; } = 30;
public bool Enabled { get; set; } = true;
}
}| 方法 | 说明 | 返回值 |
|---|---|---|
Load<T>(pluginName) |
从数据库加载配置 | 配置对象(不存在则返回默认值) |
Save<T>(pluginName, config) |
保存配置到数据库 | bool(成功/失败) |
ExistsInDatabase(pluginName) |
检查配置是否存在 | bool |
GetDatabasePath() |
获取数据库路径 | string |
- 数据库路径:
%USERPROFILE%\Documents\VPetLLM\settings.db - 表名:
plugin_data - 类型:
"Plugin"
- ✅ 极简 API:只需 1 行代码完成读写
- ✅ 统一管理:所有配置存储在同一个数据库
- ✅ 自动迁移:启动时自动从 JSON 迁移到数据库
- ✅ 多实例支持:所有 VPet 实例共享配置
- ✅ 错误处理:内置异常处理和日志记录
- ✅ 默认值:配置不存在时自动返回
new T()
首次启动时,如果检测到 JSON 配置文件(如 PixivPlugin.json),会:
- 自动读取 JSON 内容
- 迁移到数据库
plugin_data表 - 删除 JSON 文件
- 下次启动直接从数据库加载
无需手动操作,完全自动化!
// ❌ 旧方式:手动读写 JSON 文件
private void LoadSettings()
{
if (string.IsNullOrEmpty(PluginDataDir))
return;
var path = Path.Combine(PluginDataDir, "MyPlugin.json");
if (File.Exists(path))
{
var json = File.ReadAllText(path);
_settings = JsonConvert.DeserializeObject<MySettings>(json) ?? new MySettings();
}
}
private void SaveSettings()
{
if (string.IsNullOrEmpty(PluginDataDir))
return;
var path = Path.Combine(PluginDataDir, "MyPlugin.json");
var json = JsonConvert.SerializeObject(_settings, Formatting.Indented);
File.WriteAllText(path, json);
}为什么不推荐:
- ❌ 代码冗长(20+ 行 vs 1 行)
- ❌ 需要手动处理异常
- ❌ 需要检查目录和文件存在
- ❌ 多实例配置不同步
- ❌ 难以统一管理和备份
如果你的插件需要向 AI 提供动态信息(如监控数据),可以实现此接口:
public interface IDynamicInfoPlugin
{
// 返回动态信息供 AI 参考
// 此方法会被定期调用
}参考 AppLauncherPlugin 或 WebSearchPlugin,可以通过 action(setting) 参数打开设置窗口:
public Task<string> Function(string arguments)
{
var actionMatch = new Regex(@"action\((\w+)\)").Match(arguments);
if (actionMatch.Success)
{
var action = actionMatch.Groups[1].Value.ToLower();
if (action == "setting")
{
Application.Current.Dispatcher.Invoke(() =>
{
var settingWindow = new YourSettingWindow();
settingWindow.Show();
});
return Task.FromResult("设置窗口已打开。");
}
}
// ... 其他逻辑
}- 构建: 在 Visual Studio 中,将你的项目构建为
.dll文件 - 部署: 将生成的
.dll文件复制到我的文档/VPetLLM/Plugin目录下
你可以在 VPetLLM 的设置窗口中,通过"插件"选项卡来管理插件:
- 导入: 点击"导入插件"按钮,选择你的
.dll文件 - 启用/禁用: 通过勾选插件列表中的复选框来启用或禁用插件
- 卸载: 选择一个插件,然后点击"卸载插件"按钮来删除它
AI 通过特定格式的标记来调用插件:
<|plugin_插件名称_begin|> 参数内容 <|plugin_插件名称_end|>
注意:旧格式 [:plugin]pluginName(arguments) 已被弃用,不再支持。
┌─────────────────────────────────────────────────────────────────────────┐
│ VPetLLM 插件调用流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. AI 生成响应 │
│ ↓ │
│ 2. CommandFormatParser 解析响应文本 │
│ - 使用正则表达式匹配 <|plugin_xxx_begin|>...<|plugin_xxx_end|> │
│ - 提取插件名称和参数 │
│ ↓ │
│ 3. ActionProcessor 处理命令 │
│ - 识别 plugin_xxx 格式,提取插件名称 │
│ - 查找对应的 PluginHandler │
│ ↓ │
│ 4. PluginHandler 执行插件 │
│ - 根据插件名称查找已注册的插件实例 │
│ - 调用插件的 Function(arguments) 方法 │
│ ↓ │
│ 5. 插件返回结果 │
│ - 结果格式化为 [Plugin Result: pluginName] result │
│ - 通过 ResultAggregator 聚合后回传给 AI │
│ │
└─────────────────────────────────────────────────────────────────────────┘
| 插件 | 调用格式 |
|---|---|
| reminder | <|plugin_reminder_begin|> time(10), unit(minutes), event("study") <|plugin_reminder_end|> |
| WebSearch | <|plugin_WebSearch_begin|> search|AMD 9950HX <|plugin_WebSearch_end|> |
| AppLauncher | <|plugin_AppLauncher_begin|> notepad <|plugin_AppLauncher_end|> |
| example_plugin | <|plugin_example_plugin_begin|> <|plugin_example_plugin_end|> |
插件在 VPetLLM 启动时通过 PluginManager 自动加载:
- 扫描目录:扫描
我的文档/VPetLLM/Plugin目录下的所有.dll文件 - 影子拷贝:将 DLL 复制到临时目录,避免文件锁定问题
- 加载程序集:使用
AssemblyLoadContext加载程序集(支持热卸载) - 实例化插件:查找实现
IVPetLLMPlugin接口的类型并创建实例 - 初始化:调用插件的
Initialize()方法 - 注册到 ChatCore:将插件添加到聊天核心,使 AI 可以调用
启用的插件信息会被注入到 AI 的系统提示(System Message)中:
Available Plugins:
plugin_name: 插件描述 Example: `<|plugin_xxx_begin|> ... <|plugin_xxx_end|>`
AI 根据这些信息理解如何调用插件。因此,Description、Parameters 和 Examples 属性非常重要。
插件执行完成后,结果会通过 ResultAggregator 聚合(2秒窗口),然后统一回传给 AI:
// 插件返回结果后,PluginHandler 会格式化并聚合
var formattedResult = $"[Plugin Result: {pluginName}] {result}";
ResultAggregator.Enqueue(formattedResult);这样 AI 可以根据插件返回的结果继续对话或执行后续操作。
如果你的插件需要完全接管消息处理流程(如流式处理),可以实现此接口:
public interface IPluginTakeover : IVPetLLMPlugin
{
// 是否支持接管模式
bool SupportsTakeover { get; }
// 开始接管处理
Task<bool> BeginTakeoverAsync(string initialContent);
// 处理接管期间的内容片段
Task<bool> ProcessTakeoverContentAsync(string content);
// 结束接管处理
Task<string> EndTakeoverAsync();
// 检查是否应该结束接管
bool ShouldEndTakeover(string content);
}VPetLLM 内置了插件调用限流机制,防止 AI 过度调用插件:
- 默认配置:5次/2分钟(跨消息调用)
- 同一消息内:不受限流影响(允许插件联合调用)
插件可以主动向 AI 发送消息:
// 方法 1:通过 ChatCore 发送
await _vpetLLM.ChatCore.Chat("消息内容", true);
// 方法 2:通过 PluginHandler 发送格式化消息
VPetLLM.Handlers.PluginHandler.SendPluginMessage("plugin_name", "消息内容");通过 _vpetLLM 实例可以访问桌宠的各种状态:
// 获取设置
var settings = _vpetLLM.Settings;
var aiName = settings.AiName;
var userName = settings.UserName;
var language = settings.Language;
// 获取聊天历史
var history = _vpetLLM.GetChatHistory();
// 播放 TTS
await _vpetLLM.PlayTTSAsync("要说的话");
// 获取可用动画列表
var animations = _vpetLLM.GetAvailableAnimations();插件可以通过 LLMEntry 接口直接调用 LLM 服务:
// 在插件的 Function 方法中
var response = await _vpetLLM.LLMEntry.CallAsync("你的消息");特性:
- 简单易用,一行代码即可调用
- 自动记录调用日志(调用者、消息、响应、耗时)
- 不影响主对话历史
- 支持所有 LLM 提供商(OpenAI、Ollama、Gemini、Free)
使用示例:
public async Task<string> Function(string arguments)
{
if (_vpetLLM?.LLMEntry == null)
return "LLM service not available";
// 让 LLM 分析用户输入
var analysis = await _vpetLLM.LLMEntry.CallAsync($"分析:{arguments}");
return analysis;
}日志输出:
[LLM Call] Plugin:YourPluginName calling LLM
[LLM Call] Message: 分析:...
[LLM Call] Plugin:YourPluginName - Response in 2.34s
[LLM Call] Response: ...
注意: 外部应用调用时,日志会显示为 ExternalProgram: 前缀,以区分插件调用。
- 多语言支持: 在
Description属性中根据_vpetLLM.Settings.Language返回不同语言的描述 - 错误处理: 在
Function方法中妥善处理异常,返回友好的错误信息 - 日志记录: 使用
VPetLLM.Utils.Logger.Log()或_vpetLLM.Log()记录调试信息 - UI 线程: 如需操作 UI,使用
Application.Current.Dispatcher.Invoke()或InvokeAsync() - 资源清理: 在
Unload()方法中释放资源(如 HttpClient、定时器等) - 配置管理: 使用
PluginConfigHelper管理插件配置,避免手动读写 JSON 文件
- 插件名称:使用小写字母和下划线,如
my_plugin、web_search - 参数格式:使用
参数名(类型)格式,如time(int),query(string) - 示例格式:提供完整的调用示例,帮助 AI 理解
- 配置名称:与插件目录名一致,如目录
Pixiv→ 配置名"Pixiv"
- 异步操作:耗时操作使用
async/await,避免阻塞 - 缓存:对于频繁访问的数据,考虑使用缓存
- 超时处理:网络请求设置合理的超时时间
- 资源复用:如
HttpClient,应在插件生命周期内复用 - 数据库访问:
PluginConfigHelper每次调用都创建新连接,自动管理生命周期
// ✅ 推荐:使用 PluginConfigHelper
private void LoadSettings()
{
_settings = PluginConfigHelper.Load<MySettings>("MyPlugin");
}
private void SaveSettings()
{
PluginConfigHelper.Save("MyPlugin", _settings);
}
// ❌ 不推荐:手动读写 JSON 文件
private void LoadSettings()
{
var path = Path.Combine(PluginDataDir, "MyPlugin.json");
if (File.Exists(path))
{
var json = File.ReadAllText(path);
_settings = JsonConvert.DeserializeObject<MySettings>(json);
}
}- 查看日志:检查
Debug.log文件中的插件日志 - 数据库查询:使用 SQLite 工具查看配置数据
sqlite3 "$env:USERPROFILE\Documents\VPetLLM\settings.db" "SELECT * FROM plugin_data WHERE name = 'MyPlugin';"
- 配置验证:使用
PluginConfigHelper.ExistsInDatabase("MyPlugin")检查配置是否存在