Files
NapCatQQ/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts
手瓜一十雪 48ffd5597a Add plugin icon support and caching
Introduce support for plugin icons across backend and frontend. Updates include:

- napcat-onebot: add optional `icon` field to PluginPackageJson.
- Backend (api/Plugin, PluginStore, router): add handlers/utilities to locate and serve plugin icons (`GetPluginIconHandler`, getPluginIconUrl, findPluginIconPath) and wire the route `/api/Plugin/Icon/:pluginId`.
- Cache logic: implement `cachePluginIcon` to fetch GitHub user avatars and store as `data/icon.png` when package.json lacks an icon; invoked after plugin install (regular and SSE flows).
- Frontend: add `icon` to PluginItem, prefer backend-provided icon URL in plugin card (via new getPluginIconUrl util that appends webui_token query param), and add the util to handle token-based image requests.
- Plugin store UI: add a Random category (shuffled), client-side pagination, and reset page on tab/search changes.

These changes let the UI display plugin icons (falling back to author/avatar or Vercel avatars) and cache icons for better UX, while handling auth by passing the token as a query parameter for img src requests.
2026-02-20 23:32:57 +08:00

238 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
import { serverRequest } from '@/utils/request';
import { PluginStoreList, PluginStoreItem } from '@/types/plugin-store';
/** 插件状态 */
export type PluginStatus = 'active' | 'disabled' | 'stopped';
/** 插件信息 */
export interface PluginItem {
/** 显示名称 (优先 package.json 的 plugin 字段) */
name: string;
/** 包名 (package name),用于 API 操作 */
id: string;
/** 版本号 */
version: string;
/** 描述 */
description: string;
/** 作者 */
author: string;
/** 状态: active-运行中, disabled-已禁用, stopped-已停止 */
status: PluginStatus;
/** 是否有配置项 */
hasConfig?: boolean;
/** 是否有扩展页面 */
hasPages?: boolean;
/** 主页链接 */
homepage?: string;
/** 仓库链接 */
repository?: string;
/** 插件图标 URL由后端返回 */
icon?: string;
}
/** 扩展页面信息 */
export interface ExtensionPageItem {
/** 插件 ID */
pluginId: string;
/** 插件名称 */
pluginName: string;
/** 页面路径 */
path: string;
/** 页面标题 */
title: string;
/** 页面图标 */
icon?: string;
/** 页面描述 */
description?: string;
}
/** 插件列表响应 */
export interface PluginListResponse {
plugins: PluginItem[];
pluginManagerNotFound: boolean;
extensionPages: ExtensionPageItem[];
}
/** 插件配置项定义 */
export interface PluginConfigSchemaItem {
key: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'multi-select' | 'html' | 'text';
label: string;
description?: string;
default?: any;
options?: { label: string; value: string | number; }[];
placeholder?: string;
/** 标记此字段为响应式:值变化时触发 schema 刷新 */
reactive?: boolean;
/** 是否隐藏此字段 */
hidden?: boolean;
}
/** 插件配置响应 */
export interface PluginConfigResponse {
schema: PluginConfigSchemaItem[];
config: Record<string, unknown>;
/** 是否支持响应式更新 */
supportReactive?: boolean;
}
/** 服务端响应 */
export interface ServerResponse<T> {
code: number;
message: string;
data: T;
}
/**
* 插件管理器 API
*/
export default class PluginManager {
/**
* 获取插件列表
*/
public static async getPluginList (): Promise<PluginListResponse> {
const { data } = await serverRequest.get<ServerResponse<PluginListResponse>>('/Plugin/List');
return data.data;
}
/**
* 手动注册插件管理器到 NetworkManager
*/
public static async registerPluginManager (): Promise<{ message: string; }> {
const { data } = await serverRequest.post<ServerResponse<{ message: string; }>>('/Plugin/RegisterManager');
return data.data;
}
/**
* 设置插件状态(启用/禁用)
* @param id 插件包名
* @param enable 是否启用
*/
public static async setPluginStatus (id: string, enable: boolean): Promise<void> {
await serverRequest.post<ServerResponse<void>>('/Plugin/SetStatus', { id, enable });
}
/**
* 卸载插件
* @param id 插件包名
* @param cleanData 是否清理数据
*/
public static async uninstallPlugin (id: string, cleanData?: boolean): Promise<void> {
await serverRequest.post<ServerResponse<void>>('/Plugin/Uninstall', { id, cleanData });
}
/**
* 导入本地插件包
* @param file 插件 zip 文件
*/
public static async importLocalPlugin (file: File): Promise<{ message: string; pluginId: string; installPath: string; }> {
const formData = new FormData();
formData.append('plugin', file);
const { data } = await serverRequest.post<ServerResponse<{ message: string; pluginId: string; installPath: string; }>>(
'/Plugin/Import',
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
timeout: 60000, // 60秒超时
}
);
return data.data;
}
// ==================== 插件商店 ====================
/**
* 获取插件商店列表
* @param forceRefresh 是否强制刷新(跳过服务端缓存)
*/
public static async getPluginStoreList (forceRefresh: boolean = false): Promise<PluginStoreList> {
const params = forceRefresh ? { forceRefresh: 'true' } : {};
const { data } = await serverRequest.get<ServerResponse<PluginStoreList>>('/Plugin/Store/List', { params });
return data.data;
}
/**
* 获取插件商店详情
* @param id 插件 ID
*/
public static async getPluginStoreDetail (id: string): Promise<PluginStoreItem> {
const { data } = await serverRequest.get<ServerResponse<PluginStoreItem>>(`/Plugin/Store/Detail/${id}`);
return data.data;
}
/**
* 从商店安装插件
* @param id 插件 ID
* @param mirror 镜像源
*/
public static async installPluginFromStore (id: string, mirror?: string): Promise<void> {
await serverRequest.post<ServerResponse<void>>(
'/Plugin/Store/Install',
{ id, mirror },
{ timeout: 300000 } // 5分钟超时
);
}
// ==================== 插件配置 ====================
/**
* 获取插件配置
* @param id 插件包名
*/
public static async getPluginConfig (id: string): Promise<PluginConfigResponse> {
const { data } = await serverRequest.get<ServerResponse<PluginConfigResponse>>('/Plugin/Config', {
params: { id }
});
return data.data;
}
/**
* 设置插件配置
* @param id 插件包名
* @param config 配置内容
*/
public static async setPluginConfig (id: string, config: Record<string, unknown>): Promise<void> {
await serverRequest.post<ServerResponse<void>>('/Plugin/Config', { id, config });
}
/**
* 通知配置字段变化
* @param id 插件包名
* @param sessionId SSE 会话 ID
* @param key 变化的字段
* @param value 新值
* @param currentConfig 当前配置
*/
public static async notifyConfigChange (
id: string,
sessionId: string,
key: string,
value: unknown,
currentConfig: Record<string, unknown>
): Promise<void> {
await serverRequest.post<ServerResponse<void>>('/Plugin/Config/Change', {
id,
sessionId,
key,
value,
currentConfig
});
}
/**
* 获取配置 SSE URL
* @param id 插件包名
* @param config 初始配置
*/
public static getConfigSSEUrl (id: string, config?: Record<string, unknown>): string {
const params = new URLSearchParams({ id });
if (config) {
params.set('config', JSON.stringify(config));
}
return `/api/Plugin/Config/SSE?${params.toString()}`;
}
}