Skip to content

Latest commit

 

History

History
2045 lines (1559 loc) · 60.4 KB

File metadata and controls

2045 lines (1559 loc) · 60.4 KB

插件系统

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        # 文档(推荐)

plugin.json

{
  "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 取值之一:toolproviderhookcommandfrontendgeneral。省略时会按 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_typemeta.provider_id)。
entry_point string 遗留字段。 等价于 entry.backend,仅为兼容老插件保留,新插件请使用 entry.backend

* entry.backendentry.frontend(或遗留 entry_point)至少需要提供其中之一。

type 取值

取值 适用场景
tool 注册一个或多个 Agent 工具(LLM 可调用的函数)。
provider 注册自定义 LLM 提供商 / 模型端点。
hook 在应用启动 / 关闭时执行代码(app 生命周期级别)。
command 注册 /slash 控制命令。
channel 注册自定义消息频道。
frontend 提供前端 JS bundle,由 UI 动态加载。
general 兜底类型,用于组合型插件或不属于以上任何类别的插件。

plugin.py

# -*- 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 界面。

加载生命周期:

  1. Console 启动,在 window.QwenPaw 上挂载 Host SDK(React、antd 等共享依赖)和注册 API(menu、route、slot、chat 等命名空间)
  2. Console 请求 /frontend_plugin 获取已启用的前端插件列表
  3. 逐一下载各插件的 JS bundle,通过 Blob URL 动态导入执行
  4. 插件代码执行,调用 window.QwenPaw.* 注册菜单、路由、聊天定制等 UI 扩展
  5. 注册立即生效——菜单出现在侧边栏、路由可导航、聊天区域呈现定制内容

插件无需声明使用了哪些扩展点;系统通过 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   # 构建配置

plugin.json

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "type": "frontend",
  "author": "Your Name",
  "entry": { "frontend": "dist/index.js" }
}

src/index.tsx

插件入口文件在加载时执行,通过 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,使用宿主提供的 Reactexternal 避免打包 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,获得完整的类型提示。

前端扩展 API

前端插件通过 window.QwenPaw.* API 扩展 Console 界面,无需修改宿主代码。所有注册方法第一个参数是 pluginId,每个注册返回 { dispose() } 对象用于撤销。

Host SDK — window.QwenPaw.host

宿主共享依赖,插件无需打包这些库:

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 // 获取当前认证 Token

React 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();

侧边栏菜单 — window.QwenPaw.menu

方法 签名 说明
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;             // 渲染为水平分割线
}

页面路由 — window.QwenPaw.route

方法 签名 说明
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>
  );
});

通用 UI 插槽 — window.QwenPaw.slot

方法 签名 说明
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 }} />;
});

聊天欢迎界面 — chat.welcome

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>;
});

聊天主题 — chat.theme

window.QwenPaw.chat.theme.set("my-plugin", {
  colorPrimary: "#1890ff",
});

聊天头部 — chat.leftHeader / chat.rightHeader

// 设置左上角标题
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 },
);

输入框 — chat.sender

// 自定义 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" },
  ],
});

消息操作按钮 — chat.actions / chat.requestActions

// 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

使用 chat.requestPayload.add 可以在 Console 将聊天请求发送到后端前改写 requestBody。多个转换函数会按 order 从小到大执行,入参包含当前 payload、解析后的 sessionIdselectedAgent

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,方便审计和卸载时清理。

消息气泡自定义 — chat.request / chat.response

// 设置默认 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>
  );
});

工具调用渲染 — chat.toolRender

// 注册自定义工具结果渲染组件(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>
  );
});

自定义卡片 — chat.card

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

使用示例

示例 1:添加自定义 Provider

假设你想接入一个企业内部的 LLM 服务。

1. 创建插件目录

mkdir my-llm-provider
cd my-llm-provider

2. 创建 plugin.json

{
  "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"
  }
}

3. 创建 provider.py

# -*- 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,
            ),
        ]

