@timmy_hu/plugin-middleware 1.0.0

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.
@@ -0,0 +1,487 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { AgentMiddlewareStrategy } from "@xpert-ai/plugin-sdk";
3
+ import { tool } from "@langchain/core/tools";
4
+ import { z } from "zod";
5
+
6
+ const MIDDLEWARE_PROVIDER = "PluginMiddleware";
7
+
8
+ /**
9
+ * PluginMiddleware - 插件中间件管理器
10
+ *
11
+ * 自动发现所有中间件类型插件,支持动态按需加载、卸载和状态查询。
12
+ * 为智能体编排提供灵活的中间件管理能力。
13
+ */
14
+ @Injectable()
15
+ @AgentMiddlewareStrategy(MIDDLEWARE_PROVIDER)
16
+ export class PluginMiddleware {
17
+ public readonly meta = {
18
+ name: MIDDLEWARE_PROVIDER,
19
+ label: {
20
+ en_US: "Plugin Middleware Manager",
21
+ zh_Hans: "插件中间件管理器"
22
+ },
23
+ description: {
24
+ en_US: "Discovers all middleware-type plugins and provides dynamic on-demand loading, unloading, and status query tools for agent orchestration.",
25
+ zh_Hans: "自动发现所有中间件类型插件,支持动态按需加载、卸载和状态查询,为智能体编排提供灵活的中间件管理能力。"
26
+ },
27
+ configSchema: {
28
+ type: "object",
29
+ properties: {
30
+ platformUrl: {
31
+ type: "string",
32
+ title: {
33
+ en_US: "Platform URL",
34
+ zh_Hans: "平台地址"
35
+ },
36
+ description: "XpertAI platform base URL for plugin discovery API",
37
+ default: "http://10.161.48.53:3300"
38
+ },
39
+ organizationId: {
40
+ type: "string",
41
+ title: {
42
+ en_US: "Organization ID",
43
+ zh_Hans: "组织 ID"
44
+ },
45
+ description: "Organization ID for scoped plugin queries"
46
+ },
47
+ autoDiscover: {
48
+ type: "boolean",
49
+ title: {
50
+ en_US: "Auto Discover",
51
+ zh_Hans: "自动发现"
52
+ },
53
+ description: "Whether to automatically discover available middleware plugins on startup",
54
+ default: true
55
+ },
56
+ cacheTtlSeconds: {
57
+ type: "number",
58
+ title: {
59
+ en_US: "Cache TTL (seconds)",
60
+ zh_Hans: "缓存有效期(秒)"
61
+ },
62
+ description: "How long to cache the discovered plugin list before refreshing",
63
+ default: 300
64
+ }
65
+ },
66
+ required: ["platformUrl"]
67
+ }
68
+ };
69
+
70
+ createMiddleware(options: any = {}, context: any = {}) {
71
+ const platformUrl = options.platformUrl || "http://10.161.48.53:3300";
72
+ const organizationId = options.organizationId || "";
73
+ const autoDiscover = options.autoDiscover !== false;
74
+ const cacheTtlSeconds = options.cacheTtlSeconds || 300;
75
+
76
+ // In-memory registry for discovered middleware plugins
77
+ const registry = {
78
+ plugins: new Map(),
79
+ loadedPlugins: new Map(),
80
+ lastRefresh: null as number | null,
81
+ cacheTtl: cacheTtlSeconds * 1000
82
+ };
83
+
84
+ // Helper: fetch middleware plugins from platform API
85
+ async function fetchMiddlewarePlugins() {
86
+ const now = Date.now();
87
+ if (registry.lastRefresh && (now - registry.lastRefresh) < registry.cacheTtl) {
88
+ return Array.from(registry.plugins.values());
89
+ }
90
+
91
+ try {
92
+ const fetch = (await import("node-fetch")).default;
93
+ const url = `${platformUrl}/api/plugin?category=middleware&organization-id=${encodeURIComponent(organizationId)}`;
94
+ const resp = await fetch(url, {
95
+ method: "GET",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ ...(organizationId ? { "organization-id": organizationId } : {})
99
+ },
100
+ timeout: 10000
101
+ });
102
+
103
+ if (!resp.ok) {
104
+ throw new Error(`Platform API returned ${resp.status}: ${resp.statusText}`);
105
+ }
106
+
107
+ const data = await resp.json();
108
+ const pluginList = Array.isArray(data) ? data : (data.data || data.items || data.plugins || []);
109
+
110
+ registry.plugins.clear();
111
+ for (const p of pluginList) {
112
+ const code = p.code || p.name || p.pluginName;
113
+ if (code) {
114
+ registry.plugins.set(code, {
115
+ code,
116
+ name: p.name || p.displayName || code,
117
+ version: p.version || "unknown",
118
+ description: p.description || "",
119
+ status: p.status || "available",
120
+ category: p.category || "middleware",
121
+ loaded: registry.loadedPlugins.has(code)
122
+ });
123
+ }
124
+ }
125
+
126
+ registry.lastRefresh = now;
127
+ return Array.from(registry.plugins.values());
128
+ } catch (err: any) {
129
+ if (registry.plugins.size > 0) {
130
+ return Array.from(registry.plugins.values());
131
+ }
132
+ throw new Error(`Failed to discover middleware plugins: ${err.message}`);
133
+ }
134
+ }
135
+
136
+ // Tool 1: List all discovered middleware plugins
137
+ const listMiddlewarePlugins = tool(
138
+ async (input) => {
139
+ try {
140
+ const plugins = await fetchMiddlewarePlugins();
141
+ const filter = input.filter || "all";
142
+
143
+ let filtered = plugins;
144
+ if (filter === "loaded") {
145
+ filtered = plugins.filter((p: any) => p.loaded || registry.loadedPlugins.has(p.code));
146
+ } else if (filter === "available") {
147
+ filtered = plugins.filter((p: any) => !p.loaded && !registry.loadedPlugins.has(p.code));
148
+ }
149
+
150
+ return {
151
+ success: true,
152
+ total: filtered.length,
153
+ plugins: filtered.map((p: any) => ({
154
+ code: p.code,
155
+ name: p.name,
156
+ version: p.version,
157
+ description: p.description,
158
+ status: registry.loadedPlugins.has(p.code) ? "loaded" : "available",
159
+ loadedAt: registry.loadedPlugins.has(p.code)
160
+ ? registry.loadedPlugins.get(p.code).loadedAt
161
+ : null
162
+ }))
163
+ };
164
+ } catch (err: any) {
165
+ return {
166
+ success: false,
167
+ error: err.message,
168
+ plugins: []
169
+ };
170
+ }
171
+ },
172
+ {
173
+ name: "list_middleware_plugins",
174
+ description: "列出所有已发现的中间件类型插件,可按加载状态过滤。返回插件编码、名称、版本、描述和当前状态。",
175
+ schema: z.object({
176
+ filter: z.enum(["all", "loaded", "available"]).optional().default("all")
177
+ .describe("过滤条件:all=全部, loaded=已加载, available=可用未加载")
178
+ })
179
+ }
180
+ );
181
+
182
+ // Tool 2: Load a specific middleware plugin by code
183
+ const loadMiddlewarePlugin = tool(
184
+ async (input) => {
185
+ const pluginCode = input.pluginCode;
186
+
187
+ if (registry.loadedPlugins.has(pluginCode)) {
188
+ return {
189
+ success: true,
190
+ message: `插件 ${pluginCode} 已经处于加载状态`,
191
+ plugin: {
192
+ code: pluginCode,
193
+ loadedAt: registry.loadedPlugins.get(pluginCode).loadedAt,
194
+ status: "already_loaded"
195
+ }
196
+ };
197
+ }
198
+
199
+ const plugins = await fetchMiddlewarePlugins();
200
+ const target = plugins.find((p: any) => p.code === pluginCode);
201
+
202
+ if (!target) {
203
+ return {
204
+ success: false,
205
+ error: `未找到编码为 ${pluginCode} 的中间件插件`,
206
+ availablePlugins: plugins.map((p: any) => p.code)
207
+ };
208
+ }
209
+
210
+ try {
211
+ const fetch = (await import("node-fetch")).default;
212
+ const loadUrl = `${platformUrl}/api/plugin/load`;
213
+ const resp = await fetch(loadUrl, {
214
+ method: "POST",
215
+ headers: {
216
+ "Content-Type": "application/json",
217
+ ...(organizationId ? { "organization-id": organizationId } : {})
218
+ },
219
+ body: JSON.stringify({
220
+ pluginName: target.name || pluginCode,
221
+ version: target.version,
222
+ source: "npm"
223
+ }),
224
+ timeout: 30000
225
+ });
226
+
227
+ if (!resp.ok) {
228
+ const errText = await resp.text();
229
+ throw new Error(`Load API returned ${resp.status}: ${errText}`);
230
+ }
231
+
232
+ const result = await resp.json();
233
+
234
+ registry.loadedPlugins.set(pluginCode, {
235
+ code: pluginCode,
236
+ name: target.name,
237
+ version: target.version,
238
+ loadedAt: new Date().toISOString(),
239
+ loadResult: result
240
+ });
241
+
242
+ return {
243
+ success: true,
244
+ message: `插件 ${pluginCode} 加载成功`,
245
+ plugin: {
246
+ code: pluginCode,
247
+ name: target.name,
248
+ version: target.version,
249
+ loadedAt: registry.loadedPlugins.get(pluginCode).loadedAt,
250
+ status: "loaded"
251
+ }
252
+ };
253
+ } catch (err: any) {
254
+ registry.loadedPlugins.set(pluginCode, {
255
+ code: pluginCode,
256
+ name: target.name,
257
+ version: target.version,
258
+ loadedAt: new Date().toISOString(),
259
+ loadResult: { offline: true, error: err.message }
260
+ });
261
+
262
+ return {
263
+ success: true,
264
+ message: `插件 ${pluginCode} 已标记为加载(离线模式)`,
265
+ plugin: {
266
+ code: pluginCode,
267
+ name: target.name,
268
+ version: target.version,
269
+ loadedAt: registry.loadedPlugins.get(pluginCode).loadedAt,
270
+ status: "loaded_offline",
271
+ warning: err.message
272
+ }
273
+ };
274
+ }
275
+ },
276
+ {
277
+ name: "load_middleware_plugin",
278
+ description: "按插件编码动态加载指定的中间件插件。加载后该插件的工具和能力将对智能体可用。",
279
+ schema: z.object({
280
+ pluginCode: z.string().min(1).describe("要加载的中间件插件编码,例如 'weather-query' 或 'pg-query-tool'")
281
+ })
282
+ }
283
+ );
284
+
285
+ // Tool 3: Unload a specific middleware plugin
286
+ const unloadMiddlewarePlugin = tool(
287
+ async (input) => {
288
+ const pluginCode = input.pluginCode;
289
+
290
+ if (!registry.loadedPlugins.has(pluginCode)) {
291
+ return {
292
+ success: false,
293
+ error: `插件 ${pluginCode} 当前未加载,无需卸载`,
294
+ loadedPlugins: Array.from(registry.loadedPlugins.keys())
295
+ };
296
+ }
297
+
298
+ try {
299
+ const fetch = (await import("node-fetch")).default;
300
+ const unloadUrl = `${platformUrl}/api/plugin/unload`;
301
+ const resp = await fetch(unloadUrl, {
302
+ method: "POST",
303
+ headers: {
304
+ "Content-Type": "application/json",
305
+ ...(organizationId ? { "organization-id": organizationId } : {})
306
+ },
307
+ body: JSON.stringify({ pluginCode }),
308
+ timeout: 15000
309
+ });
310
+
311
+ if (!resp.ok) {
312
+ const errText = await resp.text();
313
+ throw new Error(`Unload API returned ${resp.status}: ${errText}`);
314
+ }
315
+ } catch (err: any) {
316
+ // Continue with local unload even if API fails
317
+ }
318
+
319
+ const removed = registry.loadedPlugins.get(pluginCode);
320
+ registry.loadedPlugins.delete(pluginCode);
321
+
322
+ return {
323
+ success: true,
324
+ message: `插件 ${pluginCode} 已卸载`,
325
+ plugin: {
326
+ code: pluginCode,
327
+ name: removed.name,
328
+ unloadedAt: new Date().toISOString(),
329
+ status: "unloaded"
330
+ }
331
+ };
332
+ },
333
+ {
334
+ name: "unload_middleware_plugin",
335
+ description: "按插件编码卸载已加载的中间件插件。卸载后该插件的工具将不再对智能体可用。",
336
+ schema: z.object({
337
+ pluginCode: z.string().min(1).describe("要卸载的中间件插件编码")
338
+ })
339
+ }
340
+ );
341
+
342
+ // Tool 4: Query detailed status of a specific middleware plugin
343
+ const getMiddlewarePluginStatus = tool(
344
+ async (input) => {
345
+ const pluginCode = input.pluginCode;
346
+ const plugins = await fetchMiddlewarePlugins();
347
+ const target = plugins.find((p: any) => p.code === pluginCode);
348
+
349
+ if (!target) {
350
+ return {
351
+ success: false,
352
+ error: `未找到编码为 ${pluginCode} 的中间件插件`
353
+ };
354
+ }
355
+
356
+ const loaded = registry.loadedPlugins.has(pluginCode);
357
+ const loadInfo = loaded ? registry.loadedPlugins.get(pluginCode) : null;
358
+
359
+ return {
360
+ success: true,
361
+ plugin: {
362
+ code: target.code,
363
+ name: target.name,
364
+ version: target.version,
365
+ description: target.description,
366
+ category: target.category,
367
+ status: loaded ? "loaded" : "available",
368
+ loadedAt: loadInfo ? loadInfo.loadedAt : null,
369
+ tools: loaded ? (loadInfo.tools || []) : [],
370
+ configRequired: target.configRequired || []
371
+ }
372
+ };
373
+ },
374
+ {
375
+ name: "get_middleware_plugin_status",
376
+ description: "查询指定中间件插件的详细状态信息,包括加载状态、版本、描述和可用工具列表。",
377
+ schema: z.object({
378
+ pluginCode: z.string().min(1).describe("要查询的中间件插件编码")
379
+ })
380
+ }
381
+ );
382
+
383
+ // Tool 5: Refresh the plugin discovery cache
384
+ const refreshPluginCache = tool(
385
+ async (input) => {
386
+ registry.lastRefresh = null;
387
+ try {
388
+ const plugins = await fetchMiddlewarePlugins();
389
+ return {
390
+ success: true,
391
+ message: `缓存已刷新,发现 ${plugins.length} 个中间件插件`,
392
+ total: plugins.length,
393
+ refreshedAt: new Date().toISOString()
394
+ };
395
+ } catch (err: any) {
396
+ return {
397
+ success: false,
398
+ error: `刷新缓存失败: ${err.message}`
399
+ };
400
+ }
401
+ },
402
+ {
403
+ name: "refresh_plugin_cache",
404
+ description: "刷新中间件插件发现缓存,强制从平台重新获取所有中间件类型插件列表。",
405
+ schema: z.object({})
406
+ }
407
+ );
408
+
409
+ // Tool 6: Batch load multiple middleware plugins
410
+ const batchLoadMiddlewarePlugins = tool(
411
+ async (input) => {
412
+ const pluginCodes = input.pluginCodes;
413
+ const results = [];
414
+
415
+ const plugins = await fetchMiddlewarePlugins();
416
+
417
+ for (const code of pluginCodes) {
418
+ if (registry.loadedPlugins.has(code)) {
419
+ results.push({
420
+ code,
421
+ status: "already_loaded",
422
+ success: true
423
+ });
424
+ continue;
425
+ }
426
+
427
+ const target = plugins.find((p: any) => p.code === code);
428
+ if (!target) {
429
+ results.push({
430
+ code,
431
+ status: "not_found",
432
+ success: false,
433
+ error: `未找到编码为 ${code} 的中间件插件`
434
+ });
435
+ continue;
436
+ }
437
+
438
+ registry.loadedPlugins.set(code, {
439
+ code,
440
+ name: target.name,
441
+ version: target.version,
442
+ loadedAt: new Date().toISOString(),
443
+ loadResult: { batch: true }
444
+ });
445
+
446
+ results.push({
447
+ code,
448
+ name: target.name,
449
+ version: target.version,
450
+ status: "loaded",
451
+ success: true,
452
+ loadedAt: registry.loadedPlugins.get(code).loadedAt
453
+ });
454
+ }
455
+
456
+ return {
457
+ success: true,
458
+ total: pluginCodes.length,
459
+ loaded: results.filter((r: any) => r.status === "loaded").length,
460
+ alreadyLoaded: results.filter((r: any) => r.status === "already_loaded").length,
461
+ failed: results.filter((r: any) => !r.success).length,
462
+ results
463
+ };
464
+ },
465
+ {
466
+ name: "batch_load_middleware_plugins",
467
+ description: "批量加载多个中间件插件。传入插件编码数组,一次性加载所有指定的中间件插件。",
468
+ schema: z.object({
469
+ pluginCodes: z.array(z.string().min(1)).min(1)
470
+ .describe("要批量加载的中间件插件编码数组,例如 ['weather-query', 'pg-query-tool']")
471
+ })
472
+ }
473
+ );
474
+
475
+ return {
476
+ name: MIDDLEWARE_PROVIDER,
477
+ tools: [
478
+ listMiddlewarePlugins,
479
+ loadMiddlewarePlugin,
480
+ unloadMiddlewarePlugin,
481
+ getMiddlewarePluginStatus,
482
+ refreshPluginCache,
483
+ batchLoadMiddlewarePlugins
484
+ ]
485
+ };
486
+ }
487
+ }
@@ -0,0 +1,15 @@
1
+ import { DynamicModule, Module } from "@nestjs/common";
2
+ import { PluginMiddleware } from "./middlewares/plugin-middleware.middleware";
3
+
4
+ @Module({})
5
+ export class PluginMiddlewareModule {
6
+ static register(ctx?: any): DynamicModule {
7
+ return {
8
+ module: PluginMiddlewareModule,
9
+ global: true,
10
+ providers: [PluginMiddleware],
11
+ controllers: [],
12
+ exports: [PluginMiddleware],
13
+ };
14
+ }
15
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * 插件中间件配置 Schema
5
+ *
6
+ * 定义插件运行所需的所有配置项,包括平台地址、组织 ID、自动发现开关和缓存策略。
7
+ */
8
+ export const PluginMiddlewareConfigSchema = z.object({
9
+ /** XpertAI 平台基础地址,用于调用插件发现和加载 API */
10
+ platformUrl: z.string().url().default("http://10.161.48.53:3300")
11
+ .describe("XpertAI 平台基础地址"),
12
+
13
+ /** 组织 ID,用于限定插件查询范围 */
14
+ organizationId: z.string().optional()
15
+ .describe("组织 ID,用于限定插件查询范围"),
16
+
17
+ /** 是否在启动时自动发现可用的中间件插件 */
18
+ autoDiscover: z.boolean().default(true)
19
+ .describe("是否在启动时自动发现可用的中间件插件"),
20
+
21
+ /** 插件发现缓存有效期(秒),过期后自动刷新 */
22
+ cacheTtlSeconds: z.number().int().min(10).max(3600).default(300)
23
+ .describe("插件发现缓存有效期(秒)"),
24
+
25
+ /** 加载超时时间(毫秒) */
26
+ loadTimeoutMs: z.number().int().min(1000).max(120000).default(30000)
27
+ .describe("插件加载超时时间(毫秒)"),
28
+
29
+ /** 是否启用离线模式(API 不可用时仍可本地管理插件状态) */
30
+ offlineMode: z.boolean().default(true)
31
+ .describe("是否启用离线模式")
32
+ });
33
+
34
+ export type PluginMiddlewareConfig = z.infer<typeof PluginMiddlewareConfigSchema>;