Files
Pine e2244a3cae feat: PineAgentsWeb 官网前端应用(Vite+React SPA)
- src/ 页面与组件、public/ 静态资源
- 工程配置:vite/tsconfig/package 依赖
2026-08-23 22:31:31 +08:00

2046 lines
60 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件系统
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)
## 插件管理
### 安装插件
从本地目录安装:
```bash
qwenpaw plugin install /path/to/plugin
```
从 URL 安装(支持 ZIP 文件):
```bash
qwenpaw plugin install https://example.com/plugin.zip
```
强制重新安装:
```bash
qwenpaw plugin install /path/to/plugin --force
```
**注意**:插件操作只能在 QwenPaw 离线时执行。
### 列出已安装插件
```bash
qwenpaw plugin list
```
输出示例:
```
Installed Plugins:
==================
my-provider (v1.0.0)
Custom LLM provider integration
Author: Developer Name
Path: /Users/user/.qwenpaw/plugins/my-provider
```
### 查看插件详情
```bash
qwenpaw plugin info <plugin-id>
```
### 卸载插件
```bash
qwenpaw plugin uninstall <plugin-id>
```
## 插件开发
### 后端插件
#### 基本结构
每个插件至少需要两个文件:
```
my-plugin/
├── plugin.json # 插件清单(必需)
├── plugin.py # 入口点(后端必需)
└── README.md # 文档(推荐)
```
#### plugin.json
```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` | 否 | 取值之一:`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`)至少需要提供其中之一。
#### `type` 取值
| 取值 | 适用场景 |
| ---------- | ---------------------------------------------------- |
| `tool` | 注册一个或多个 Agent 工具(LLM 可调用的函数)。 |
| `provider` | 注册自定义 LLM 提供商 / 模型端点。 |
| `hook` | 在应用启动 / 关闭时执行代码(app 生命周期级别)。 |
| `command` | 注册 `/slash` 控制命令。 |
| `channel` | 注册自定义消息频道。 |
| `frontend` | 提供前端 JS bundle,由 UI 动态加载。 |
| `general` | 兜底类型,用于组合型插件或不属于以上任何类别的插件。 |
#### plugin.py
```python
# -*- 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 SDKReact、antd 等共享依赖)和注册 APImenu、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
```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 注册扩展:
```tsx
const { React, antd } = window.QwenPaw.host;
const pluginId = "my-plugin";
// 调用 window.QwenPaw.* API 注册菜单、路由、聊天定制等
// 详见下方「前端扩展 API」
```
#### 构建工具链
**package.json**
```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**
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react",
"strict": false,
"skipLibCheck": true
}
}
```
**vite.config.ts**
```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,使用应用已加载的版本。
#### 构建和安装
```bash
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`
宿主共享依赖,插件无需打包这些库:
```ts
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 组件内使用):**
```ts
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
```
**命令式获取(可在任意位置调用):**
```ts
const agentId = window.QwenPaw.host.getSelectedAgentId();
const sessionId = window.QwenPaw.host.getCurrentSessionId();
```
**认证代理请求(自动注入 Authorization 和 X-Agent-Id 请求头):**
```ts
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 参数:**
```ts
{
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 参数:**
```ts
{
id: string; // 全局唯一,如 "my-plugin.home"
path: string; // URL 路径,支持 react-router 模式
component: React.ComponentType; // 页面组件
}
```
**wrap 示例(为已有页面加顶部 banner):**
```tsx
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 | 全局覆盖层 |
**示例:**
```tsx
// 替换 Header Logo
window.QwenPaw.slot.replace("my-plugin", "header.logo", (defaultLogo) => {
return <img src="https://example.com/logo.svg" style={{ height: 24 }} />;
});
```
### 聊天欢迎界面 — `chat.welcome`
```tsx
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`
```ts
window.QwenPaw.chat.theme.set("my-plugin", {
colorPrimary: "#1890ff",
});
```
### 聊天头部 — `chat.leftHeader` / `chat.rightHeader`
```tsx
// 设置左上角标题
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`
```ts
// 自定义 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`
```tsx
// 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`、解析后的 `sessionId``selectedAgent`
```ts
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`
```tsx
// 设置默认 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`
```tsx
// 注册自定义工具结果渲染组件(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`
```ts
window.QwenPaw.chat.card("my-plugin", "my-card", MyCardComponent);
```
### 审计与调试
```ts
// 查看扩展注册记录
console.table(window.QwenPaw.audit.overrides());
// 清理插件的所有 Chat 扩展注册
window.QwenPaw.chat.disposeAll("my-plugin");
```
### 国际化支持
所有支持 `Localized<T>` 类型的字段可传入函数,按语言返回不同值:
```ts
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. 创建插件目录
```bash
mkdir my-llm-provider
cd my-llm-provider
```
#### 2. 创建 plugin.json
```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
```python
# -*- 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
```python
# -*- 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. 安装和使用
```bash
# 安装插件
qwenpaw plugin install my-llm-provider
# 启动 QwenPaw
qwenpaw app
# 在 Web UI 中配置 API Key
# 然后就可以使用新的 Provider 了
```
### 示例 2:添加启动钩子
假设你想在 QwenPaw 启动时初始化一个监控服务。
#### 1. 创建插件
```bash
mkdir monitoring-hook
cd monitoring-hook
```
#### 2. 创建 plugin.json
```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
```python
# -*- 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. 安装
```bash
qwenpaw plugin install monitoring-hook
qwenpaw app
```
### 示例 3:添加自定义命令
假设你想添加一个 `/status` 命令来查看系统状态。
#### 1. 创建插件
```bash
mkdir status-command
cd status-command
```
#### 2. 创建 plugin.json
```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
```python
# -*- 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. 安装和使用
```bash
qwenpaw plugin install status-command
qwenpaw app
# 使用命令
/status
```
### 示例 4:添加自定义前端页面
向侧边栏添加一个欢迎页面。构建工具链文件(`package.json``tsconfig.json``vite.config.ts`)参考上方「前端插件 > 构建工具链」。
**plugin.json**
```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**
```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,
});
```
```bash
npm install && npm run build
cp -r . ~/.qwenpaw/plugins/welcome-plugin/
qwenpaw app
```
### 示例 5:自定义工具调用渲染
自定义 Agent 工具调用结果的展示方式。项目结构同示例 4,仅 `src/index.tsx` 不同。
**src/index.tsx**
```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**
```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. 创建插件目录
```bash
mkdir pet-api-plugin && cd pet-api-plugin
```
#### 2. 创建 plugin.json
```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
```python
# -*- 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. 安装并试用
```bash
qwenpaw plugin install pet-api-plugin
```
启动 QwenPaw 后,可在终端用 `curl` 测试(端口请按你本地实际为准,例如 `8088`):
```bash
# 列出全部宠物
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>` 标签。
- 插件卸载或禁用时会自动卸载对应路由。
### 示例 8Tracing Middleware(工具调用追踪)
本示例展示如何注册一个 `on_acting` middleware,当设置环境变量 `QWENPAW_TRACE` 时记录每次 tool call 的名称、参数和执行耗时。
**plugin.json**
```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**
```python
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`
---
### 示例 9Thinking Log Middleware(推理过程日志)
本示例展示如何注册一个 `on_reasoning` middleware,捕获并打印模型的思维链。
**plugin.json**
```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**
```python
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. 创建插件目录
```bash
mkdir sample-channel-plugin && cd sample-channel-plugin
```
#### 2. 创建 plugin.json
```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)`** — 向用户/会话发送消息。
```python
# -*- 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)` 安全读取字段。
#### 4. 创建 plugin.py — 插件入口
```python
# -*- 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. 安装和使用
```bash
qwenpaw plugin install sample-channel-plugin
qwenpaw app
```
启动后,在控制台的 **Control → Channels** 中可以看到 Sample 频道卡片,点击即可
填写凭证并启用。
#### 6. 添加 Webhook 端点(可选)
如果你的频道需要接收 HTTP 回调(如你的平台事件 API),可以在同一个插件中
注册 FastAPI 路由:
```python
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`
## 依赖管理
### 使用 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. 错误处理
钩子回调应该优雅处理错误,避免阻塞应用启动:
```python
def startup_hook():
try:
# 你的初始化代码
pass
except Exception as e:
logger.error(f"Initialization failed: {e}", exc_info=True)
# 不要 raise,让应用继续启动
```
### 3. 日志记录
使用 Python logging 记录插件行为:
```python
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 = 低优先级(最后执行)
**示例**
```python
# 最先执行
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. 检查插件是否已安装:
```bash
qwenpaw plugin list
```
2. 查看 QwenPaw 日志:
```bash
tail -f ~/.qwenpaw/logs/qwenpaw.log | grep -i plugin
```
3. 验证插件清单格式:
```bash
qwenpaw plugin info <plugin-id>
```
### 依赖安装失败
1. 检查 `requirements.txt` 格式
2. 手动安装依赖测试:
```bash
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。
```python
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
注册启动钩子。
```python
api.register_startup_hook(
hook_name: str, # 钩子名称
callback: Callable, # 回调函数
priority: int = 100, # 优先级(越低越早执行)
)
```
### register_shutdown_hook
注册关闭钩子。
```python
api.register_shutdown_hook(
hook_name: str, # 钩子名称
callback: Callable, # 回调函数
priority: int = 100, # 优先级(越低越早执行)
)
```
### register_http_router
将 `fastapi.APIRouter` 挂载到 `/api` + _prefix_ 下。
```python
api.register_http_router(
router: APIRouter, # fastapi.APIRouter 实例
*,
prefix: str, # /api 下的路径,例如 "/pets"
tags: Optional[List[str]] = None, # OpenAPI 标签(可选)
)
```
完整步骤见上文「示例 7:暴露 FastAPI 接口」。
### register_control_command
注册自定义 `/slash` 控制命令。
```python
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)` 方法。
### register_tool
将工具函数注册到 Agent 的工具集中。
```python
api.register_tool(
tool_name: str, # 工具函数的唯一名称
tool_func: Callable, # 要注册的工具函数
description: str = "", # UI 中显示的描述
icon: str = "🔧", # 显示图标(emoji 字符串)
enabled: bool = False, # 是否默认启用
)
```
### register_uninstall_hook
注册卸载钩子,仅在插件被显式卸载时执行。
```python
api.register_uninstall_hook(
hook_name: str, # 钩子名称
callback: Callable, # 回调函数
priority: int = 100, # 优先级(越低越早执行)
)
```
### register_workspace_created_hook
注册 workspace 创建时触发的钩子。
```python
api.register_workspace_created_hook(
hook_name: str, # 钩子名称
callback: Callable, # 回调函数: (workspace_info: dict) -> None
priority: int = 100, # 优先级(越低越早执行)
)
```
### get_tool_config / set_tool_config
获取或保存每个 Agent 的工具配置。
```python
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` 工厂。
```python
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 的请求处理,推荐以下方式:
- **增强 agent 推理循环**:使用 `register_middleware` 注册 AgentScope middleware`on_acting` / `on_reasoning` 钩子)
- **拦截特定命令**:使用 `register_control_command` 注册自定义命令处理器
- **在请求生命周期中注入逻辑**:使用 `HookRegistry`8 阶段 hook
当前请求流程为 `Runtime.run()` → `AgentBuilder.build()` → `AgentExecutor.run()`。
### 访问运行时信息
通过 `api.runtime` 访问运行时信息:
```python
def my_hook():
# 访问 provider manager
provider_manager = api.runtime.provider_manager
# 获取所有 providers
providers = provider_manager.list_provider_info()
```
## 插件打包
将插件打包为 ZIP 文件以便分发:
```bash
cd /path/to/plugins
zip -r my-plugin-1.0.0.zip my-plugin/
```
用户可以通过 URL 安装:
```bash
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 helpersprovider_manager 等)
### Q: 插件可以修改 QwenPaw 的核心行为吗?
A: 可以,通过 `register_middleware`(注入 AgentScope middleware)、`register_control_command`、`register_tool`、runtime hooks 和其他 PluginApi 方法。请谨慎使用,确保不会破坏核心功能。
### Q: 插件之间会冲突吗?
A: 如果多个插件注册相同的 provider_id 或 command_name,后注册的会覆盖先注册的。建议使用唯一的 ID。
## 示例插件
### GPT Image 2 工具插件
一个为 QwenPaw agents 添加 OpenAI GPT Image 2 图片生成能力的工具插件。
**系统要求:**
- QwenPaw 最低版本:`1.1.5`
**安装方法:**
```bash
# 克隆 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`。