4. 创建 plugin.py

# -*- 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()

5. 安装和使用

# 安装插件
qwenpaw plugin install my-llm-provider

# 启动 QwenPaw
qwenpaw app

# 在 Web UI 中配置 API Key
# 然后就可以使用新的 Provider 了

示例 2:添加启动钩子

假设你想在 QwenPaw 启动时初始化一个监控服务。

1. 创建插件

mkdir monitoring-hook
cd monitoring-hook

2. 创建 plugin.json

{
  "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"
  }
}

3. 创建 plugin.py

# -*- 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()

4. 安装

qwenpaw plugin install monitoring-hook
qwenpaw app

示例 3:添加自定义命令

假设你想添加一个 /status 命令来查看系统状态。

1. 创建插件

mkdir status-command
cd status-command

2. 创建 plugin.json

{
  "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"
  }
}

3. 创建 plugin.py

# -*- 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()

4. 安装和使用

qwenpaw plugin install status-command
qwenpaw app

# 使用命令
/status

示例 4:添加自定义前端页面

向侧边栏添加一个欢迎页面。构建工具链文件(package.jsontsconfig.jsonvite.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

示例 5:自定义工具调用渲染

自定义 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>
  );
});

示例 6:自定义聊天欢迎界面

定制对话页面的欢迎语、描述和推荐提示词。项目结构同示例 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: "优化这段逻辑" },
  ],
});

示例 7:暴露 FastAPI 接口

后端插件可以通过注册 fastapi.APIRouter 暴露自己的 HTTP 接口。路由会挂载在 /api 加上你指定的前缀下,与 QwenPaw 核心 API 使用同一个 FastAPI 应用,因此 共享 CORS、鉴权等设置,并会出现在 /openapi.json/docs 中。

下面示例增加一个简单的 /api/pets 接口:列出宠物,并支持新增。

1. 创建插件目录

mkdir pet-api-plugin && cd pet-api-plugin

2. 创建 plugin.json

{
  "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"
  }
}

3. 创建 plugin.py

# -*- 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()

4. 安装并试用

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> 标签。
  • 插件卸载或禁用时会自动卸载对应路由。

示例 8:Tracing Middleware(工具调用追踪)

本示例展示如何注册一个 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 包裹其他 middleware
  • on_acting 钩子:在 tool call 执行前/后测量耗时
  • 完整源码参见 plugins/middleware-demo/tracing-middleware/tracing_plugin.py

示例 9:Thinking Log Middleware(推理过程日志)

本示例展示如何注册一个 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

示例 10:注册自定义消息频道

Channel 插件可以为 QwenPaw 添加新的消息平台。注册后的频道会在控制台 UI 中与内置 频道(钉钉、Telegram 等)一起显示,支持同样的启用/禁用和配置方式。

1. 创建插件目录

mkdir sample-channel-plugin && cd sample-channel-plugin

2. 创建 plugin.json

{
  "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"
  }
}

3. 创建 channel.py — BaseChannel 子类

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() 收到的 configtypes.SimpleNamespace 对象(不是 dict 或 Pydantic model)。框架会将 BaseChannelConfig 的默认值与用户保存的配置合并后传入。请始终使用 getattr(config, "field", default) 安全读取字段。

4. 创建 plugin.py — 插件入口

# -*- 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()

5. 安装和使用

qwenpaw plugin install sample-channel-plugin
qwenpaw app

启动后,在控制台的 Control → Channels 中可以看到 Sample 频道卡片,点击即可 填写凭证并启用。

6. 添加 Webhook 端点(可选)

