QwenPaw 提供了插件系统,允许用户扩展 QwenPaw 的功能。
插件系统支持以下扩展能力:
- Provider 插件:添加新的 LLM Provider 和模型
- Middleware 插件:注册 AgentScope
MiddlewareBase工厂,在 agent 推理循环中包裹on_acting/on_reasoning等钩子 - Hook 插件:在应用启动/关闭时执行自定义代码(app 生命周期级别,仅执行一次)
- Command 插件:注册自定义的
/command魔法命令 - HTTP API 插件:通过 FastAPI
APIRouter在/api下暴露自定义 REST 接口 - 前端扩展插件:在浏览器中运行的 JS 插件,共享宿主的 React / Ant Design 运行时,通过声明式
window.QwenPaw.*API 扩展界面——注册侧边栏菜单、页面路由、UI 插槽、聊天定制等,无需修改宿主代码 - Channel 插件:注册自定义消息频道(如 Slack、LINE)
从本地目录安装:
qwenpaw plugin install /path/to/plugin从 URL 安装(支持 ZIP 文件):
qwenpaw plugin install https://example.com/plugin.zip强制重新安装:
qwenpaw plugin install /path/to/plugin --force注意:插件操作只能在 QwenPaw 离线时执行。
qwenpaw plugin list输出示例:
Installed Plugins:
==================
my-provider (v1.0.0)
Custom LLM provider integration
Author: Developer Name
Path: /Users/user/.qwenpaw/plugins/my-provider
qwenpaw plugin info <plugin-id>qwenpaw plugin uninstall <plugin-id>每个插件至少需要两个文件:
my-plugin/
├── plugin.json # 插件清单(必需)
├── plugin.py # 入口点(后端必需)
└── README.md # 文档(推荐)
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"type": "general",
"description": "Plugin description",
"author": "Your Name",
"entry": {
"backend": "plugin.py"
},
"dependencies": [],
"qwenpaw_version": {
"min": "1.0.0",
"max": "2.1.0"
},
"meta": {}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string |
是 | 插件唯一标识,同时作为安装目录名,不能包含路径分隔符。 |
version |
string |
是 | 插件语义化版本号(例如 1.0.0)。 |
name |
string 或对象 |
否 | 显示名称,缺省取 id。也可写成 {"zh-CN": "...", "en-US": "..."},运行时按"英文优先"的顺序取第一个非空值。 |
type |
string |
否 | 取值之一:tool、provider、hook、command、frontend、general。省略时会按 meta / entry 推断(仅为兼容旧插件),新插件建议显式声明。 |
description |
string 或对象 |
否 | 插件列表里的简短描述,支持本地化对象形式(同 name)。 |
author |
string |
否 | 作者或组织名称。 |
entry.backend |
string |
否* | 相对插件目录的 Python 入口文件路径,需在其中导出 plugin。 |
entry.frontend |
string |
否* | 已构建的前端 bundle 路径(如 dist/index.js)。 |
dependencies |
string[] |
否 | Python 依赖列表,安装时通过 pip / uv 自动安装。 |
qwenpaw_version |
object |
否 | QwenPaw 版本约束(推荐)。包含 min(包含)和 max(不包含,可选)两个子字段,语义为 >=min, <max。省略 max 时默认取 {major}.{minor+1}.0。 |
min_version |
string |
否 | 遗留字段。 需要的最低 QwenPaw 版本。当 qwenpaw_version 存在时被忽略,仅为兼容第三方旧插件保留。 |
max_version |
string |
否 | 遗留字段。 不兼容的第一个 QwenPaw 版本(不包含)。配合 min_version 使用;省略时从 min_version 推导。 |
meta |
object |
否 | 自由元数据。前端 UI 与 type 推断都会读取(如 meta.tools[]、meta.hook_type、meta.provider_id)。 |
entry_point |
string |
否 | 遗留字段。 等价于 entry.backend,仅为兼容老插件保留,新插件请使用 entry.backend。 |
* entry.backend、entry.frontend(或遗留 entry_point)至少需要提供其中之一。
| 取值 | 适用场景 |
|---|---|
tool |
注册一个或多个 Agent 工具(LLM 可调用的函数)。 |
provider |
注册自定义 LLM 提供商 / 模型端点。 |
hook |
在应用启动 / 关闭时执行代码(app 生命周期级别)。 |
command |
注册 /slash 控制命令。 |
channel |
注册自定义消息频道。 |
frontend |
提供前端 JS bundle,由 UI 动态加载。 |
general |
兜底类型,用于组合型插件或不属于以上任何类别的插件。 |
# -*- coding: utf-8 -*-
"""My Plugin Entry Point."""
from qwenpaw.plugins.api import PluginApi
import logging
logger = logging.getLogger(__name__)
class MyPlugin:
"""My Plugin."""
def register(self, api: PluginApi):
"""Register plugin capabilities.
Args:
api: PluginApi instance
"""
logger.info("Registering my plugin...")
# 注册你的功能
# api.register_provider(...)
# api.register_startup_hook(...)
# api.register_shutdown_hook(...)
logger.info("✓ My plugin registered")
# Export plugin instance
plugin = MyPlugin()前端插件是运行在浏览器端的 JavaScript 扩展。与后端插件通过 Python PluginApi 注册能力不同,前端插件通过全局 window.QwenPaw.* API 声明式地扩展 Console 界面。
加载生命周期:
- Console 启动,在
window.QwenPaw上挂载 Host SDK(React、antd 等共享依赖)和注册 API(menu、route、slot、chat 等命名空间) - Console 请求
/frontend_plugin获取已启用的前端插件列表 - 逐一下载各插件的 JS bundle,通过 Blob URL 动态导入执行
- 插件代码执行,调用
window.QwenPaw.*注册菜单、路由、聊天定制等 UI 扩展 - 注册立即生效——菜单出现在侧边栏、路由可导航、聊天区域呈现定制内容
插件无需声明使用了哪些扩展点;系统通过 pluginId 自动追踪所有注册。卸载或禁用插件时,通过 dispose() 或 chat.disposeAll(pluginId) 清理全部注册。
设计特点:
| 特点 | 说明 |
|---|---|
| 共享运行时 | React、ReactDOM、Ant Design 由宿主提供,插件无需打包,避免版本冲突和体积膨胀 |
| 声明式注册 | 三个核心动词:set(设置 / 合并属性)、render(替换渲染)、add(追加项目) |
| pluginId 隔离 | 所有注册方法以 pluginId 为第一参数,系统据此追踪来源、检测冲突、支持按插件清理 |
| 可撤销 | 每个注册返回 { dispose() } 对象,调用即撤销,支持热重载和插件卸载 |
| 国际化 | 文本字段支持 Localized<T> 类型——传入 (locale) => string 函数按语言返回不同值 |
扩展点一览:
| 命名空间 | 能力 | 典型用途 |
|---|---|---|
host |
共享依赖、React Hooks、认证请求 | 获取 React / antd、读取主题和语言、调用后端 API |
menu |
侧边栏菜单项 | 添加导航入口 |
route |
页面路由 | 注册新页面、包装已有页面 |
slot |
通用 UI 插槽 | 向 Header / Sidebar 等预设位置注入内容 |
chat.welcome |
欢迎界面 | 自定义问候语、推荐提示词 |
chat.theme |
聊天主题色 | 更换主色调 |
chat.leftHeader / rightHeader |
聊天头部 | 设置品牌 Logo、添加操作按钮 |
chat.sender |
输入框 | 自定义 placeholder、输入建议 |
chat.actions / requestActions |
消息操作按钮 | 在消息下方添加自定义操作 |
chat.requestPayload |
外发聊天请求体 | 请求发送到后端前追加或改写自定义字段 |
chat.request / response |
消息气泡 | 在消息前后追加内容或完全替换渲染 |
chat.toolRender |
工具调用渲染 | 自定义工具结果展示(如天气卡片) |
chat.card |
自定义卡片 | 注册新的卡片类型 |
audit |
审计与调试 | 查看所有扩展注册记录 |
my-plugin/
├── plugin.json # 插件清单(必需)
├── src/
│ └── index.tsx # 入口点,调用 window.QwenPaw.* API
├── package.json # 依赖声明
├── tsconfig.json # TypeScript 配置
└── vite.config.ts # 构建配置
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"type": "frontend",
"author": "Your Name",
"entry": { "frontend": "dist/index.js" }
}插件入口文件在加载时执行,通过 window.QwenPaw.* API 注册扩展:
const { React, antd } = window.QwenPaw.host;
const pluginId = "my-plugin";
// 调用 window.QwenPaw.* API 注册菜单、路由、聊天定制等
// 详见下方「前端扩展 API」package.json:
{
"name": "my-plugin",
"version": "1.0.0",
"scripts": { "build": "vite build" },
"devDependencies": {
"vite": "^5.0.0",
"typescript": "^5.0.0",
"@vitejs/plugin-react": "^4.0.0"
}
}tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react",
"strict": false,
"skipLibCheck": true
}
}vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react({ jsxRuntime: "classic" })],
build: {
lib: {
entry: "src/index.tsx",
formats: ["es"],
fileName: () => "index.js",
},
rollupOptions: { external: ["react", "react-dom"] },
},
});jsxRuntime: "classic" 将 JSX 编译为 React.createElement,使用宿主提供的 React;external 避免打包 React,使用应用已加载的版本。
npm install && npm run build
cp -r . ~/.qwenpaw/plugins/my-plugin/
qwenpaw app可将 console/src/plugins/types/qwenpaw.d.ts 复制到插件项目中作为 qwenpaw-host.d.ts,获得完整的类型提示。
前端插件通过 window.QwenPaw.* API 扩展 Console 界面,无需修改宿主代码。所有注册方法第一个参数是 pluginId,每个注册返回 { dispose() } 对象用于撤销。
宿主共享依赖,插件无需打包这些库:
host.React // React 库
host.ReactDOM // ReactDOM 库
host.antd // Ant Design 组件库
host.antdIcons // Ant Design 图标库
host.apiBaseUrl // API 基础 URL
host.getApiUrl(path: string) // 拼接完整 API URL
host.getApiToken(): string | null // 获取当前认证 TokenReact Hooks(在 React 组件内使用):
const theme = window.QwenPaw.host.useTheme(); // "light" | "dark"
const locale = window.QwenPaw.host.useLocale(); // "zh" | "en"
const agent = window.QwenPaw.host.useSelectedAgent(); // { id: string }
const session = window.QwenPaw.host.useCurrentSession(); // { id: string } | null命令式获取(可在任意位置调用):
const agentId = window.QwenPaw.host.getSelectedAgentId();
const sessionId = window.QwenPaw.host.getCurrentSessionId();认证代理请求(自动注入 Authorization 和 X-Agent-Id 请求头):
const resp = await window.QwenPaw.host.fetch("/api/v1/my-endpoint", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "test" }),
});
const data = await resp.json();| 方法 | 签名 | 说明 |
|---|---|---|
add |
(pluginId, item | item[]): Disposable |
添加菜单项 |
replace |
(pluginId, targetId, item): Disposable |
替换已有菜单项 |
remove |
(targetId): void |
移除菜单项 |
snapshot |
(location?): MenuItem[] |
获取当前菜单快照 |
MenuItem 参数:
{
id: string; // 全局唯一,如 "my-plugin.foo"
label: string | (() => ReactNode);
icon?: ReactComponent | ReactNode;
route?: string; // 点击时导航到的路由 id
parentId?: string; // 挂在哪个分组下
location?: "primary.agentScoped" | "primary.settings" | "userMenu";
before?: string; // 排在某个 id 之前
after?: string; // 排在某个 id 之后
order?: number; // 数值越小越靠前
visible?: () => boolean; // 动态控制显隐
isGroup?: boolean; // 作为分组标题
divider?: boolean; // 渲染为水平分割线
}| 方法 | 签名 | 说明 |
|---|---|---|
add |
(pluginId, route | route[]): Disposable |
注册新路由 |
replace |
(pluginId, targetId, component): Disposable |
替换已有路由的组件 |
wrap |
(pluginId, targetId, wrapper): Disposable |
包装已有路由(洋葱模式) |
remove |
(targetId): void |
移除路由 |
Route 参数:
{
id: string; // 全局唯一,如 "my-plugin.home"
path: string; // URL 路径,支持 react-router 模式
component: React.ComponentType; // 页面组件
}wrap 示例(为已有页面加顶部 banner):
window.QwenPaw.route.wrap("my-plugin", "core.chat", (Inner) => {
return () => (
<div>
<div style={{ background: "#fff3cd", padding: 8, textAlign: "center" }}>
Beta Feature
</div>
<Inner />
</div>
);
});| 方法 | 签名 | 说明 |
|---|---|---|
fill |
(pluginId, name, render, opts?): Disposable |
向插槽追加内容(可多个共存) |
replace |
(pluginId, name, render, opts?): Disposable |
替换插槽内容(最后注册的生效,屏蔽所有 fill) |
snapshot |
(): SlotInfo[] |
获取所有已注册的插槽信息 |
内置插槽:
| 插槽名 | 类型 | UI 位置 |
|---|---|---|
header.logo |
replace | 顶部导航栏最左侧 |
header.left |
fill | 顶部导航栏左区(Logo 右边) |
header.right |
fill | 顶部导航栏右区(设置按钮左边) |
sider.top |
fill | 侧边栏顶部(Agent 选择器下方) |
sider.bottom |
fill | 侧边栏底部(菜单下方) |
content.statusBar |
fill | 主内容区顶部 |
overlay.global |
fill | 全局覆盖层 |
示例:
// 替换 Header Logo
window.QwenPaw.slot.replace("my-plugin", "header.logo", (defaultLogo) => {
return <img src="https://example.com/logo.svg" style={{ height: 24 }} />;
});window.QwenPaw.chat.welcome.set("my-plugin", {
greeting: (locale) => (locale.startsWith("zh") ? "你好!" : "Hello!"),
description: "I specialize in data analysis.",
avatar: "https://example.com/avatar.png",
nick: "My Bot",
prompts: [
{ label: "分析数据", value: "请分析上传的数据集" },
{ label: "生成图表", value: "根据数据创建柱状图" },
],
});
// 或完全替换欢迎界面
window.QwenPaw.chat.welcome.render("my-plugin", (props) => {
return <div>Custom Welcome</div>;
});window.QwenPaw.chat.theme.set("my-plugin", {
colorPrimary: "#1890ff",
});// 设置左上角标题
window.QwenPaw.chat.leftHeader.set("my-plugin", {
title: "My Brand",
logo: <img src="logo.svg" style={{ height: 20 }} />,
});
// 在右上角添加按钮
window.QwenPaw.chat.rightHeader.add(
"my-plugin",
<button
onClick={() => alert("Plugin action!")}
style={{ border: "none", background: "none", cursor: "pointer" }}
>
My Button
</button>,
{ id: "my-plugin.btn", order: 10 },
);// 自定义 placeholder
window.QwenPaw.chat.sender.set("my-plugin", {
placeholder: "Ask me anything...",
disclaimer: "Responses may not be accurate.",
});
// 添加输入建议
window.QwenPaw.chat.sender.addSuggestion("my-plugin", {
id: "my-plugin.suggestions",
items: [
{ label: "/analyze", value: "analyze" },
{ label: "/visualize", value: "visualize" },
],
});// AI 回复消息下方添加操作按钮
window.QwenPaw.chat.actions.add("my-plugin", {
id: "my-plugin.star",
icon: <span>⭐</span>,
onClick: ({ data }) => console.log("Starred:", data),
});
// 用户消息下方添加操作按钮
window.QwenPaw.chat.requestActions.add("my-plugin", {
id: "my-plugin.edit",
icon: <span>✏️</span>,
onClick: ({ data }) => console.log("Edit:", data),
});使用 chat.requestPayload.add 可以在 Console 将聊天请求发送到后端前改写 requestBody。多个转换函数会按 order 从小到大执行,入参包含当前 payload、解析后的 sessionId 和 selectedAgent。
window.QwenPaw.chat.requestPayload.add(
"my-plugin",
({ payload, sessionId, selectedAgent }) => ({
...payload,
request_context: {
session_id: sessionId,
agent_id: selectedAgent,
datasource_id: "ds-123",
},
}),
{ id: "my-plugin.request-context", order: 10 },
);转换函数返回新对象时会替换当前请求体;返回 undefined 时保持请求体不变。建议使用全局唯一的 id,方便审计和卸载时清理。
// 设置默认 AI 回复的头像和昵称
// 当前会复用 welcome.avatar / welcome.nick,因为默认 ResponseCard 读取这两个字段
window.QwenPaw.chat.response.set("my-plugin", {
avatar: "https://example.com/bot-avatar.png",
nick: "My Bot",
});
// 在用户消息前方追加内容
window.QwenPaw.chat.request.prepend("my-plugin", ({ data }) => {
return <div style={{ fontSize: 10, color: "#999" }}>User</div>;
});
// 在最新 AI 回复下方追加信息条
window.QwenPaw.chat.response.append("my-plugin", ({ data, isLast }) => {
if (!isLast) return null;
return (
<div
style={{
background: "#e3f2fd",
padding: "4px 8px",
borderRadius: 4,
fontSize: 12,
}}
>
Powered by My Plugin
</div>
);
});
// 完全替换用户消息渲染(可调用 fallback() 保留默认渲染)
window.QwenPaw.chat.request.render("my-plugin", ({ data, fallback }) => {
return (
<div style={{ border: "1px dashed #ccc", borderRadius: 8, padding: 4 }}>
{fallback()}
</div>
);
});// 注册自定义工具结果渲染组件(props 包含 result, sessionId, messageId)
window.QwenPaw.chat.toolRender("my-plugin", "get_weather", ({ result }) => {
const data = typeof result === "string" ? JSON.parse(result) : result;
return (
<div style={{ padding: 12, border: "1px solid #e8e8e8", borderRadius: 8 }}>
{data.city}: {data.temperature}°C
</div>
);
});window.QwenPaw.chat.card("my-plugin", "my-card", MyCardComponent);// 查看扩展注册记录
console.table(window.QwenPaw.audit.overrides());
// 清理插件的所有 Chat 扩展注册
window.QwenPaw.chat.disposeAll("my-plugin");所有支持 Localized<T> 类型的字段可传入函数,按语言返回不同值:
window.QwenPaw.chat.welcome.set("my-plugin", {
greeting: (locale) => (locale.startsWith("zh") ? "你好!" : "Hello!"),
});| 错误 | 原因 | 解决 |
|---|---|---|
e.item.render is not a function |
render/prepend/append 传了非函数 | 确保传入 React 组件或返回 ReactNode 的函数 |
duplicate id |
两次 add 使用了相同 id |
使用全局唯一 id(推荐 pluginId.xxx 格式) |
| Hook 在组件外调用 | useTheme() 等在非 React 上下文使用 |
改用 getSelectedAgentId() 等命令式 API |
假设你想接入一个企业内部的 LLM 服务。
mkdir my-llm-provider
cd my-llm-provider{
"id": "my-llm-provider",
"name": "My LLM Provider",
"version": "1.0.0",
"type": "provider",
"description": "Custom LLM provider for enterprise",
"author": "Your Name",
"entry": {
"backend": "plugin.py"
},
"dependencies": ["httpx>=0.24.0"],
"qwenpaw_version": {
"min": "1.0.0",
"max": "2.1.0"
},
"meta": {
"api_key_url": "https://example.com/get-api-key",
"api_key_hint": "Get your API key from example.com"
}
}# -*- coding: utf-8 -*-
"""My LLM Provider Implementation."""
from qwenpaw.providers.openai_provider import OpenAIProvider
from qwenpaw.providers.provider import ModelInfo
from typing import List
class MyLLMProvider(OpenAIProvider):
"""My custom LLM provider (OpenAI-compatible)."""
def __init__(self, **kwargs):
"""Initialize provider."""
super().__init__(**kwargs)
@classmethod
def get_default_models(cls) -> List[ModelInfo]:
"""获取默认模型列表。"""
return [
ModelInfo(
id="my-model-v1",
name="My Model V1",
supports_multimodal=False,
supports_image=False,
supports_video=False,
),
ModelInfo(
id="my-model-v2",
name="My Model V2",
supports_multimodal=True,
supports_image=True,
supports_video=False,
),
]# -*- coding: utf-8 -*-
"""My LLM Provider Plugin Entry Point."""
import importlib.util
import logging
import os
from qwenpaw.plugins.api import PluginApi
logger = logging.getLogger(__name__)
class MyLLMProviderPlugin:
"""My LLM Provider Plugin."""
def register(self, api: PluginApi):
"""Register the provider.
Args:
api: PluginApi instance
"""
logger.info("Registering My LLM Provider...")
# 从同一目录加载 provider 模块
plugin_dir = os.path.dirname(os.path.abspath(__file__))
provider_path = os.path.join(plugin_dir, "provider.py")
spec = importlib.util.spec_from_file_location(
"my_provider", provider_path
)
provider_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(provider_module)
MyLLMProvider = provider_module.MyLLMProvider
# Register provider
api.register_provider(
provider_id="my-llm",
provider_class=MyLLMProvider,
label="My LLM",
base_url="https://api.example.com/v1",
)
logger.info("✓ My LLM Provider registered")
# Export plugin instance
plugin = MyLLMProviderPlugin()# 安装插件
qwenpaw plugin install my-llm-provider
# 启动 QwenPaw
qwenpaw app
# 在 Web UI 中配置 API Key
# 然后就可以使用新的 Provider 了假设你想在 QwenPaw 启动时初始化一个监控服务。
mkdir monitoring-hook
cd monitoring-hook{
"id": "monitoring-hook",
"name": "Monitoring Hook",
"version": "1.0.0",
"type": "hook",
"description": "Initialize monitoring service at startup",
"author": "Your Name",
"entry": {
"backend": "plugin.py"
},
"dependencies": [],
"qwenpaw_version": {
"min": "1.0.0",
"max": "2.1.0"
}
}# -*- coding: utf-8 -*-
"""Monitoring Hook Plugin Entry Point."""
from qwenpaw.plugins.api import PluginApi
import logging
logger = logging.getLogger(__name__)
class MonitoringHookPlugin:
"""Monitoring Hook Plugin."""
def register(self, api: PluginApi):
"""Register the monitoring hook.
Args:
api: PluginApi instance
"""
logger.info("Registering monitoring hook...")
def startup_hook():
"""Startup hook to initialize monitoring."""
try:
logger.info("=== Monitoring Service Initialization ===")
# 初始化你的监控服务
# from my_monitoring import init_monitoring
# init_monitoring(app_name="QwenPaw")
logger.info("✓ Monitoring initialized successfully")
except Exception as e:
logger.error(
f"Failed to initialize monitoring: {e}",
exc_info=True,
)
# 注册启动钩子(priority=0 表示最高优先级)
api.register_startup_hook(
hook_name="monitoring_init",
callback=startup_hook,
priority=0,
)
logger.info("✓ Monitoring hook registered")
# Export plugin instance
plugin = MonitoringHookPlugin()qwenpaw plugin install monitoring-hook
qwenpaw app假设你想添加一个 /status 命令来查看系统状态。
mkdir status-command
cd status-command{
"id": "status-command",
"name": "Status Command",
"version": "1.0.0",
"type": "command",
"description": "Custom status command",
"author": "Your Name",
"entry": {
"backend": "plugin.py"
},
"dependencies": [],
"qwenpaw_version": {
"min": "1.0.0",
"max": "2.1.0"
}
}# -*- coding: utf-8 -*-
"""Status Command Plugin Entry Point."""
import logging
from qwenpaw.plugins.api import PluginApi
logger = logging.getLogger(__name__)
class StatusCommandPlugin:
"""Status Command Plugin."""
def register(self, api: PluginApi):
"""Register the status command."""
from qwenpaw.runtime.commands.control.base import (
BaseControlCommandHandler,
)
class StatusCommandHandler(BaseControlCommandHandler):
command_name = "status"
help_text = "Check system status"
async def handle(self, ctx, args: str):
from agentscope.message import Msg
return Msg(
name="system",
role="assistant",
content="System is running normally.",
)
api.register_control_command(
handler=StatusCommandHandler(),
priority_level=10,
)
logger.info("✓ Status command registered: /status")
# Export plugin instance
plugin = StatusCommandPlugin()qwenpaw plugin install status-command
qwenpaw app
# 使用命令
/status向侧边栏添加一个欢迎页面。构建工具链文件(package.json、tsconfig.json、vite.config.ts)参考上方「前端插件 > 构建工具链」。
plugin.json:
{
"id": "welcome-plugin",
"name": "Welcome Plugin",
"version": "1.0.0",
"type": "frontend",
"description": "Welcome page plugin",
"author": "Your Name",
"entry": { "frontend": "dist/index.js" }
}src/index.tsx:
const { React, antd } = window.QwenPaw.host;
const { Typography, Card } = antd;
const pluginId = "welcome-plugin";
const WelcomePage = () => {
const theme = window.QwenPaw.host.useTheme();
return (
<Card
style={{
maxWidth: 480,
margin: "40px auto",
background: theme === "dark" ? "#1f1f1f" : "#fff",
}}
>
<Typography.Title level={2}>Welcome to QwenPaw</Typography.Title>
<Typography.Paragraph>插件系统运行正常!</Typography.Paragraph>
</Card>
);
};
window.QwenPaw.menu.add(pluginId, {
id: "welcome-plugin.home",
label: "Welcome",
icon: "spark-home-line",
route: "welcome-plugin.home",
});
window.QwenPaw.route.add(pluginId, {
id: "welcome-plugin.home",
path: "/welcome-plugin/home",
component: WelcomePage,
});npm install && npm run build
cp -r . ~/.qwenpaw/plugins/welcome-plugin/
qwenpaw app自定义 Agent 工具调用结果的展示方式。项目结构同示例 4,仅 src/index.tsx 不同。
src/index.tsx:
const { React, antd } = window.QwenPaw.host;
const { Card, Descriptions } = antd;
const pluginId = "tool-render-plugin";
window.QwenPaw.chat.toolRender(pluginId, "get_weather", ({ result }) => {
const data = typeof result === "string" ? JSON.parse(result) : result;
return (
<Card title="天气信息" size="small" style={{ marginTop: 8, maxWidth: 400 }}>
<Descriptions column={1} size="small">
<Descriptions.Item label="城市">{data.city}</Descriptions.Item>
<Descriptions.Item label="温度">{data.temperature}°C</Descriptions.Item>
<Descriptions.Item label="天气">{data.weather}</Descriptions.Item>
</Descriptions>
</Card>
);
});定制对话页面的欢迎语、描述和推荐提示词。项目结构同示例 4,仅 src/index.tsx 不同。
src/index.tsx:
const pluginId = "custom-greeting-plugin";
window.QwenPaw.chat.welcome.set(pluginId, {
greeting: (locale) =>
locale.startsWith("zh")
? "你好!我是定制版 QwenPaw"
: "Hello! I'm customized QwenPaw",
description: "这是一个定制化的聊天助手",
prompts: [
{ label: "分析代码", value: "帮我分析这段代码" },
{ label: "单元测试", value: "写一个单元测试" },
{ label: "优化逻辑", value: "优化这段逻辑" },
],
});后端插件可以通过注册 fastapi.APIRouter 暴露自己的 HTTP 接口。路由会挂载在
/api 加上你指定的前缀下,与 QwenPaw 核心 API 使用同一个 FastAPI 应用,因此
共享 CORS、鉴权等设置,并会出现在 /openapi.json 与 /docs 中。
下面示例增加一个简单的 /api/pets 接口:列出宠物,并支持新增。
mkdir pet-api-plugin && cd pet-api-plugin{
"id": "pet-api-plugin",
"name": "Pet API Plugin",
"version": "1.0.0",
"type": "general",
"description": "Expose a small REST API under /api/pets",
"author": "Your Name",
"entry": {
"backend": "plugin.py"
},
"dependencies": [],
"qwenpaw_version": {
"min": "1.1.5",
"max": "2.1.0"
}
}# -*- coding: utf-8 -*-
"""Pet API Plugin Entry Point."""
import logging
from typing import List
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from qwenpaw.plugins.api import PluginApi
logger = logging.getLogger(__name__)
class Pet(BaseModel):
"""Pet model."""
id: int
name: str
species: str
class PetCreate(BaseModel):
"""Pet creation payload."""
name: str
species: str
_PETS: List[Pet] = [
Pet(id=1, name="Mochi", species="cat"),
Pet(id=2, name="Bao", species="dog"),
]
def build_router() -> APIRouter:
"""Build the plugin's APIRouter.
Routes are mounted under ``/api`` + the prefix passed to
``register_http_router``. With ``prefix="/pets"`` the handlers
below are served at ``/api/pets`` and ``/api/pets/{pet_id}``.
"""
router = APIRouter()
@router.get("", response_model=List[Pet])
def list_pets() -> List[Pet]:
"""Return all pets."""
return list(_PETS)
@router.get("/{pet_id}", response_model=Pet)
def get_pet(pet_id: int) -> Pet:
"""Return a single pet by id."""
for pet in _PETS:
if pet.id == pet_id:
return pet
raise HTTPException(status_code=404, detail="Pet not found")
@router.post("", response_model=Pet, status_code=201)
def create_pet(payload: PetCreate) -> Pet:
"""Create a new pet."""
new_id = (max((p.id for p in _PETS), default=0)) + 1
pet = Pet(id=new_id, name=payload.name, species=payload.species)
_PETS.append(pet)
return pet
return router
class PetApiPlugin:
"""Pet API Plugin."""
def register(self, api: PluginApi):
"""Register the HTTP router.
Args:
api: PluginApi instance
"""
logger.info("Registering Pet API plugin...")
api.register_http_router(
build_router(),
prefix="/pets",
tags=["pets"],
)
logger.info("✓ Pet API registered at /api/pets")
# Export plugin instance
plugin = PetApiPlugin()qwenpaw plugin install pet-api-plugin启动 QwenPaw 后,可在终端用 curl 测试(端口请按你本地实际为准,例如 8088):
# 列出全部宠物
curl http://127.0.0.1:8088/api/pets
# 按 id 查询
curl http://127.0.0.1:8088/api/pets/1
# 新增宠物(POST 到集合路径 /api/pets)
curl -X POST http://127.0.0.1:8088/api/pets \
-H "Content-Type: application/json" \
-d '{"name": "Luna", "species": "rabbit"}'说明:
prefix必须以/开头,且不能仅为/,应使用有语义的片段(如/pets)。完整路径恒为/api+ 你的prefix。- 每个前缀只能被一个插件占用;重复注册相同前缀会抛出
ValueError。 tags可选;省略时路由在 OpenAPI 中会默认打上plugin:<插件 id>标签。- 插件卸载或禁用时会自动卸载对应路由。
本示例展示如何注册一个 on_acting middleware,当设置环境变量 QWENPAW_TRACE 时记录每次 tool call 的名称、参数和执行耗时。
plugin.json:
{
"id": "middleware-demo-tracing",
"name": "Tracing Middleware Demo",
"version": "1.0.0",
"description": "Demo: logs tool calls with execution timing to a trace file",
"author": "QwenPaw Team",
"type": "general",
"entry": {
"backend": "tracing_plugin.py"
},
"dependencies": [],
"qwenpaw_version": {
"min": "1.0.0",
"max": "2.1.0"
}
}tracing_plugin.py:
import os
import time
from pathlib import Path
from typing import Any, AsyncGenerator, Callable
from agentscope.middleware import MiddlewareBase
from qwenpaw.plugins.api import PluginApi
class TracingMiddleware(MiddlewareBase):
"""Logs tool call name, input, and execution duration."""
def __init__(self, trace_file: Path) -> None:
self._trace_file = trace_file
self._trace_file.parent.mkdir(parents=True, exist_ok=True)
async def on_acting(
self,
agent: Any,
input_kwargs: dict[str, Any],
next_handler: Callable[..., AsyncGenerator[Any, None]],
) -> AsyncGenerator[Any, None]:
tool_call = input_kwargs["tool_call"]
tool_name = getattr(tool_call, "name", str(tool_call))
tool_input = getattr(tool_call, "input", "")
start = time.perf_counter()
try:
async for item in next_handler():
yield item
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
line = f"[{time.strftime('%H:%M:%S')}] {tool_name}({tool_input[:100]}) — {elapsed_ms:.1f}ms\n"
with open(self._trace_file, "a", encoding="utf-8") as f:
f.write(line)
def _tracing_factory(ctx: Any, agent_config: Any) -> TracingMiddleware | None:
"""Create TracingMiddleware when QWENPAW_TRACE env var is set."""
if not os.environ.get("QWENPAW_TRACE"):
return None
workspace_dir = getattr(ctx, "workspace_dir", None)
if workspace_dir is None:
return None
trace_file = Path(workspace_dir) / ".qwenpaw" / "trace.log"
return TracingMiddleware(trace_file=trace_file)
class TracingPlugin:
def register(self, api: PluginApi) -> None:
api.register_middleware(_tracing_factory, priority=50)
plugin = TracingPlugin()要点:
- 条件激活:工厂函数检测环境变量
QWENPAW_TRACE,仅设置时启用 priority=50:比默认优先级更高(数值更小 = 更靠外层),确保 tracing 包裹其他 middlewareon_acting钩子:在 tool call 执行前/后测量耗时- 完整源码参见
plugins/middleware-demo/tracing-middleware/tracing_plugin.py
本示例展示如何注册一个 on_reasoning middleware,捕获并打印模型的思维链。
plugin.json:
{
"id": "middleware-demo-thinking-log",
"name": "Thinking Log Middleware Demo",
"version": "1.0.0",
"description": "Demo: prints model reasoning steps to stdout",
"author": "QwenPaw Team",
"type": "general",
"entry": {
"backend": "thinking_log_plugin.py"
},
"dependencies": [],
"qwenpaw_version": {
"min": "1.0.0",
"max": "2.1.0"
}
}thinking_log_plugin.py:
import sys
from typing import Any, AsyncGenerator, Callable
from agentscope.middleware import MiddlewareBase
from agentscope.event import ThinkingBlockDeltaEvent, TextBlockDeltaEvent
from qwenpaw.plugins.api import PluginApi
class ThinkingLogMiddleware(MiddlewareBase):
"""Prints reasoning stream events to stdout."""
async def on_reasoning(
self,
agent: Any,
input_kwargs: dict[str, Any],
next_handler: Callable[..., AsyncGenerator[Any, None]],
) -> AsyncGenerator[Any, None]:
async for item in next_handler():
if isinstance(item, ThinkingBlockDeltaEvent):
print(f"[THINKING] {item.delta}", end="", file=sys.stdout, flush=True)
elif isinstance(item, TextBlockDeltaEvent):
print(f"[TEXT] {item.delta}", end="", file=sys.stdout, flush=True)
yield item
def _thinking_log_factory(ctx: Any, agent_config: Any) -> ThinkingLogMiddleware:
"""Always create the middleware (unconditional activation)."""
return ThinkingLogMiddleware()
class ThinkingLogPlugin:
def register(self, api: PluginApi) -> None:
api.register_middleware(_thinking_log_factory, priority=80)
plugin = ThinkingLogPlugin()要点:
- 无条件激活:工厂始终返回实例,适用于所有请求
on_reasoning钩子:在模型推理阶段捕获流式事件(ThinkingBlockDeltaEvent为思维链,TextBlockDeltaEvent为文本响应)- 实时打印:每收到一个 delta 事件即打印,同时 yield 给下游,不阻塞流式响应
- 完整源码参见
plugins/middleware-demo/thinking-log-middleware/thinking_log_plugin.py
Channel 插件可以为 QwenPaw 添加新的消息平台。注册后的频道会在控制台 UI 中与内置 频道(钉钉、Telegram 等)一起显示,支持同样的启用/禁用和配置方式。
mkdir sample-channel-plugin && cd sample-channel-plugin{
"id": "sample-channel",
"name": "Sample Channel",
"version": "1.0.0",
"type": "channel",
"description": "Sample messaging channel integration for QwenPaw",
"author": "Your Name",
"entry": {
"backend": "plugin.py"
},
"dependencies": ["sample-sdk>=1.0.0"],
"qwenpaw_version": {
"min": "1.1.5",
"max": "2.1.0"
}
}Channel 类必须实现 BaseChannel 的契约,核心方法包括:
from_config(cls, process, config, ...)— 类方法,从保存的配置创建实例。ChannelManager启动时通过它实例化你的频道。start()/stop()— 生命周期钩子,频道启用/禁用时调用。send(to_handle, text, meta)— 向用户/会话发送消息。
# -*- coding: utf-8 -*-
"""Sample 频道实现。"""
import logging
from pathlib import Path
from typing import Optional
from qwenpaw.app.channels.base import (
BaseChannel,
OnReplySent,
ProcessHandler,
)
from qwenpaw.app.channels.renderer import ChannelDisplayConfig
logger = logging.getLogger(__name__)
class SampleChannel(BaseChannel):
"""Sample 消息频道。"""
channel = "sample" # 唯一 key,必须与 config key 一致
def __init__(
self,
process: ProcessHandler,
enabled: bool = True,
bot_token: str = "",
signing_secret: str = "",
bot_prefix: str = "",
on_reply_sent: OnReplySent = None,
display_config: ChannelDisplayConfig | None = None,
**kwargs,
):
super().__init__(
process,
on_reply_sent=on_reply_sent,
display_config=display_config,
)
self.enabled = enabled
self.bot_prefix = bot_prefix
self.bot_token = bot_token
self.signing_secret = signing_secret
@classmethod
def from_config(
cls,
process: ProcessHandler,
config,
on_reply_sent: OnReplySent = None,
display_config: ChannelDisplayConfig | None = None,
workspace_dir: Optional[Path] = None,
) -> "SampleChannel":
"""从配置创建实例。
注意:插件频道的 ``config`` 是 ``types.SimpleNamespace`` 对象
(不是 dict),请使用 ``getattr(config, "field", default)``
安全读取字段。
"""
return cls(
process=process,
enabled=getattr(config, "enabled", False),
bot_token=getattr(config, "bot_token", ""),
signing_secret=getattr(config, "signing_secret", ""),
bot_prefix=getattr(config, "bot_prefix", ""),
on_reply_sent=on_reply_sent,
display_config=display_config
or ChannelDisplayConfig.from_config(config),
)
async def start(self):
"""启动 Sample 事件监听。"""
logger.info("Sample channel starting (token=%s...)", self.bot_token[:8])
# 在此启动你的平台 API 客户端
async def stop(self):
"""停止 Sample 事件监听。"""
logger.info("Sample channel stopping")
async def send(self, to_handle: str, text: str, meta=None):
"""向 Sample 用户或频道发送消息。"""
logger.info("Sending to sample %s: %s", to_handle, text[:50])
# 使用 sample-sdk 发送消息重要:
config参数类型 — 插件频道的from_config()收到的config是types.SimpleNamespace对象(不是 dict 或 Pydantic model)。框架会将BaseChannelConfig的默认值与用户保存的配置合并后传入。请始终使用getattr(config, "field", default)安全读取字段。
# -*- coding: utf-8 -*-
"""Sample Channel 插件入口。"""
import logging
from qwenpaw.plugins.api import PluginApi
logger = logging.getLogger(__name__)
class SampleChannelPlugin:
"""Sample Channel 插件。"""
def register(self, api: PluginApi):
"""注册 Sample 频道。"""
from .channel import SampleChannel
api.register_channel(
channel_class=SampleChannel,
label="Sample",
description="Sample messaging channel integration",
icon="https://example.com/sample-icon.png", # 可选:卡片图标(仅 http/https)
doc_url={ # 可选:文档链接,支持纯字符串或本地化字典(仅 http/https)
"zh": "https://example.com/docs?lang=zh",
"en": "https://example.com/docs?lang=en",
},
config_fields=[
{
"name": "bot_token",
"label": "Bot Token",
"type": "password",
"required": True,
"placeholder": "your-bot-token-here",
"help": "Bot access token",
},
{
"name": "signing_secret",
"label": "Signing Secret",
"type": "password",
"required": True,
"help": "Signing secret for request verification",
},
{
"name": "streaming_enabled",
"label": {
"zh-CN": "流式输出",
"en-US": "Streaming Output",
},
"type": "switch",
"required": False,
"default": False,
},
],
)
logger.info("✓ Sample channel registered")
plugin = SampleChannelPlugin()qwenpaw plugin install sample-channel-plugin
qwenpaw app启动后,在控制台的 Control → Channels 中可以看到 Sample 频道卡片,点击即可 填写凭证并启用。
如果你的频道需要接收 HTTP 回调(如你的平台事件 API),可以在同一个插件中 注册 FastAPI 路由:
from fastapi import APIRouter
def register(self, api: PluginApi):
from .channel import SampleChannel
api.register_channel(channel_class=SampleChannel, ...)
# 在 /api/sample/events 挂载 webhook 端点
router = APIRouter()
@router.post("/events")
async def sample_events(request):
body = await request.json()
# 处理事件验证和消息
return {"ok": True}
api.register_http_router(router, prefix="/sample", tags=["sample"])要点:
channel_class必须是BaseChannel的子类,且需要有channel类属性(唯一 key)。- 必须实现
from_config—ChannelManager启动时通过它创建频道实例。config参数是SimpleNamespace,不是 dict。 config_fields定义控制台设置面板中显示的表单字段,支持类型:text、password、number、switch、select。- 每个字段的
label、help、placeholder既可以是纯字符串,也可以是 本地化字典。字典的键同时支持长编码(如zh-CN、en-US)和短编码 (如zh、en),两者可混用。取值按优先级回退(当前语言精确码 → 短码 → 短码前缀匹配 → 英文 → 中文 → 字典首个非空值),确保缺失某语言 时不会显示为空白。 icon(可选)为频道卡片自定义图标 URL,仅支持http/https链接;其他值 会被忽略并回退到默认图标。doc_url(可选)为频道文档链接,可以是纯字符串,也可以是本地化字典 (如{"zh": "...", "en": "..."},键的长短码规则同label)。仅支持http/https链接;控制台设置面板标题栏会显示一个 “Doc” 按钮,点击按当前 语言跳转,值非法或缺失时不显示按钮。- 插件频道与内置频道共享启用/禁用、访问控制、
bot_prefix等功能。 - 如果插件频道 key 与内置频道冲突,内置频道优先,插件频道会被跳过并打印警告。
- 对于基于 webhook 的频道,可在同一个插件中组合
register_channel和register_http_router。
如果插件需要额外的 Python 包,创建 requirements.txt:
httpx>=0.24.0
pydantic>=2.0.0
插件安装时会自动安装依赖。
--index-url https://custom-pypi.example.com/simple
my-package>=1.0.0
- 插件 ID:使用小写字母和连字符,如
my-plugin - 版本号:遵循语义化版本(1.0.0, 1.1.0, 2.0.0)
钩子回调应该优雅处理错误,避免阻塞应用启动:
def startup_hook():
try:
# 你的初始化代码
pass
except Exception as e:
logger.error(f"Initialization failed: {e}", exc_info=True)
# 不要 raise,让应用继续启动使用 Python logging 记录插件行为:
import logging
logger = logging.getLogger(__name__)
logger.info("Plugin loaded")
logger.debug("Debug information")
logger.error("Error occurred", exc_info=True)提供清晰的 README.md 文档,包括:
- 功能说明
- 安装步骤
- 使用示例
- 配置说明
- 故障排查
钩子按优先级顺序执行:
- 优先级值越低,执行越早
- Priority 0 = 最高优先级(最先执行)
- Priority 100 = 默认优先级
- Priority 200 = 低优先级(最后执行)
示例:
# 最先执行
api.register_startup_hook("early", callback, priority=0)
# 默认顺序
api.register_startup_hook("normal", callback, priority=100)
# 最后执行
api.register_startup_hook("late", callback, priority=200)-
检查插件是否已安装:
qwenpaw plugin list
-
查看 QwenPaw 日志:
tail -f ~/.qwenpaw/logs/qwenpaw.log | grep -i plugin
-
验证插件清单格式:
qwenpaw plugin info <plugin-id>
- 检查
requirements.txt格式 - 手动安装依赖测试:
pip install -r /path/to/plugin/requirements.txt
- 使用
--force重新安装插件
- 确认插件已安装并重启 QwenPaw
- 检查 Web UI 的模型管理页面
- 查看日志中的 provider 注册信息
- 确认插件已安装
- 检查日志中命令处理器是否注册成功
- 确认命令名称是否匹配(如
/status)
- 只安装可信插件:插件代码会在 QwenPaw 进程中执行
- 检查依赖:确保插件依赖来自可信源
- 审查代码:安装前审查插件源代码
- 热加载注意:当前版本支持运行中通过 API 热安装/热卸载插件,无需重启。请注意热加载时的状态一致性
注册自定义 LLM Provider。
api.register_provider(
provider_id: str, # Provider 唯一标识符(必填)
provider_class: Type, # Provider 类(必填)
label: str = "", # 显示名称(可选,默认为 provider_id)
base_url: str = "", # API base URL(可选)
**metadata, # 额外关键字参数(chat_model, require_api_key 等)
)注册启动钩子。
api.register_startup_hook(
hook_name: str, # 钩子名称
callback: Callable, # 回调函数
priority: int = 100, # 优先级(越低越早执行)
)注册关闭钩子。
api.register_shutdown_hook(
hook_name: str, # 钩子名称
callback: Callable, # 回调函数
priority: int = 100, # 优先级(越低越早执行)
)将 fastapi.APIRouter 挂载到 /api + prefix 下。
api.register_http_router(
router: APIRouter, # fastapi.APIRouter 实例
*,
prefix: str, # /api 下的路径,例如 "/pets"
tags: Optional[List[str]] = None, # OpenAPI 标签(可选)
)完整步骤见上文「示例 7:暴露 FastAPI 接口」。
注册自定义 /slash 控制命令。
api.register_control_command(
handler: BaseControlCommandHandler, # 命令处理器实例
priority_level: int = 10, # 命令优先级(默认: 10)
)handler 必须继承 qwenpaw.runtime.commands.control.base.BaseControlCommandHandler,并实现 command_name、help_text 和 async handle(self, ctx, args) 方法。
将工具函数注册到 Agent 的工具集中。
api.register_tool(
tool_name: str, # 工具函数的唯一名称
tool_func: Callable, # 要注册的工具函数
description: str = "", # UI 中显示的描述
icon: str = "🔧", # 显示图标(emoji 字符串)
enabled: bool = False, # 是否默认启用
)注册卸载钩子,仅在插件被显式卸载时执行。
api.register_uninstall_hook(
hook_name: str, # 钩子名称
callback: Callable, # 回调函数
priority: int = 100, # 优先级(越低越早执行)
)注册 workspace 创建时触发的钩子。
api.register_workspace_created_hook(
hook_name: str, # 钩子名称
callback: Callable, # 回调函数: (workspace_info: dict) -> None
priority: int = 100, # 优先级(越低越早执行)
)获取或保存每个 Agent 的工具配置。
config = api.get_tool_config(tool_name: str, agent_id: str) # 返回 dict
api.set_tool_config(tool_name: str, agent_id: str, config: dict)注册 AgentScope MiddlewareBase 工厂。
api.register_middleware(
middleware_factory: Callable, # 工厂函数
*,
priority: int = 100, # 优先级(越低越靠外层)
)工厂函数签名:(ctx: HookContext, agent_config: AgentProfileConfig) -> MiddlewareBase | None
ctx包含session_id、agent_id、workspace_dir等请求级上下文- 返回
None表示本次请求跳过该 middleware priority越小越先进入洋葱模型(即越靠外层)
工厂在每次请求的 AgentBuilder.build() 阶段被调用,返回的 middleware 实例将被插入到 agent 的中间件链中。
完整步骤见上文「示例 8」和「示例 9」。
如需拦截或增强 agent 的请求处理,推荐以下方式:
- 增强 agent 推理循环:使用
register_middleware注册 AgentScope middleware(on_acting/on_reasoning钩子) - 拦截特定命令:使用
register_control_command注册自定义命令处理器 - 在请求生命周期中注入逻辑:使用
HookRegistry(8 阶段 hook)
当前请求流程为 Runtime.run() → AgentBuilder.build() → AgentExecutor.run()。
通过 api.runtime 访问运行时信息:
def my_hook():
# 访问 provider manager
provider_manager = api.runtime.provider_manager
# 获取所有 providers
providers = provider_manager.list_provider_info()将插件打包为 ZIP 文件以便分发:
cd /path/to/plugins
zip -r my-plugin-1.0.0.zip my-plugin/用户可以通过 URL 安装:
qwenpaw plugin install https://example.com/my-plugin-1.0.0.zipA: 插件通过 PluginApi 访问核心功能,包括:
- Provider 注册
- Middleware 注册(
register_middleware) - Hook 注册
- 自定义命令注册(
register_control_command) - HTTP 路由注册(
register_http_router) - Runtime helpers(provider_manager 等)
A: 可以,通过 register_middleware(注入 AgentScope middleware)、register_control_command、register_tool、runtime hooks 和其他 PluginApi 方法。请谨慎使用,确保不会破坏核心功能。
A: 如果多个插件注册相同的 provider_id 或 command_name,后注册的会覆盖先注册的。建议使用唯一的 ID。
一个为 QwenPaw agents 添加 OpenAI GPT Image 2 图片生成能力的工具插件。
系统要求:
- QwenPaw 最低版本:
1.1.5
安装方法:
# 克隆 QwenPaw 仓库(如果尚未克隆)
git clone https://github.com/agentscope-ai/QwenPaw.git
cd QwenPaw
# 安装插件
qwenpaw plugin install plugins/tool/gpt-image2配置步骤:
- 安装完成后,重启 QwenPaw
- 进入 Agent 设置 → 工具管理
- 找到 "generate_image_gpt" 工具
- 点击"配置"按钮,输入你的 OpenAI API Key
- 启用该工具
使用方法:
配置完成后,agent 可以通过调用工具来生成图片:
用户: 请生成一张可爱的小猫在花园里玩耍的图片
Agent: [调用 generate_image_gpt 工具]
[返回生成的图片]
功能特性:
- 支持多种图片尺寸:1024x1024, 1024x1792, 1792x1024
- 质量选项:low, medium, high, auto
- 自动验证 API Key
- Per-agent 配置(每个 agent 可以使用不同的 API Key)
更多详情请参考 plugins/tool/gpt-image2/README.md。