@timmy_hu/plugin-middleware 1.0.1 → 1.0.2
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/.xpertai-plugin/plugin.json +2 -2
- package/examples/request.example.json +90 -30
- package/index.js +275 -285
- package/package.json +1 -1
- package/src/middlewares/plugin-middleware.middleware.ts +271 -146
- package/src/schemas/config.schema.ts +3 -14
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
|
|
19
|
-
zh_Hans: "
|
|
18
|
+
en_US: "Discovers all middleware-type plugins installed on the XpertAI platform, shows their load status, and can install new middleware plugins from npm. Authenticates via /api/auth/login.",
|
|
19
|
+
zh_Hans: "自动发现 XpertAI 平台上所有已安装的中间件类型插件,展示加载状态,并支持从 npm 安装新的中间件插件。通过 /api/auth/login 登录获取 token。"
|
|
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,146 @@ 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
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
// In-memory state
|
|
65
|
+
const state = {
|
|
66
|
+
allPlugins: [], // 所有插件列表
|
|
67
|
+
middlewarePlugins: [], // 中间件类型插件列表
|
|
74
68
|
lastRefresh: null,
|
|
75
69
|
cacheTtl: cacheTtlSeconds * 1000,
|
|
76
70
|
token: null,
|
|
77
71
|
tokenExpiry: null
|
|
78
72
|
};
|
|
79
73
|
|
|
80
|
-
//
|
|
74
|
+
// ======== 登录:调用 /api/auth/login 获取 token ========
|
|
81
75
|
async function loginAndGetToken() {
|
|
82
76
|
const now = Date.now();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return registry.token;
|
|
77
|
+
if (state.token && state.tokenExpiry && now < state.tokenExpiry) {
|
|
78
|
+
return state.token;
|
|
86
79
|
}
|
|
87
|
-
|
|
88
80
|
if (!email || !password) {
|
|
89
81
|
throw new Error("缺少登录凭据:请在插件配置中填写 email 和 password");
|
|
90
82
|
}
|
|
91
|
-
|
|
92
83
|
const fetch = (await import("node-fetch")).default;
|
|
93
|
-
const
|
|
94
|
-
const resp = await fetch(loginUrl, {
|
|
84
|
+
const resp = await fetch(`${platformUrl}/api/auth/login`, {
|
|
95
85
|
method: "POST",
|
|
96
86
|
headers: { "Content-Type": "application/json" },
|
|
97
87
|
body: JSON.stringify({ email, password }),
|
|
98
88
|
timeout: 15000
|
|
99
89
|
});
|
|
100
|
-
|
|
101
90
|
if (!resp.ok) {
|
|
102
91
|
const errText = await resp.text();
|
|
103
92
|
throw new Error(`登录失败 (${resp.status}): ${errText}`);
|
|
104
93
|
}
|
|
105
|
-
|
|
106
94
|
const data = await resp.json();
|
|
107
95
|
const token = data.access_token || data.token || (data.data && data.data.access_token);
|
|
108
96
|
if (!token) {
|
|
109
|
-
throw new Error(`登录响应中未找到 token
|
|
97
|
+
throw new Error(`登录响应中未找到 token 字段`);
|
|
110
98
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Assume token expires in 50 minutes (conservative)
|
|
114
|
-
registry.tokenExpiry = now + 50 * 60 * 1000;
|
|
99
|
+
state.token = token;
|
|
100
|
+
state.tokenExpiry = now + 50 * 60 * 1000;
|
|
115
101
|
return token;
|
|
116
102
|
}
|
|
117
103
|
|
|
118
|
-
// ---- Helper: build auth headers ----
|
|
119
104
|
function authHeaders(token) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
};
|
|
124
|
-
if (organizationId) {
|
|
125
|
-
headers["organization-id"] = organizationId;
|
|
126
|
-
}
|
|
127
|
-
return headers;
|
|
105
|
+
const h = { "Content-Type": "application/json", "Authorization": `Bearer ${token}` };
|
|
106
|
+
if (organizationId) h["organization-id"] = organizationId;
|
|
107
|
+
return h;
|
|
128
108
|
}
|
|
129
109
|
|
|
130
|
-
//
|
|
131
|
-
async function
|
|
110
|
+
// ======== 获取所有已安装插件(GET /api/plugin) ========
|
|
111
|
+
async function fetchAllPlugins() {
|
|
132
112
|
const now = Date.now();
|
|
133
|
-
if (
|
|
134
|
-
return
|
|
113
|
+
if (state.lastRefresh && (now - state.lastRefresh) < state.cacheTtl && state.allPlugins.length > 0) {
|
|
114
|
+
return state.allPlugins;
|
|
135
115
|
}
|
|
136
|
-
|
|
137
116
|
const token = await loginAndGetToken();
|
|
138
117
|
const fetch = (await import("node-fetch")).default;
|
|
139
|
-
const
|
|
140
|
-
const resp = await fetch(url, {
|
|
118
|
+
const resp = await fetch(`${platformUrl}/api/plugin`, {
|
|
141
119
|
method: "GET",
|
|
142
120
|
headers: authHeaders(token),
|
|
143
|
-
timeout:
|
|
121
|
+
timeout: 15000
|
|
144
122
|
});
|
|
145
|
-
|
|
146
123
|
if (!resp.ok) {
|
|
147
|
-
// If 401, try re-login once
|
|
148
124
|
if (resp.status === 401) {
|
|
149
|
-
|
|
150
|
-
registry.tokenExpiry = null;
|
|
125
|
+
state.token = null; state.tokenExpiry = null;
|
|
151
126
|
const newToken = await loginAndGetToken();
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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);
|
|
127
|
+
const retry = await fetch(`${platformUrl}/api/plugin`, { method: "GET", headers: authHeaders(newToken), timeout: 15000 });
|
|
128
|
+
if (!retry.ok) throw new Error(`Platform API returned ${retry.status}`);
|
|
129
|
+
const retryData = await retry.json();
|
|
130
|
+
return processPluginList(retryData);
|
|
162
131
|
}
|
|
163
132
|
throw new Error(`Platform API returned ${resp.status}: ${resp.statusText}`);
|
|
164
133
|
}
|
|
165
|
-
|
|
166
134
|
const data = await resp.json();
|
|
167
|
-
return
|
|
135
|
+
return processPluginList(data);
|
|
168
136
|
}
|
|
169
137
|
|
|
170
|
-
function
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
138
|
+
function processPluginList(data) {
|
|
139
|
+
const list = Array.isArray(data) ? data : (data.data || data.items || data.plugins || []);
|
|
140
|
+
state.allPlugins = list;
|
|
141
|
+
state.middlewarePlugins = list.filter(p => {
|
|
142
|
+
const meta = p.meta || {};
|
|
143
|
+
return meta.category === "middleware";
|
|
144
|
+
});
|
|
145
|
+
state.lastRefresh = Date.now();
|
|
146
|
+
return state.allPlugins;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ======== 格式化中间件插件信息 ========
|
|
150
|
+
function formatMiddlewarePlugin(p) {
|
|
151
|
+
const meta = p.meta || {};
|
|
152
|
+
return {
|
|
153
|
+
name: p.name || meta.name,
|
|
154
|
+
packageName: p.packageName || "",
|
|
155
|
+
displayName: meta.displayName || p.name || meta.name,
|
|
156
|
+
version: meta.version || p.currentVersion || "unknown",
|
|
157
|
+
description: meta.description || "",
|
|
158
|
+
category: meta.category || "unknown",
|
|
159
|
+
author: meta.author || "",
|
|
160
|
+
keywords: meta.keywords || [],
|
|
161
|
+
source: p.source || "",
|
|
162
|
+
loadStatus: p.loadStatus || "unknown",
|
|
163
|
+
loadError: p.loadError || null,
|
|
164
|
+
configurationStatus: p.configurationStatus || null,
|
|
165
|
+
configurationError: p.configurationError || null,
|
|
166
|
+
canConfigure: p.canConfigure || false,
|
|
167
|
+
canUninstall: p.canUninstall || false,
|
|
168
|
+
canUpdate: p.canUpdate || false,
|
|
169
|
+
hasUpdate: p.hasUpdate || false,
|
|
170
|
+
effectiveInCurrentScope: p.effectiveInCurrentScope !== false,
|
|
171
|
+
componentSummary: p.componentSummary || { total: 0, skills: 0, mcpServers: 0, apps: 0, hooks: 0 }
|
|
172
|
+
};
|
|
189
173
|
}
|
|
190
174
|
|
|
191
175
|
// ======== Tool 1: list_middleware_plugins ========
|
|
192
176
|
const listMiddlewarePlugins = tool(
|
|
193
177
|
async (input) => {
|
|
194
178
|
try {
|
|
195
|
-
|
|
179
|
+
await fetchAllPlugins();
|
|
196
180
|
const filter = input.filter || "all";
|
|
197
|
-
let filtered =
|
|
181
|
+
let filtered = state.middlewarePlugins;
|
|
182
|
+
|
|
198
183
|
if (filter === "loaded") {
|
|
199
|
-
filtered =
|
|
200
|
-
} else if (filter === "
|
|
201
|
-
filtered =
|
|
184
|
+
filtered = filtered.filter(p => p.loadStatus === "loaded");
|
|
185
|
+
} else if (filter === "error") {
|
|
186
|
+
filtered = filtered.filter(p => p.loadStatus === "error" || p.loadError);
|
|
187
|
+
} else if (filter === "updatable") {
|
|
188
|
+
filtered = filtered.filter(p => p.hasUpdate === true);
|
|
202
189
|
}
|
|
190
|
+
|
|
203
191
|
return {
|
|
204
192
|
success: true,
|
|
205
193
|
total: filtered.length,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
}))
|
|
194
|
+
totalAllPlugins: state.allPlugins.length,
|
|
195
|
+
totalMiddleware: state.middlewarePlugins.length,
|
|
196
|
+
filter,
|
|
197
|
+
plugins: filtered.map(formatMiddlewarePlugin)
|
|
216
198
|
};
|
|
217
199
|
} catch (err) {
|
|
218
200
|
return { success: false, error: err.message, plugins: [] };
|
|
@@ -220,262 +202,270 @@ class PluginMiddleware {
|
|
|
220
202
|
},
|
|
221
203
|
{
|
|
222
204
|
name: "list_middleware_plugins",
|
|
223
|
-
description: "
|
|
205
|
+
description: "列出平台上所有已安装的中间件类型插件,可按状态过滤。返回插件名称、版本、描述、加载状态等详细信息。",
|
|
224
206
|
schema: z.object({
|
|
225
|
-
filter: z.enum(["all", "loaded", "
|
|
226
|
-
.describe("过滤条件:all=全部, loaded=已加载,
|
|
207
|
+
filter: z.enum(["all", "loaded", "error", "updatable"]).optional().default("all")
|
|
208
|
+
.describe("过滤条件:all=全部, loaded=已加载, error=加载出错, updatable=有可用更新")
|
|
227
209
|
})
|
|
228
210
|
}
|
|
229
211
|
);
|
|
230
212
|
|
|
231
|
-
// ======== Tool 2:
|
|
232
|
-
const
|
|
213
|
+
// ======== Tool 2: get_middleware_plugin_detail ========
|
|
214
|
+
const getMiddlewarePluginDetail = tool(
|
|
233
215
|
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
|
-
}
|
|
242
|
-
|
|
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
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
216
|
try {
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
217
|
+
await fetchAllPlugins();
|
|
218
|
+
const keyword = input.pluginNameOrPackage;
|
|
219
|
+
const found = state.middlewarePlugins.find(p => {
|
|
220
|
+
const meta = p.meta || {};
|
|
221
|
+
return p.name === keyword
|
|
222
|
+
|| meta.name === keyword
|
|
223
|
+
|| p.packageName === keyword
|
|
224
|
+
|| meta.displayName === keyword;
|
|
261
225
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
226
|
+
|
|
227
|
+
if (!found) {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: `未找到名为 "${keyword}" 的中间件插件`,
|
|
231
|
+
availableMiddleware: state.middlewarePlugins.map(p => ({
|
|
232
|
+
name: p.name, packageName: p.packageName, displayName: (p.meta || {}).displayName
|
|
233
|
+
}))
|
|
234
|
+
};
|
|
265
235
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
236
|
+
|
|
237
|
+
const detail = formatMiddlewarePlugin(found);
|
|
238
|
+
|
|
239
|
+
// 尝试获取组件信息
|
|
240
|
+
try {
|
|
241
|
+
const token = await loginAndGetToken();
|
|
242
|
+
const fetch = (await import("node-fetch")).default;
|
|
243
|
+
const compResp = await fetch(`${platformUrl}/api/plugin/${encodeURIComponent(found.name)}/components`, {
|
|
244
|
+
method: "GET", headers: authHeaders(token), timeout: 10000
|
|
245
|
+
});
|
|
246
|
+
if (compResp.ok) {
|
|
247
|
+
const compData = await compResp.json();
|
|
248
|
+
detail.components = compData.items || compData;
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
detail.components = [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { success: true, plugin: detail };
|
|
275
255
|
} catch (err) {
|
|
276
|
-
|
|
277
|
-
code: pluginCode, name: target.name, version: target.version,
|
|
278
|
-
loadedAt: new Date().toISOString(), loadResult: { offline: true, error: err.message }
|
|
279
|
-
});
|
|
280
|
-
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 }
|
|
283
|
-
};
|
|
256
|
+
return { success: false, error: err.message };
|
|
284
257
|
}
|
|
285
258
|
},
|
|
286
259
|
{
|
|
287
|
-
name: "
|
|
288
|
-
description: "
|
|
260
|
+
name: "get_middleware_plugin_detail",
|
|
261
|
+
description: "查询指定中间件插件的详细信息,包括加载状态、配置状态、组件列表、版本信息等。",
|
|
289
262
|
schema: z.object({
|
|
290
|
-
|
|
263
|
+
pluginNameOrPackage: z.string().min(1)
|
|
264
|
+
.describe("插件名称、packageName 或 displayName,例如 'echarts-generator' 或 '@timmy_hu/bulk-email-sender'")
|
|
291
265
|
})
|
|
292
266
|
}
|
|
293
267
|
);
|
|
294
268
|
|
|
295
|
-
// ======== Tool 3:
|
|
296
|
-
const
|
|
269
|
+
// ======== Tool 3: install_middleware_plugin ========
|
|
270
|
+
const installMiddlewarePlugin = tool(
|
|
297
271
|
async (input) => {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
success: false,
|
|
302
|
-
error: `插件 ${pluginCode} 当前未加载,无需卸载`,
|
|
303
|
-
loadedPlugins: Array.from(registry.loadedPlugins.keys())
|
|
304
|
-
};
|
|
305
|
-
}
|
|
272
|
+
const packageName = input.packageName;
|
|
273
|
+
const version = input.version || "latest";
|
|
274
|
+
|
|
306
275
|
try {
|
|
307
276
|
const token = await loginAndGetToken();
|
|
308
277
|
const fetch = (await import("node-fetch")).default;
|
|
309
|
-
const resp = await fetch(`${platformUrl}/api/plugin
|
|
278
|
+
const resp = await fetch(`${platformUrl}/api/plugin`, {
|
|
310
279
|
method: "POST",
|
|
311
280
|
headers: authHeaders(token),
|
|
312
|
-
body: JSON.stringify({
|
|
313
|
-
timeout:
|
|
281
|
+
body: JSON.stringify({ pluginName: packageName, version, source: "npm" }),
|
|
282
|
+
timeout: 60000
|
|
314
283
|
});
|
|
284
|
+
|
|
315
285
|
if (!resp.ok) {
|
|
316
286
|
const errText = await resp.text();
|
|
317
|
-
throw new Error(
|
|
287
|
+
throw new Error(`安装失败 (${resp.status}): ${errText}`);
|
|
318
288
|
}
|
|
289
|
+
|
|
290
|
+
const result = await resp.json();
|
|
291
|
+
|
|
292
|
+
// 刷新缓存
|
|
293
|
+
state.lastRefresh = null;
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
success: true,
|
|
297
|
+
message: `插件 ${packageName} 安装成功`,
|
|
298
|
+
installed: {
|
|
299
|
+
name: result.name || packageName,
|
|
300
|
+
packageName: result.packageName || packageName,
|
|
301
|
+
version: result.currentVersion || version,
|
|
302
|
+
organizationId: result.organizationId || organizationId
|
|
303
|
+
}
|
|
304
|
+
};
|
|
319
305
|
} catch (err) {
|
|
320
|
-
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
error: err.message,
|
|
309
|
+
packageName,
|
|
310
|
+
version
|
|
311
|
+
};
|
|
321
312
|
}
|
|
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
313
|
},
|
|
329
314
|
{
|
|
330
|
-
name: "
|
|
331
|
-
description: "
|
|
315
|
+
name: "install_middleware_plugin",
|
|
316
|
+
description: "从 npm 安装一个新的中间件插件到 XpertAI 平台。安装后插件将自动加载并可供智能体使用。",
|
|
332
317
|
schema: z.object({
|
|
333
|
-
|
|
318
|
+
packageName: z.string().min(1).describe("npm 包名,例如 '@timmy_hu/bulk-email-sender'"),
|
|
319
|
+
version: z.string().optional().default("latest").describe("版本号,默认为 latest")
|
|
334
320
|
})
|
|
335
321
|
}
|
|
336
322
|
);
|
|
337
323
|
|
|
338
|
-
// ======== Tool 4:
|
|
339
|
-
const
|
|
324
|
+
// ======== Tool 4: refresh_plugin_list ========
|
|
325
|
+
const refreshPluginList = tool(
|
|
340
326
|
async (input) => {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
327
|
+
state.lastRefresh = null;
|
|
328
|
+
state.token = null;
|
|
329
|
+
state.tokenExpiry = null;
|
|
330
|
+
try {
|
|
331
|
+
await fetchAllPlugins();
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
message: `插件列表已刷新,共发现 ${state.allPlugins.length} 个插件,其中 ${state.middlewarePlugins.length} 个中间件类型`,
|
|
335
|
+
totalPlugins: state.allPlugins.length,
|
|
336
|
+
totalMiddleware: state.middlewarePlugins.length,
|
|
337
|
+
refreshedAt: new Date().toISOString()
|
|
338
|
+
};
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return { success: false, error: `刷新失败: ${err.message}` };
|
|
346
341
|
}
|
|
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
342
|
},
|
|
361
343
|
{
|
|
362
|
-
name: "
|
|
363
|
-
description: "
|
|
364
|
-
schema: z.object({
|
|
365
|
-
pluginCode: z.string().min(1).describe("要查询的中间件插件编码")
|
|
366
|
-
})
|
|
344
|
+
name: "refresh_plugin_list",
|
|
345
|
+
description: "刷新插件列表缓存,强制重新登录并从平台获取所有已安装插件的最新状态。",
|
|
346
|
+
schema: z.object({})
|
|
367
347
|
}
|
|
368
348
|
);
|
|
369
349
|
|
|
370
|
-
// ======== Tool 5:
|
|
371
|
-
const
|
|
350
|
+
// ======== Tool 5: check_login_status ========
|
|
351
|
+
const checkLoginStatus = tool(
|
|
372
352
|
async (input) => {
|
|
373
|
-
registry.lastRefresh = null;
|
|
374
|
-
registry.token = null;
|
|
375
|
-
registry.tokenExpiry = null;
|
|
376
353
|
try {
|
|
377
|
-
const
|
|
354
|
+
const token = await loginAndGetToken();
|
|
378
355
|
return {
|
|
379
356
|
success: true,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
357
|
+
loggedIn: true,
|
|
358
|
+
email,
|
|
359
|
+
platformUrl,
|
|
360
|
+
organizationId: organizationId || "(未设置)",
|
|
361
|
+
tokenExpiry: state.tokenExpiry ? new Date(state.tokenExpiry).toISOString() : null,
|
|
362
|
+
cachedPlugins: state.allPlugins.length,
|
|
363
|
+
cachedMiddleware: state.middlewarePlugins.length,
|
|
364
|
+
message: "登录状态正常,token 有效"
|
|
383
365
|
};
|
|
384
366
|
} catch (err) {
|
|
385
|
-
return {
|
|
367
|
+
return {
|
|
368
|
+
success: false,
|
|
369
|
+
loggedIn: false,
|
|
370
|
+
email,
|
|
371
|
+
platformUrl,
|
|
372
|
+
error: err.message
|
|
373
|
+
};
|
|
386
374
|
}
|
|
387
375
|
},
|
|
388
376
|
{
|
|
389
|
-
name: "
|
|
390
|
-
description: "
|
|
377
|
+
name: "check_login_status",
|
|
378
|
+
description: "检查当前平台登录状态,验证 email/password 是否有效、token 是否过期,以及缓存中有多少插件。",
|
|
391
379
|
schema: z.object({})
|
|
392
380
|
}
|
|
393
381
|
);
|
|
394
382
|
|
|
395
|
-
// ======== Tool 6:
|
|
396
|
-
const
|
|
383
|
+
// ======== Tool 6: search_middleware_plugins ========
|
|
384
|
+
const searchMiddlewarePlugins = tool(
|
|
397
385
|
async (input) => {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
timeout: 30000
|
|
419
|
-
});
|
|
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
|
|
425
|
-
});
|
|
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 }
|
|
431
|
-
});
|
|
432
|
-
results.push({ code, name: target.name, version: target.version, status: "loaded_offline", success: true, warning: err.message });
|
|
433
|
-
}
|
|
386
|
+
try {
|
|
387
|
+
await fetchAllPlugins();
|
|
388
|
+
const keyword = (input.keyword || "").toLowerCase();
|
|
389
|
+
const results = state.middlewarePlugins.filter(p => {
|
|
390
|
+
const meta = p.meta || {};
|
|
391
|
+
const searchFields = [
|
|
392
|
+
p.name, p.packageName, meta.name, meta.displayName,
|
|
393
|
+
meta.description, (meta.keywords || []).join(" ")
|
|
394
|
+
].map(s => (s || "").toLowerCase());
|
|
395
|
+
return searchFields.some(f => f.includes(keyword));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
success: true,
|
|
400
|
+
keyword: input.keyword,
|
|
401
|
+
total: results.length,
|
|
402
|
+
plugins: results.map(formatMiddlewarePlugin)
|
|
403
|
+
};
|
|
404
|
+
} catch (err) {
|
|
405
|
+
return { success: false, error: err.message, plugins: [] };
|
|
434
406
|
}
|
|
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
407
|
},
|
|
443
408
|
{
|
|
444
|
-
name: "
|
|
445
|
-
description: "
|
|
409
|
+
name: "search_middleware_plugins",
|
|
410
|
+
description: "按关键词搜索中间件类型插件。搜索范围包括插件名称、包名、描述和关键词。",
|
|
446
411
|
schema: z.object({
|
|
447
|
-
|
|
448
|
-
.describe("要批量加载的中间件插件编码数组,例如 ['weather-query', 'pg-query-tool']")
|
|
412
|
+
keyword: z.string().min(1).describe("搜索关键词,例如 'echarts'、'email'、'retry'")
|
|
449
413
|
})
|
|
450
414
|
}
|
|
451
415
|
);
|
|
452
416
|
|
|
453
|
-
// ======== Tool 7:
|
|
454
|
-
const
|
|
417
|
+
// ======== Tool 7: get_plugin_statistics ========
|
|
418
|
+
const getPluginStatistics = tool(
|
|
455
419
|
async (input) => {
|
|
456
420
|
try {
|
|
457
|
-
|
|
421
|
+
await fetchAllPlugins();
|
|
422
|
+
|
|
423
|
+
// 按 category 统计
|
|
424
|
+
const categoryStats = {};
|
|
425
|
+
for (const p of state.allPlugins) {
|
|
426
|
+
const cat = (p.meta || {}).category || "unknown";
|
|
427
|
+
categoryStats[cat] = (categoryStats[cat] || 0) + 1;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 按 loadStatus 统计中间件
|
|
431
|
+
const loadStatusStats = {};
|
|
432
|
+
for (const p of state.middlewarePlugins) {
|
|
433
|
+
const ls = p.loadStatus || "unknown";
|
|
434
|
+
loadStatusStats[ls] = (loadStatusStats[ls] || 0) + 1;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 有更新的插件
|
|
438
|
+
const updatable = state.middlewarePlugins.filter(p => p.hasUpdate === true);
|
|
439
|
+
|
|
440
|
+
// 有配置错误的插件
|
|
441
|
+
const configErrors = state.middlewarePlugins.filter(p => p.configurationError);
|
|
442
|
+
|
|
443
|
+
// 有加载错误的插件
|
|
444
|
+
const loadErrors = state.middlewarePlugins.filter(p => p.loadError);
|
|
445
|
+
|
|
458
446
|
return {
|
|
459
447
|
success: true,
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
448
|
+
statistics: {
|
|
449
|
+
totalPlugins: state.allPlugins.length,
|
|
450
|
+
totalMiddleware: state.middlewarePlugins.length,
|
|
451
|
+
byCategory: categoryStats,
|
|
452
|
+
middlewareByLoadStatus: loadStatusStats,
|
|
453
|
+
updatableCount: updatable.length,
|
|
454
|
+
updatablePlugins: updatable.map(p => ({ name: p.name, packageName: p.packageName, version: (p.meta || {}).version })),
|
|
455
|
+
configErrorCount: configErrors.length,
|
|
456
|
+
configErrorPlugins: configErrors.map(p => ({ name: p.name, error: p.configurationError })),
|
|
457
|
+
loadErrorCount: loadErrors.length,
|
|
458
|
+
loadErrorPlugins: loadErrors.map(p => ({ name: p.name, error: p.loadError }))
|
|
459
|
+
},
|
|
460
|
+
generatedAt: new Date().toISOString()
|
|
465
461
|
};
|
|
466
462
|
} catch (err) {
|
|
467
|
-
return {
|
|
468
|
-
success: false,
|
|
469
|
-
loggedIn: false,
|
|
470
|
-
email: email,
|
|
471
|
-
platformUrl: platformUrl,
|
|
472
|
-
error: err.message
|
|
473
|
-
};
|
|
463
|
+
return { success: false, error: err.message };
|
|
474
464
|
}
|
|
475
465
|
},
|
|
476
466
|
{
|
|
477
|
-
name: "
|
|
478
|
-
description: "
|
|
467
|
+
name: "get_plugin_statistics",
|
|
468
|
+
description: "获取平台上所有插件的统计信息,包括按分类统计、按加载状态统计、有更新的插件、有错误的插件等。",
|
|
479
469
|
schema: z.object({})
|
|
480
470
|
}
|
|
481
471
|
);
|
|
@@ -484,12 +474,12 @@ class PluginMiddleware {
|
|
|
484
474
|
name: MIDDLEWARE_PROVIDER,
|
|
485
475
|
tools: [
|
|
486
476
|
listMiddlewarePlugins,
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
477
|
+
getMiddlewarePluginDetail,
|
|
478
|
+
installMiddlewarePlugin,
|
|
479
|
+
refreshPluginList,
|
|
480
|
+
checkLoginStatus,
|
|
481
|
+
searchMiddlewarePlugins,
|
|
482
|
+
getPluginStatistics
|
|
493
483
|
]
|
|
494
484
|
};
|
|
495
485
|
}
|
|
@@ -503,12 +493,12 @@ class PluginModule {}
|
|
|
503
493
|
const plugin = {
|
|
504
494
|
meta: {
|
|
505
495
|
name: "@timmy_hu/plugin-middleware",
|
|
506
|
-
version: "1.0.
|
|
496
|
+
version: "1.0.2",
|
|
507
497
|
level: "organization",
|
|
508
498
|
category: "middleware",
|
|
509
499
|
displayName: "插件中间件",
|
|
510
|
-
description: "
|
|
511
|
-
keywords: ["middleware", "plugin", "discovery", "
|
|
500
|
+
description: "自动发现 XpertAI 平台上所有已安装的中间件类型插件,展示加载状态,支持从 npm 安装新插件、搜索、统计。通过 /api/auth/login 登录获取 token。",
|
|
501
|
+
keywords: ["middleware", "plugin", "discovery", "install", "agent", "orchestration"],
|
|
512
502
|
author: "fpi"
|
|
513
503
|
},
|
|
514
504
|
register(ctx) {
|