如果你的频道需要接收 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_configChannelManager 启动时通过它创建频道实例。 config 参数是 SimpleNamespace,不是 dict。
  • config_fields 定义控制台设置面板中显示的表单字段,支持类型:textpasswordnumberswitchselect
  • 每个字段的 labelhelpplaceholder 既可以是纯字符串,也可以是 本地化字典。字典的键同时支持长编码(如 zh-CNen-US)和短编码 (如 zhen),两者可混用。取值按优先级回退(当前语言精确码 → 短码 → 短码前缀匹配 → 英文 → 中文 → 字典首个非空值),确保缺失某语言 时不会显示为空白。
  • icon(可选)为频道卡片自定义图标 URL,仅支持 http/https 链接;其他值 会被忽略并回退到默认图标。
  • doc_url(可选)为频道文档链接,可以是纯字符串,也可以是本地化字典 (如 {"zh": "...", "en": "..."},键的长短码规则同 label)。仅支持 http/https 链接;控制台设置面板标题栏会显示一个 “Doc” 按钮,点击按当前 语言跳转,值非法或缺失时不显示按钮。
  • 插件频道与内置频道共享启用/禁用、访问控制、bot_prefix 等功能。
  • 如果插件频道 key 与内置频道冲突,内置频道优先,插件频道会被跳过并打印警告。
  • 对于基于 webhook 的频道,可在同一个插件中组合 register_channelregister_http_router

依赖管理

使用 requirements.txt

如果插件需要额外的 Python 包,创建 requirements.txt

httpx>=0.24.0
pydantic>=2.0.0

插件安装时会自动安装依赖。

使用自定义 PyPI 源

--index-url https://custom-pypi.example.com/simple
my-package>=1.0.0

最佳实践

1. 命名规范

  • 插件 ID:使用小写字母和连字符,如 my-plugin
  • 版本号:遵循语义化版本(1.0.0, 1.1.0, 2.0.0)

2. 错误处理

钩子回调应该优雅处理错误,避免阻塞应用启动:

def startup_hook():
    try:
        # 你的初始化代码
        pass
    except Exception as e:
        logger.error(f"Initialization failed: {e}", exc_info=True)
        # 不要 raise,让应用继续启动

3. 日志记录

使用 Python logging 记录插件行为:

import logging

logger = logging.getLogger(__name__)

logger.info("Plugin loaded")
logger.debug("Debug information")
logger.error("Error occurred", exc_info=True)

4. 文档

提供清晰的 README.md 文档,包括:

  • 功能说明
  • 安装步骤
  • 使用示例
  • 配置说明
  • 故障排查

优先级系统

Hook 优先级

钩子按优先级顺序执行:

  • 优先级值越低,执行越早
  • 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)

故障排查

插件未加载

  1. 检查插件是否已安装:

    qwenpaw plugin list
  2. 查看 QwenPaw 日志:

    tail -f ~/.qwenpaw/logs/qwenpaw.log | grep -i plugin
  3. 验证插件清单格式:

    qwenpaw plugin info <plugin-id>

依赖安装失败

  1. 检查 requirements.txt 格式
  2. 手动安装依赖测试:
    pip install -r /path/to/plugin/requirements.txt
  3. 使用 --force 重新安装插件

Provider 未显示

  1. 确认插件已安装并重启 QwenPaw
  2. 检查 Web UI 的模型管理页面
  3. 查看日志中的 provider 注册信息

命令未响应

  1. 确认插件已安装
  2. 检查日志中命令处理器是否注册成功
  3. 确认命令名称是否匹配(如 /status

安全注意事项

  1. 只安装可信插件:插件代码会在 QwenPaw 进程中执行
  2. 检查依赖:确保插件依赖来自可信源
  3. 审查代码:安装前审查插件源代码
  4. 热加载注意:当前版本支持运行中通过 API 热安装/热卸载插件,无需重启。请注意热加载时的状态一致性

PluginApi 参考

register_provider

注册自定义 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 等)
)

register_startup_hook

注册启动钩子。

api.register_startup_hook(
    hook_name: str,      # 钩子名称
    callback: Callable,  # 回调函数
    priority: int = 100, # 优先级(越低越早执行)
)

register_shutdown_hook

注册关闭钩子。

