@timmy_hu/plugin-middleware 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -15,8 +15,8 @@ class PluginMiddleware {
15
15
  zh_Hans: "插件中间件管理器"
16
16
  },
17
17
  description: {
18
- en_US: "Discovers all middleware-type plugins and provides dynamic on-demand loading, unloading, and status query tools for agent orchestration. Authenticates via /api/auth/login.",
19
- zh_Hans: "自动发现所有中间件类型插件,支持动态按需加载、卸载和状态查询。通过 /api/auth/login 登录获取 token 后调用平台 API。"
18
+ en_US: "Discovers all middleware-type plugins installed on the XpertAI platform, shows their load status, and provides guidance on binding plugins to agents. Note: Plugins must be manually bound to agents in the XpertAI platform UI.",
19
+ zh_Hans: "自动发现 XpertAI 平台上所有已安装的中间件类型插件,展示加载状态,并提供插件绑定指导。注意:插件需要在 XpertAI 平台 UI 中手动绑定到智能体。"
20
20
  },
21
21
  configSchema: {
22
22
  type: "object",
@@ -42,12 +42,6 @@ class PluginMiddleware {
42
42
  title: { en_US: "Organization ID", zh_Hans: "组织 ID" },
43
43
  description: "Organization ID for scoped plugin queries"
44
44
  },
45
- autoDiscover: {
46
- type: "boolean",
47
- title: { en_US: "Auto Discover", zh_Hans: "自动发现" },
48
- description: "Whether to automatically discover available middleware plugins on startup",
49
- default: true
50
- },
51
45
  cacheTtlSeconds: {
52
46
  type: "number",
53
47
  title: { en_US: "Cache TTL (seconds)", zh_Hans: "缓存有效期(秒)" },
@@ -61,158 +55,143 @@ class PluginMiddleware {
61
55
  }
62
56
 
63
57
  createMiddleware(options = {}, context = {}) {
64
- const platformUrl = options.platformUrl || "http://10.161.48.53:3300";
58
+ const platformUrl = (options.platformUrl || "http://10.161.48.53:3300").replace(/\/+$/, "");
65
59
  const email = options.email || "";
66
60
  const password = options.password || "";
67
61
  const organizationId = options.organizationId || "";
68
62
  const cacheTtlSeconds = options.cacheTtlSeconds || 300;
69
63
 
70
- // In-memory registry
71
- const registry = {
72
- plugins: new Map(),
73
- loadedPlugins: new Map(),
64
+ const state = {
65
+ allPlugins: [],
66
+ middlewarePlugins: [],
74
67
  lastRefresh: null,
75
68
  cacheTtl: cacheTtlSeconds * 1000,
76
69
  token: null,
77
70
  tokenExpiry: null
78
71
  };
79
72
 
80
- // ---- Login: call /api/auth/login to get token ----
81
73
  async function loginAndGetToken() {
82
74
  const now = Date.now();
83
- // Reuse token if still valid (within 50 minutes, assuming 1h expiry)
84
- if (registry.token && registry.tokenExpiry && now < registry.tokenExpiry) {
85
- return registry.token;
75
+ if (state.token && state.tokenExpiry && now < state.tokenExpiry) {
76
+ return state.token;
86
77
  }
87
-
88
78
  if (!email || !password) {
89
79
  throw new Error("缺少登录凭据:请在插件配置中填写 email 和 password");
90
80
  }
91
-
92
81
  const fetch = (await import("node-fetch")).default;
93
- const loginUrl = `${platformUrl}/api/auth/login`;
94
- const resp = await fetch(loginUrl, {
82
+ const resp = await fetch(`${platformUrl}/api/auth/login`, {
95
83
  method: "POST",
96
84
  headers: { "Content-Type": "application/json" },
97
85
  body: JSON.stringify({ email, password }),
98
86
  timeout: 15000
99
87
  });
100
-
101
88
  if (!resp.ok) {
102
89
  const errText = await resp.text();
103
90
  throw new Error(`登录失败 (${resp.status}): ${errText}`);
104
91
  }
105
-
106
92
  const data = await resp.json();
107
93
  const token = data.access_token || data.token || (data.data && data.data.access_token);
108
94
  if (!token) {
109
- throw new Error(`登录响应中未找到 token 字段,响应: ${JSON.stringify(data)}`);
95
+ throw new Error(`登录响应中未找到 token 字段`);
110
96
  }
111
-
112
- registry.token = token;
113
- // Assume token expires in 50 minutes (conservative)
114
- registry.tokenExpiry = now + 50 * 60 * 1000;
97
+ state.token = token;
98
+ state.tokenExpiry = now + 50 * 60 * 1000;
115
99
  return token;
116
100
  }
117
101
 
118
- // ---- Helper: build auth headers ----
119
102
  function authHeaders(token) {
120
- const headers = {
121
- "Content-Type": "application/json",
122
- "Authorization": `Bearer ${token}`
123
- };
124
- if (organizationId) {
125
- headers["organization-id"] = organizationId;
126
- }
127
- return headers;
103
+ const h = { "Content-Type": "application/json", "Authorization": `Bearer ${token}` };
104
+ if (organizationId) h["organization-id"] = organizationId;
105
+ return h;
128
106
  }
129
107
 
130
- // ---- Fetch middleware plugins from platform ----
131
- async function fetchMiddlewarePlugins() {
108
+ async function fetchAllPlugins() {
132
109
  const now = Date.now();
133
- if (registry.lastRefresh && (now - registry.lastRefresh) < registry.cacheTtl) {
134
- return Array.from(registry.plugins.values());
110
+ if (state.lastRefresh && (now - state.lastRefresh) < state.cacheTtl && state.allPlugins.length > 0) {
111
+ return state.allPlugins;
135
112
  }
136
-
137
113
  const token = await loginAndGetToken();
138
114
  const fetch = (await import("node-fetch")).default;
139
- const url = `${platformUrl}/api/plugin?category=middleware`;
140
- const resp = await fetch(url, {
115
+ const resp = await fetch(`${platformUrl}/api/plugin`, {
141
116
  method: "GET",
142
117
  headers: authHeaders(token),
143
- timeout: 10000
118
+ timeout: 15000
144
119
  });
145
-
146
120
  if (!resp.ok) {
147
- // If 401, try re-login once
148
121
  if (resp.status === 401) {
149
- registry.token = null;
150
- registry.tokenExpiry = null;
122
+ state.token = null; state.tokenExpiry = null;
151
123
  const newToken = await loginAndGetToken();
152
- const retryResp = await fetch(url, {
153
- method: "GET",
154
- headers: authHeaders(newToken),
155
- timeout: 10000
156
- });
157
- if (!retryResp.ok) {
158
- throw new Error(`Platform API returned ${retryResp.status}: ${retryResp.statusText}`);
159
- }
160
- const retryData = await retryResp.json();
161
- return parsePluginList(retryData);
124
+ const retry = await fetch(`${platformUrl}/api/plugin`, { method: "GET", headers: authHeaders(newToken), timeout: 15000 });
125
+ if (!retry.ok) throw new Error(`Platform API returned ${retry.status}`);
126
+ return processPluginList(await retry.json());
162
127
  }
163
128
  throw new Error(`Platform API returned ${resp.status}: ${resp.statusText}`);
164
129
  }
165
-
166
130
  const data = await resp.json();
167
- return parsePluginList(data);
131
+ return processPluginList(data);
168
132
  }
169
133
 
170
- function parsePluginList(data) {
171
- const pluginList = Array.isArray(data) ? data : (data.data || data.items || data.plugins || []);
172
- registry.plugins.clear();
173
- for (const p of pluginList) {
174
- const code = p.code || p.name || p.pluginName;
175
- if (code) {
176
- registry.plugins.set(code, {
177
- code,
178
- name: p.name || p.displayName || code,
179
- version: p.version || "unknown",
180
- description: p.description || "",
181
- status: p.status || "available",
182
- category: p.category || "middleware",
183
- loaded: registry.loadedPlugins.has(code)
184
- });
185
- }
186
- }
187
- registry.lastRefresh = Date.now();
188
- return Array.from(registry.plugins.values());
134
+ function processPluginList(data) {
135
+ const list = Array.isArray(data) ? data : (data.data || data.items || data.plugins || []);
136
+ state.allPlugins = list;
137
+ state.middlewarePlugins = list.filter(p => {
138
+ const meta = p.meta || {};
139
+ return meta.category === "middleware";
140
+ });
141
+ state.lastRefresh = Date.now();
142
+ return state.allPlugins;
143
+ }
144
+
145
+ function formatMiddlewarePlugin(p) {
146
+ const meta = p.meta || {};
147
+ return {
148
+ name: p.name || meta.name,
149
+ packageName: p.packageName || "",
150
+ displayName: meta.displayName || p.name || meta.name,
151
+ version: meta.version || p.currentVersion || "unknown",
152
+ description: meta.description || "",
153
+ category: meta.category || "unknown",
154
+ author: meta.author || "",
155
+ keywords: meta.keywords || [],
156
+ source: p.source || "",
157
+ loadStatus: p.loadStatus || "unknown",
158
+ loadError: p.loadError || null,
159
+ configurationStatus: p.configurationStatus || null,
160
+ configurationError: p.configurationError || null,
161
+ canConfigure: p.canConfigure || false,
162
+ canUninstall: p.canUninstall || false,
163
+ canUpdate: p.canUpdate || false,
164
+ hasUpdate: p.hasUpdate || false,
165
+ effectiveInCurrentScope: p.effectiveInCurrentScope !== false,
166
+ scopeRelation: p.scopeRelation || "unknown",
167
+ componentSummary: p.componentSummary || { total: 0, skills: 0, mcpServers: 0, apps: 0, hooks: 0 }
168
+ };
189
169
  }
190
170
 
191
- // ======== Tool 1: list_middleware_plugins ========
171
+ // Tool 1: list_middleware_plugins
192
172
  const listMiddlewarePlugins = tool(
193
173
  async (input) => {
194
174
  try {
195
- const plugins = await fetchMiddlewarePlugins();
175
+ await fetchAllPlugins();
196
176
  const filter = input.filter || "all";
197
- let filtered = plugins;
177
+ let filtered = state.middlewarePlugins;
178
+
198
179
  if (filter === "loaded") {
199
- filtered = plugins.filter(p => p.loaded || registry.loadedPlugins.has(p.code));
200
- } else if (filter === "available") {
201
- filtered = plugins.filter(p => !p.loaded && !registry.loadedPlugins.has(p.code));
180
+ filtered = filtered.filter(p => p.loadStatus === "loaded");
181
+ } else if (filter === "error") {
182
+ filtered = filtered.filter(p => p.loadStatus === "error" || p.loadError);
183
+ } else if (filter === "updatable") {
184
+ filtered = filtered.filter(p => p.hasUpdate === true);
202
185
  }
186
+
203
187
  return {
204
188
  success: true,
205
189
  total: filtered.length,
206
- plugins: filtered.map(p => ({
207
- code: p.code,
208
- name: p.name,
209
- version: p.version,
210
- description: p.description,
211
- status: registry.loadedPlugins.has(p.code) ? "loaded" : "available",
212
- loadedAt: registry.loadedPlugins.has(p.code)
213
- ? registry.loadedPlugins.get(p.code).loadedAt
214
- : null
215
- }))
190
+ totalAllPlugins: state.allPlugins.length,
191
+ totalMiddleware: state.middlewarePlugins.length,
192
+ filter,
193
+ plugins: filtered.map(formatMiddlewarePlugin),
194
+ note: "⚠️ 插件已安装并加载,但需要在 XpertAI 平台 UI 中手动绑定到智能体才能使用其工具。请使用 get_binding_guidance 工具获取绑定指导。"
216
195
  };
217
196
  } catch (err) {
218
197
  return { success: false, error: err.message, plugins: [] };
@@ -220,263 +199,517 @@ class PluginMiddleware {
220
199
  },
221
200
  {
222
201
  name: "list_middleware_plugins",
223
- description: "列出所有已发现的中间件类型插件,可按加载状态过滤。返回插件编码、名称、版本、描述和当前状态。",
202
+ description: "列出平台上所有已安装的中间件类型插件。注意:插件需要在 XpertAI 平台 UI 中手动绑定到智能体。",
224
203
  schema: z.object({
225
- filter: z.enum(["all", "loaded", "available"]).optional().default("all")
226
- .describe("过滤条件:all=全部, loaded=已加载, available=可用未加载")
204
+ filter: z.enum(["all", "loaded", "error", "updatable"]).optional().default("all")
205
+ .describe("过滤条件:all=全部, loaded=已加载, error=加载出错, updatable=有可用更新")
227
206
  })
228
207
  }
229
208
  );
230
209
 
231
- // ======== Tool 2: load_middleware_plugin ========
232
- const loadMiddlewarePlugin = tool(
210
+ // Tool 2: get_middleware_plugin_detail
211
+ const getMiddlewarePluginDetail = tool(
233
212
  async (input) => {
234
- const pluginCode = input.pluginCode;
235
- if (registry.loadedPlugins.has(pluginCode)) {
236
- return {
237
- success: true,
238
- message: `插件 ${pluginCode} 已经处于加载状态`,
239
- plugin: { code: pluginCode, loadedAt: registry.loadedPlugins.get(pluginCode).loadedAt, status: "already_loaded" }
240
- };
241
- }
213
+ try {
214
+ await fetchAllPlugins();
215
+ const keyword = input.pluginNameOrPackage;
216
+ const found = state.middlewarePlugins.find(p => {
217
+ const meta = p.meta || {};
218
+ return p.name === keyword
219
+ || meta.name === keyword
220
+ || p.packageName === keyword
221
+ || meta.displayName === keyword;
222
+ });
242
223
 
243
- const plugins = await fetchMiddlewarePlugins();
244
- const target = plugins.find(p => p.code === pluginCode);
245
- if (!target) {
246
- return {
247
- success: false,
248
- error: `未找到编码为 ${pluginCode} 的中间件插件`,
249
- availablePlugins: plugins.map(p => p.code)
250
- };
224
+ if (!found) {
225
+ return {
226
+ success: false,
227
+ error: `未找到名为 "${keyword}" 的中间件插件`,
228
+ availableMiddleware: state.middlewarePlugins.map(p => ({
229
+ name: p.name, packageName: p.packageName, displayName: (p.meta || {}).displayName
230
+ }))
231
+ };
232
+ }
233
+
234
+ const detail = formatMiddlewarePlugin(found);
235
+
236
+ try {
237
+ const token = await loginAndGetToken();
238
+ const fetch = (await import("node-fetch")).default;
239
+ const compResp = await fetch(`${platformUrl}/api/plugin/${encodeURIComponent(found.name)}/components`, {
240
+ method: "GET", headers: authHeaders(token), timeout: 10000
241
+ });
242
+ if (compResp.ok) {
243
+ const compData = await compResp.json();
244
+ detail.components = compData.items || compData;
245
+ }
246
+ } catch (e) {
247
+ detail.components = [];
248
+ }
249
+
250
+ // 添加绑定状态提示
251
+ if (detail.componentSummary.total === 0) {
252
+ detail.bindingWarning = "⚠️ 该插件没有暴露任何组件(工具/技能/MCP服务器)。请检查插件实现是否正确注册了中间件策略。";
253
+ } else if (!detail.effectiveInCurrentScope) {
254
+ detail.bindingWarning = "⚠️ 该插件在当前作用域中未生效。请在 XpertAI 平台 UI 中将此插件绑定到当前智能体。";
255
+ }
256
+
257
+ return { success: true, plugin: detail };
258
+ } catch (err) {
259
+ return { success: false, error: err.message };
251
260
  }
261
+ },
262
+ {
263
+ name: "get_middleware_plugin_detail",
264
+ description: "查询指定中间件插件的详细信息,包括加载状态、配置状态、组件列表、版本信息和绑定状态。",
265
+ schema: z.object({
266
+ pluginNameOrPackage: z.string().min(1)
267
+ .describe("插件名称、packageName 或 displayName,例如 'echarts-generator' 或 '@timmy_hu/bulk-email-sender'")
268
+ })
269
+ }
270
+ );
271
+
272
+ // Tool 3: install_middleware_plugin
273
+ const installMiddlewarePlugin = tool(
274
+ async (input) => {
275
+ const packageName = input.packageName;
276
+ const version = input.version || "latest";
252
277
 
253
278
  try {
254
279
  const token = await loginAndGetToken();
255
280
  const fetch = (await import("node-fetch")).default;
256
- const resp = await fetch(`${platformUrl}/api/plugin/load`, {
281
+ const resp = await fetch(`${platformUrl}/api/plugin`, {
257
282
  method: "POST",
258
283
  headers: authHeaders(token),
259
- body: JSON.stringify({ pluginName: target.name || pluginCode, version: target.version, source: "npm" }),
260
- timeout: 30000
284
+ body: JSON.stringify({ pluginName: packageName, version, source: "npm" }),
285
+ timeout: 60000
261
286
  });
287
+
262
288
  if (!resp.ok) {
263
289
  const errText = await resp.text();
264
- throw new Error(`Load API returned ${resp.status}: ${errText}`);
290
+ throw new Error(`安装失败 (${resp.status}): ${errText}`);
265
291
  }
292
+
266
293
  const result = await resp.json();
267
- registry.loadedPlugins.set(pluginCode, {
268
- code: pluginCode, name: target.name, version: target.version,
269
- loadedAt: new Date().toISOString(), loadResult: result
270
- });
294
+ state.lastRefresh = null;
295
+
271
296
  return {
272
- success: true, message: `插件 ${pluginCode} 加载成功`,
273
- plugin: { code: pluginCode, name: target.name, version: target.version, loadedAt: registry.loadedPlugins.get(pluginCode).loadedAt, status: "loaded" }
297
+ success: true,
298
+ message: `插件 ${packageName} 安装成功`,
299
+ installed: {
300
+ name: result.name || packageName,
301
+ packageName: result.packageName || packageName,
302
+ version: result.currentVersion || version,
303
+ organizationId: result.organizationId || organizationId
304
+ },
305
+ nextSteps: [
306
+ "1. 插件已安装并加载到平台",
307
+ "2. 请在 XpertAI 平台 UI 中进入智能体编排页面",
308
+ "3. 点击\"添加中间件\"按钮",
309
+ "4. 在列表中找到刚安装的插件并添加",
310
+ "5. 保存智能体配置后,插件的工具即可使用"
311
+ ]
274
312
  };
275
313
  } catch (err) {
276
- registry.loadedPlugins.set(pluginCode, {
277
- code: pluginCode, name: target.name, version: target.version,
278
- loadedAt: new Date().toISOString(), loadResult: { offline: true, error: err.message }
279
- });
280
314
  return {
281
- success: true, message: `插件 ${pluginCode} 已标记为加载(离线模式)`,
282
- plugin: { code: pluginCode, name: target.name, version: target.version, loadedAt: registry.loadedPlugins.get(pluginCode).loadedAt, status: "loaded_offline", warning: err.message }
315
+ success: false,
316
+ error: err.message,
317
+ packageName,
318
+ version
283
319
  };
284
320
  }
285
321
  },
286
322
  {
287
- name: "load_middleware_plugin",
288
- description: "按插件编码动态加载指定的中间件插件。加载后该插件的工具和能力将对智能体可用。",
323
+ name: "install_middleware_plugin",
324
+ description: "从 npm 安装一个新的中间件插件到 XpertAI 平台。安装后需要在平台 UI 中手动绑定到智能体。",
289
325
  schema: z.object({
290
- pluginCode: z.string().min(1).describe("要加载的中间件插件编码,例如 'weather-query' 'pg-query-tool'")
326
+ packageName: z.string().min(1).describe("npm 包名,例如 '@timmy_hu/bulk-email-sender'"),
327
+ version: z.string().optional().default("latest").describe("版本号,默认为 latest")
291
328
  })
292
329
  }
293
330
  );
294
331
 
295
- // ======== Tool 3: unload_middleware_plugin ========
296
- const unloadMiddlewarePlugin = tool(
332
+ // Tool 4: refresh_plugin_list
333
+ const refreshPluginList = tool(
297
334
  async (input) => {
298
- const pluginCode = input.pluginCode;
299
- if (!registry.loadedPlugins.has(pluginCode)) {
335
+ state.lastRefresh = null;
336
+ state.token = null;
337
+ state.tokenExpiry = null;
338
+ try {
339
+ await fetchAllPlugins();
300
340
  return {
301
- success: false,
302
- error: `插件 ${pluginCode} 当前未加载,无需卸载`,
303
- loadedPlugins: Array.from(registry.loadedPlugins.keys())
341
+ success: true,
342
+ message: `插件列表已刷新,共发现 ${state.allPlugins.length} 个插件,其中 ${state.middlewarePlugins.length} 个中间件类型`,
343
+ totalPlugins: state.allPlugins.length,
344
+ totalMiddleware: state.middlewarePlugins.length,
345
+ refreshedAt: new Date().toISOString()
304
346
  };
347
+ } catch (err) {
348
+ return { success: false, error: `刷新失败: ${err.message}` };
305
349
  }
350
+ },
351
+ {
352
+ name: "refresh_plugin_list",
353
+ description: "刷新插件列表缓存,强制重新登录并从平台获取所有已安装插件的最新状态。",
354
+ schema: z.object({})
355
+ }
356
+ );
357
+
358
+ // Tool 5: check_login_status
359
+ const checkLoginStatus = tool(
360
+ async (input) => {
306
361
  try {
307
362
  const token = await loginAndGetToken();
308
- const fetch = (await import("node-fetch")).default;
309
- const resp = await fetch(`${platformUrl}/api/plugin/unload`, {
310
- method: "POST",
311
- headers: authHeaders(token),
312
- body: JSON.stringify({ pluginCode }),
313
- timeout: 15000
314
- });
315
- if (!resp.ok) {
316
- const errText = await resp.text();
317
- throw new Error(`Unload API returned ${resp.status}: ${errText}`);
318
- }
363
+ return {
364
+ success: true,
365
+ loggedIn: true,
366
+ email,
367
+ platformUrl,
368
+ organizationId: organizationId || "(未设置)",
369
+ tokenExpiry: state.tokenExpiry ? new Date(state.tokenExpiry).toISOString() : null,
370
+ cachedPlugins: state.allPlugins.length,
371
+ cachedMiddleware: state.middlewarePlugins.length,
372
+ message: "登录状态正常,token 有效"
373
+ };
319
374
  } catch (err) {
320
- // Continue with local unload even if API fails
375
+ return {
376
+ success: false,
377
+ loggedIn: false,
378
+ email,
379
+ platformUrl,
380
+ error: err.message
381
+ };
321
382
  }
322
- const removed = registry.loadedPlugins.get(pluginCode);
323
- registry.loadedPlugins.delete(pluginCode);
324
- return {
325
- success: true, message: `插件 ${pluginCode} 已卸载`,
326
- plugin: { code: pluginCode, name: removed.name, unloadedAt: new Date().toISOString(), status: "unloaded" }
327
- };
328
383
  },
329
384
  {
330
- name: "unload_middleware_plugin",
331
- description: "按插件编码卸载已加载的中间件插件。卸载后该插件的工具将不再对智能体可用。",
332
- schema: z.object({
333
- pluginCode: z.string().min(1).describe("要卸载的中间件插件编码")
334
- })
385
+ name: "check_login_status",
386
+ description: "检查当前平台登录状态,验证 email/password 是否有效、token 是否过期,以及缓存中有多少插件。",
387
+ schema: z.object({})
335
388
  }
336
389
  );
337
390
 
338
- // ======== Tool 4: get_middleware_plugin_status ========
339
- const getMiddlewarePluginStatus = tool(
391
+ // Tool 6: search_middleware_plugins
392
+ const searchMiddlewarePlugins = tool(
340
393
  async (input) => {
341
- const pluginCode = input.pluginCode;
342
- const plugins = await fetchMiddlewarePlugins();
343
- const target = plugins.find(p => p.code === pluginCode);
344
- if (!target) {
345
- return { success: false, error: `未找到编码为 ${pluginCode} 的中间件插件` };
394
+ try {
395
+ await fetchAllPlugins();
396
+ const keyword = (input.keyword || "").toLowerCase();
397
+ const results = state.middlewarePlugins.filter(p => {
398
+ const meta = p.meta || {};
399
+ const searchFields = [
400
+ p.name, p.packageName, meta.name, meta.displayName,
401
+ meta.description, (meta.keywords || []).join(" ")
402
+ ].map(s => (s || "").toLowerCase());
403
+ return searchFields.some(f => f.includes(keyword));
404
+ });
405
+
406
+ return {
407
+ success: true,
408
+ keyword: input.keyword,
409
+ total: results.length,
410
+ plugins: results.map(formatMiddlewarePlugin)
411
+ };
412
+ } catch (err) {
413
+ return { success: false, error: err.message, plugins: [] };
346
414
  }
347
- const loaded = registry.loadedPlugins.has(pluginCode);
348
- const loadInfo = loaded ? registry.loadedPlugins.get(pluginCode) : null;
349
- return {
350
- success: true,
351
- plugin: {
352
- code: target.code, name: target.name, version: target.version,
353
- description: target.description, category: target.category,
354
- status: loaded ? "loaded" : "available",
355
- loadedAt: loadInfo ? loadInfo.loadedAt : null,
356
- tools: loaded ? (loadInfo.tools || []) : [],
357
- configRequired: target.configRequired || []
358
- }
359
- };
360
415
  },
361
416
  {
362
- name: "get_middleware_plugin_status",
363
- description: "查询指定中间件插件的详细状态信息,包括加载状态、版本、描述和可用工具列表。",
417
+ name: "search_middleware_plugins",
418
+ description: "按关键词搜索中间件类型插件。搜索范围包括插件名称、包名、描述和关键词。",
364
419
  schema: z.object({
365
- pluginCode: z.string().min(1).describe("要查询的中间件插件编码")
420
+ keyword: z.string().min(1).describe("搜索关键词,例如 'echarts'、'email'、'retry'")
366
421
  })
367
422
  }
368
423
  );
369
424
 
370
- // ======== Tool 5: refresh_plugin_cache ========
371
- const refreshPluginCache = tool(
425
+ // Tool 7: get_plugin_statistics
426
+ const getPluginStatistics = tool(
372
427
  async (input) => {
373
- registry.lastRefresh = null;
374
- registry.token = null;
375
- registry.tokenExpiry = null;
376
428
  try {
377
- const plugins = await fetchMiddlewarePlugins();
429
+ await fetchAllPlugins();
430
+
431
+ const categoryStats = {};
432
+ for (const p of state.allPlugins) {
433
+ const cat = (p.meta || {}).category || "unknown";
434
+ categoryStats[cat] = (categoryStats[cat] || 0) + 1;
435
+ }
436
+
437
+ const loadStatusStats = {};
438
+ for (const p of state.middlewarePlugins) {
439
+ const ls = p.loadStatus || "unknown";
440
+ loadStatusStats[ls] = (loadStatusStats[ls] || 0) + 1;
441
+ }
442
+
443
+ const updatable = state.middlewarePlugins.filter(p => p.hasUpdate === true);
444
+ const configErrors = state.middlewarePlugins.filter(p => p.configurationError);
445
+ const loadErrors = state.middlewarePlugins.filter(p => p.loadError);
446
+
378
447
  return {
379
448
  success: true,
380
- message: `缓存已刷新,发现 ${plugins.length} 个中间件插件`,
381
- total: plugins.length,
382
- refreshedAt: new Date().toISOString()
449
+ statistics: {
450
+ totalPlugins: state.allPlugins.length,
451
+ totalMiddleware: state.middlewarePlugins.length,
452
+ byCategory: categoryStats,
453
+ middlewareByLoadStatus: loadStatusStats,
454
+ updatableCount: updatable.length,
455
+ updatablePlugins: updatable.map(p => ({ name: p.name, packageName: p.packageName, version: (p.meta || {}).version })),
456
+ configErrorCount: configErrors.length,
457
+ configErrorPlugins: configErrors.map(p => ({ name: p.name, error: p.configurationError })),
458
+ loadErrorCount: loadErrors.length,
459
+ loadErrorPlugins: loadErrors.map(p => ({ name: p.name, error: p.loadError }))
460
+ },
461
+ generatedAt: new Date().toISOString()
383
462
  };
384
463
  } catch (err) {
385
- return { success: false, error: `刷新缓存失败: ${err.message}` };
464
+ return { success: false, error: err.message };
386
465
  }
387
466
  },
388
467
  {
389
- name: "refresh_plugin_cache",
390
- description: "刷新中间件插件发现缓存,强制重新登录并获取所有中间件类型插件列表。",
468
+ name: "get_plugin_statistics",
469
+ description: "获取平台上所有插件的统计信息,包括按分类统计、按加载状态统计、有更新的插件、有错误的插件等。",
391
470
  schema: z.object({})
392
471
  }
393
472
  );
394
473
 
395
- // ======== Tool 6: batch_load_middleware_plugins ========
396
- const batchLoadMiddlewarePlugins = tool(
474
+ // Tool 8: get_binding_guidance (NEW)
475
+ const getBindingGuidance = tool(
397
476
  async (input) => {
398
- const pluginCodes = input.pluginCodes;
399
- const results = [];
400
- const plugins = await fetchMiddlewarePlugins();
401
- for (const code of pluginCodes) {
402
- if (registry.loadedPlugins.has(code)) {
403
- results.push({ code, status: "already_loaded", success: true });
404
- continue;
477
+ const pluginName = input.pluginName;
478
+
479
+ try {
480
+ await fetchAllPlugins();
481
+
482
+ const plugin = state.middlewarePlugins.find(p => {
483
+ const meta = p.meta || {};
484
+ return p.name === pluginName
485
+ || meta.name === pluginName
486
+ || p.packageName === pluginName
487
+ || meta.displayName === pluginName;
488
+ });
489
+
490
+ if (!plugin) {
491
+ return {
492
+ success: false,
493
+ error: `未找到名为 "${pluginName}" 的中间件插件`,
494
+ guidance: "请先使用 list_middleware_plugins 查看所有可用的中间件插件"
495
+ };
405
496
  }
406
- const target = plugins.find(p => p.code === code);
407
- if (!target) {
408
- results.push({ code, status: "not_found", success: false, error: `未找到编码为 ${code} 的中间件插件` });
409
- continue;
497
+
498
+ const meta = plugin.meta || {};
499
+ const detail = formatMiddlewarePlugin(plugin);
500
+
501
+ const guidance = {
502
+ pluginName: detail.displayName,
503
+ packageName: detail.packageName,
504
+ version: detail.version,
505
+ loadStatus: detail.loadStatus,
506
+ componentSummary: detail.componentSummary,
507
+ steps: [
508
+ {
509
+ step: 1,
510
+ action: "打开 XpertAI 平台",
511
+ url: platformUrl,
512
+ description: "登录到 XpertAI 平台"
513
+ },
514
+ {
515
+ step: 2,
516
+ action: "进入智能体编排页面",
517
+ description: "找到并编辑需要使用该插件的智能体"
518
+ },
519
+ {
520
+ step: 3,
521
+ action: "添加中间件",
522
+ description: `点击"添加中间件"按钮,在列表中找到 "${detail.displayName}" (${detail.packageName})`
523
+ },
524
+ {
525
+ step: 4,
526
+ action: "配置插件(如需要)",
527
+ description: detail.canConfigure ? "根据插件要求填写配置参数" : "该插件无需额外配置"
528
+ },
529
+ {
530
+ step: 5,
531
+ action: "保存智能体",
532
+ description: "保存智能体配置,插件的工具将立即可用"
533
+ }
534
+ ],
535
+ troubleshooting: []
536
+ };
537
+
538
+ // 诊断问题
539
+ if (detail.loadStatus !== "loaded") {
540
+ guidance.troubleshooting.push({
541
+ issue: "插件未加载",
542
+ status: detail.loadStatus,
543
+ error: detail.loadError,
544
+ solution: "插件加载失败,请检查插件是否正确安装,或联系插件作者"
545
+ });
410
546
  }
411
- try {
412
- const token = await loginAndGetToken();
413
- const fetch = (await import("node-fetch")).default;
414
- const resp = await fetch(`${platformUrl}/api/plugin/load`, {
415
- method: "POST",
416
- headers: authHeaders(token),
417
- body: JSON.stringify({ pluginName: target.name || code, version: target.version, source: "npm" }),
418
- timeout: 30000
547
+
548
+ if (detail.componentSummary.total === 0) {
549
+ guidance.troubleshooting.push({
550
+ issue: "插件没有暴露任何组件",
551
+ componentSummary: detail.componentSummary,
552
+ solution: "插件实现可能有问题,没有正确注册中间件策略或工具。请联系插件作者检查插件代码。"
419
553
  });
420
- if (!resp.ok) throw new Error(`Load API returned ${resp.status}`);
421
- const result = await resp.json();
422
- registry.loadedPlugins.set(code, {
423
- code, name: target.name, version: target.version,
424
- loadedAt: new Date().toISOString(), loadResult: result
554
+ }
555
+
556
+ if (!detail.effectiveInCurrentScope) {
557
+ guidance.troubleshooting.push({
558
+ issue: "插件在当前作用域未生效",
559
+ scopeRelation: detail.scopeRelation,
560
+ solution: "请在智能体编排页面中手动添加此中间件到智能体"
425
561
  });
426
- results.push({ code, name: target.name, version: target.version, status: "loaded", success: true, loadedAt: registry.loadedPlugins.get(code).loadedAt });
427
- } catch (err) {
428
- registry.loadedPlugins.set(code, {
429
- code, name: target.name, version: target.version,
430
- loadedAt: new Date().toISOString(), loadResult: { offline: true, error: err.message }
562
+ }
563
+
564
+ if (detail.configurationError) {
565
+ guidance.troubleshooting.push({
566
+ issue: "插件配置错误",
567
+ error: detail.configurationError,
568
+ solution: "请检查并修正插件配置"
431
569
  });
432
- results.push({ code, name: target.name, version: target.version, status: "loaded_offline", success: true, warning: err.message });
433
570
  }
571
+
572
+ return {
573
+ success: true,
574
+ guidance
575
+ };
576
+ } catch (err) {
577
+ return {
578
+ success: false,
579
+ error: err.message
580
+ };
434
581
  }
435
- return {
436
- success: true, total: pluginCodes.length,
437
- loaded: results.filter(r => r.status === "loaded").length,
438
- alreadyLoaded: results.filter(r => r.status === "already_loaded").length,
439
- failed: results.filter(r => !r.success).length,
440
- results
441
- };
442
582
  },
443
583
  {
444
- name: "batch_load_middleware_plugins",
445
- description: "批量加载多个中间件插件。传入插件编码数组,一次性加载所有指定的中间件插件。",
584
+ name: "get_binding_guidance",
585
+ description: "获取将指定中间件插件绑定到智能体的详细步骤指导,包括诊断插件状态和提供故障排除建议。",
446
586
  schema: z.object({
447
- pluginCodes: z.array(z.string().min(1)).min(1)
448
- .describe("要批量加载的中间件插件编码数组,例如 ['weather-query', 'pg-query-tool']")
587
+ pluginName: z.string().min(1)
588
+ .describe("插件名称、packageName displayName,例如 '@timmy_hu/bulk-email-sender'")
449
589
  })
450
590
  }
451
591
  );
452
592
 
453
- // ======== Tool 7: login_status ========
454
- const checkLoginStatus = tool(
593
+ // Tool 9: diagnose_plugin_availability (NEW)
594
+ const diagnosePluginAvailability = tool(
455
595
  async (input) => {
596
+ const pluginName = input.pluginName;
597
+
456
598
  try {
457
- const token = await loginAndGetToken();
599
+ await fetchAllPlugins();
600
+
601
+ const plugin = state.middlewarePlugins.find(p => {
602
+ const meta = p.meta || {};
603
+ return p.name === pluginName
604
+ || meta.name === pluginName
605
+ || p.packageName === pluginName
606
+ || meta.displayName === pluginName;
607
+ });
608
+
609
+ if (!plugin) {
610
+ return {
611
+ success: false,
612
+ error: `未找到名为 "${pluginName}" 的中间件插件`,
613
+ diagnosis: "插件未安装",
614
+ solution: "使用 install_middleware_plugin 工具安装该插件"
615
+ };
616
+ }
617
+
618
+ const meta = plugin.meta || {};
619
+ const detail = formatMiddlewarePlugin(plugin);
620
+
621
+ const checks = [];
622
+ let allPassed = true;
623
+
624
+ // Check 1: 插件是否加载
625
+ const loadCheck = {
626
+ name: "插件加载状态",
627
+ passed: detail.loadStatus === "loaded",
628
+ status: detail.loadStatus,
629
+ error: detail.loadError
630
+ };
631
+ checks.push(loadCheck);
632
+ if (!loadCheck.passed) allPassed = false;
633
+
634
+ // Check 2: 是否有组件
635
+ const componentCheck = {
636
+ name: "组件暴露",
637
+ passed: detail.componentSummary.total > 0,
638
+ total: detail.componentSummary.total,
639
+ breakdown: detail.componentSummary
640
+ };
641
+ checks.push(componentCheck);
642
+ if (!componentCheck.passed) allPassed = false;
643
+
644
+ // Check 3: 配置状态
645
+ const configCheck = {
646
+ name: "配置状态",
647
+ passed: !detail.configurationError,
648
+ status: detail.configurationStatus,
649
+ error: detail.configurationError
650
+ };
651
+ checks.push(configCheck);
652
+ if (!configCheck.passed) allPassed = false;
653
+
654
+ // Check 4: 作用域生效
655
+ const scopeCheck = {
656
+ name: "作用域生效",
657
+ passed: detail.effectiveInCurrentScope,
658
+ scopeRelation: detail.scopeRelation
659
+ };
660
+ checks.push(scopeCheck);
661
+ if (!scopeCheck.passed) allPassed = false;
662
+
663
+ const diagnosis = {
664
+ pluginName: detail.displayName,
665
+ packageName: detail.packageName,
666
+ version: detail.version,
667
+ allChecksPassed: allPassed,
668
+ checks,
669
+ summary: allPassed
670
+ ? "✅ 插件状态正常,应该可以在智能体中使用。如果仍无法使用,请确保已在智能体编排页面中添加了该中间件。"
671
+ : "❌ 插件存在问题,需要修复后才能使用",
672
+ recommendations: []
673
+ };
674
+
675
+ if (!loadCheck.passed) {
676
+ diagnosis.recommendations.push("插件加载失败,请检查插件代码或联系插件作者");
677
+ }
678
+
679
+ if (!componentCheck.passed) {
680
+ diagnosis.recommendations.push("插件没有暴露任何工具或组件。这通常意味着插件代码没有正确实现中间件策略。请联系插件作者检查代码。");
681
+ }
682
+
683
+ if (!configCheck.passed) {
684
+ diagnosis.recommendations.push(`插件配置错误: ${detail.configurationError}`);
685
+ }
686
+
687
+ if (!scopeCheck.passed) {
688
+ diagnosis.recommendations.push("插件在当前作用域未生效。请在 XpertAI 平台 UI 的智能体编排页面中手动添加此中间件。");
689
+ }
690
+
691
+ if (allPassed) {
692
+ diagnosis.recommendations.push("插件状态正常。如果工具仍不可用,请确保:1) 已在智能体编排页面添加了该中间件;2) 已保存智能体配置;3) 刷新了智能体页面。");
693
+ }
694
+
458
695
  return {
459
696
  success: true,
460
- loggedIn: true,
461
- email: email,
462
- platformUrl: platformUrl,
463
- tokenExpiry: registry.tokenExpiry ? new Date(registry.tokenExpiry).toISOString() : null,
464
- message: "登录状态正常,token 有效"
697
+ diagnosis
465
698
  };
466
699
  } catch (err) {
467
700
  return {
468
701
  success: false,
469
- loggedIn: false,
470
- email: email,
471
- platformUrl: platformUrl,
472
702
  error: err.message
473
703
  };
474
704
  }
475
705
  },
476
706
  {
477
- name: "check_login_status",
478
- description: "检查当前平台登录状态,验证 email/password 是否有效、token 是否过期。",
479
- schema: z.object({})
707
+ name: "diagnose_plugin_availability",
708
+ description: "诊断指定中间件插件为什么其工具不可用。检查加载状态、组件暴露、配置状态和作用域生效情况,并提供修复建议。",
709
+ schema: z.object({
710
+ pluginName: z.string().min(1)
711
+ .describe("插件名称、packageName 或 displayName,例如 '@timmy_hu/bulk-email-sender'")
712
+ })
480
713
  }
481
714
  );
482
715
 
@@ -484,12 +717,14 @@ class PluginMiddleware {
484
717
  name: MIDDLEWARE_PROVIDER,
485
718
  tools: [
486
719
  listMiddlewarePlugins,
487
- loadMiddlewarePlugin,
488
- unloadMiddlewarePlugin,
489
- getMiddlewarePluginStatus,
490
- refreshPluginCache,
491
- batchLoadMiddlewarePlugins,
492
- checkLoginStatus
720
+ getMiddlewarePluginDetail,
721
+ installMiddlewarePlugin,
722
+ refreshPluginList,
723
+ checkLoginStatus,
724
+ searchMiddlewarePlugins,
725
+ getPluginStatistics,
726
+ getBindingGuidance,
727
+ diagnosePluginAvailability
493
728
  ]
494
729
  };
495
730
  }
@@ -503,12 +738,12 @@ class PluginModule {}
503
738
  const plugin = {
504
739
  meta: {
505
740
  name: "@timmy_hu/plugin-middleware",
506
- version: "1.0.1",
741
+ version: "1.0.3",
507
742
  level: "organization",
508
743
  category: "middleware",
509
744
  displayName: "插件中间件",
510
- description: "自动发现所有中间件类型插件,支持动态按需加载、卸载和状态查询。通过 /api/auth/login 登录获取 token 后调用平台 API。",
511
- keywords: ["middleware", "plugin", "discovery", "dynamic-loading", "agent", "orchestration"],
745
+ description: "自动发现 XpertAI 平台上所有已安装的中间件类型插件,展示加载状态,提供插件绑定指导和诊断工具。注意:插件需要在 XpertAI 平台 UI 中手动绑定到智能体。",
746
+ keywords: ["middleware", "plugin", "discovery", "install", "binding", "diagnosis", "agent"],
512
747
  author: "fpi"
513
748
  },
514
749
  register(ctx) {