api.register_shutdown_hook(
    hook_name: str,      # 钩子名称
    callback: Callable,  # 回调函数
    priority: int = 100, # 优先级(越低越早执行)
)

register_http_router

fastapi.APIRouter 挂载到 /api + prefix 下。

api.register_http_router(
    router: APIRouter,             # fastapi.APIRouter 实例
    *,
    prefix: str,                   # /api 下的路径,例如 "/pets"
    tags: Optional[List[str]] = None,  # OpenAPI 标签(可选)
)

完整步骤见上文「示例 7:暴露 FastAPI 接口」。

register_control_command

注册自定义 /slash 控制命令。

api.register_control_command(
    handler: BaseControlCommandHandler,  # 命令处理器实例
    priority_level: int = 10,            # 命令优先级(默认: 10)
)

handler 必须继承 qwenpaw.runtime.commands.control.base.BaseControlCommandHandler,并实现 command_namehelp_textasync handle(self, ctx, args) 方法。

register_tool

将工具函数注册到 Agent 的工具集中。

api.register_tool(
    tool_name: str,          # 工具函数的唯一名称
    tool_func: Callable,     # 要注册的工具函数
    description: str = "",   # UI 中显示的描述
    icon: str = "🔧",        # 显示图标(emoji 字符串)
    enabled: bool = False,   # 是否默认启用
)

register_uninstall_hook

注册卸载钩子,仅在插件被显式卸载时执行。

api.register_uninstall_hook(
    hook_name: str,      # 钩子名称
    callback: Callable,  # 回调函数
    priority: int = 100, # 优先级(越低越早执行)
)

register_workspace_created_hook

注册 workspace 创建时触发的钩子。

api.register_workspace_created_hook(
    hook_name: str,      # 钩子名称
    callback: Callable,  # 回调函数: (workspace_info: dict) -> None
    priority: int = 100, # 优先级(越低越早执行)
)

get_tool_config / set_tool_config

获取或保存每个 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)

register_middleware

注册 AgentScope MiddlewareBase 工厂。

api.register_middleware(
    middleware_factory: Callable,   # 工厂函数
    *,
    priority: int = 100,           # 优先级(越低越靠外层)
)

工厂函数签名:(ctx: HookContext, agent_config: AgentProfileConfig) -> MiddlewareBase | None

  • ctx 包含 session_idagent_idworkspace_dir 等请求级上下文
  • 返回 None 表示本次请求跳过该 middleware
  • priority 越小越先进入洋葱模型(即越靠外层)

工厂在每次请求的 AgentBuilder.build() 阶段被调用,返回的 middleware 实例将被插入到 agent 的中间件链中。

完整步骤见上文「示例 8」和「示例 9」。

高级功能

修改 Agent 行为

如需拦截或增强 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.zip

常见问题

Q: 插件可以访问哪些 QwenPaw API?

A: 插件通过 PluginApi 访问核心功能,包括:

  • Provider 注册
  • Middleware 注册(register_middleware
  • Hook 注册
  • 自定义命令注册(register_control_command
  • HTTP 路由注册(register_http_router
  • Runtime helpers(provider_manager 等)

Q: 插件可以修改 QwenPaw 的核心行为吗?

A: 可以,通过 register_middleware(注入 AgentScope middleware)、register_control_commandregister_tool、runtime hooks 和其他 PluginApi 方法。请谨慎使用,确保不会破坏核心功能。

Q: 插件之间会冲突吗?

A: 如果多个插件注册相同的 provider_id 或 command_name,后注册的会覆盖先注册的。建议使用唯一的 ID。

示例插件

GPT Image 2 工具插件

一个为 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

配置步骤:

  1. 安装完成后,重启 QwenPaw
  2. 进入 Agent 设置 → 工具管理
  3. 找到 "generate_image_gpt" 工具
  4. 点击"配置"按钮,输入你的 OpenAI API Key
  5. 启用该工具

使用方法:

配置完成后,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