@dainprotocol/service-sdk 2.0.87 → 2.0.89
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/dist/client/client.d.ts +39 -6
- package/dist/client/client.js +253 -9
- package/dist/client/client.js.map +1 -1
- package/dist/client/types.d.ts +273 -6
- package/dist/client/types.js +129 -5
- package/dist/client/types.js.map +1 -1
- package/dist/extensions/telegram-oauth.js +1 -276
- package/dist/extensions/telegram-oauth.js.map +1 -1
- package/dist/lib/schemaStructure.js +1 -0
- package/dist/lib/schemaStructure.js.map +1 -1
- package/dist/lib/toolDiscovery.d.ts +20 -0
- package/dist/lib/toolDiscovery.js +30 -0
- package/dist/lib/toolDiscovery.js.map +1 -0
- package/dist/plugins/crypto-plugin.js +17 -12
- package/dist/plugins/crypto-plugin.js.map +1 -1
- package/dist/service/auth.d.ts +2 -2
- package/dist/service/auth.js +39 -8
- package/dist/service/auth.js.map +1 -1
- package/dist/service/core.js +3 -1
- package/dist/service/core.js.map +1 -1
- package/dist/service/nodeService.js +2 -2
- package/dist/service/nodeService.js.map +1 -1
- package/dist/service/server.js +556 -94
- package/dist/service/server.js.map +1 -1
- package/dist/service/types.d.ts +133 -4
- package/dist/service/webhooks.d.ts +8 -8
- package/dist/service/webhooks.js +2 -1
- package/dist/service/webhooks.js.map +1 -1
- package/package.json +13 -12
package/dist/service/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const cors_1 = require("hono/cors");
|
|
|
9
9
|
const http_exception_1 = require("hono/http-exception");
|
|
10
10
|
const auth_1 = require("./auth");
|
|
11
11
|
const schemaStructure_1 = require("../lib/schemaStructure");
|
|
12
|
+
const toolDiscovery_1 = require("../lib/toolDiscovery");
|
|
12
13
|
const oauth2_1 = require("./oauth2");
|
|
13
14
|
const bs58_1 = tslib_1.__importDefault(require("bs58"));
|
|
14
15
|
const processes_1 = require("./processes");
|
|
@@ -32,6 +33,21 @@ function debugWarn(...args) {
|
|
|
32
33
|
console.warn(...args);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
function getFirstForwardedValue(value) {
|
|
37
|
+
if (!value)
|
|
38
|
+
return undefined;
|
|
39
|
+
const first = value.split(",")[0]?.trim();
|
|
40
|
+
return first || undefined;
|
|
41
|
+
}
|
|
42
|
+
function normalizeForwardedPrefix(prefix) {
|
|
43
|
+
if (!prefix)
|
|
44
|
+
return undefined;
|
|
45
|
+
const trimmed = prefix.trim();
|
|
46
|
+
if (!trimmed || trimmed === "/")
|
|
47
|
+
return undefined;
|
|
48
|
+
const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
49
|
+
return normalized.replace(/\/+$/, "");
|
|
50
|
+
}
|
|
35
51
|
/**
|
|
36
52
|
* Safely parse JSON body from request, returning empty object on failure.
|
|
37
53
|
*/
|
|
@@ -63,23 +79,30 @@ function requireScope(requiredScope) {
|
|
|
63
79
|
// Helper to sign and stream SSE events
|
|
64
80
|
function signedStreamSSE(c, privateKey, config, handler) {
|
|
65
81
|
return (0, streaming_1.streamSSE)(c, async (stream) => {
|
|
82
|
+
const requestSignal = c.req.raw?.signal;
|
|
83
|
+
const isAborted = () => stream.aborted || stream.closed || requestSignal?.aborted === true;
|
|
66
84
|
const signedStream = {
|
|
67
85
|
writeSSE: async (event) => {
|
|
86
|
+
if (isAborted())
|
|
87
|
+
return;
|
|
68
88
|
const timestamp = Date.now().toString();
|
|
69
89
|
const message = `${event.data}:${timestamp}`;
|
|
70
90
|
const messageHash = (0, sha256_1.sha256)(message);
|
|
71
91
|
const signatureBytes = ed25519_1.ed25519.sign(messageHash, privateKey);
|
|
72
92
|
const signature = (0, utils_1.bytesToHex)(signatureBytes);
|
|
73
93
|
// Fast path for non-critical events (progress/UI updates)
|
|
74
|
-
const isCriticalEvent = event.event === 'result' ||
|
|
94
|
+
const isCriticalEvent = event.event === 'result' ||
|
|
95
|
+
event.event === 'process-created' ||
|
|
96
|
+
event.event === 'datasource-update';
|
|
75
97
|
const dataWithSignature = isCriticalEvent
|
|
76
98
|
? JSON.stringify({
|
|
77
99
|
data: JSON.parse(event.data),
|
|
78
100
|
_signature: { signature, timestamp, agentId: config.identity.agentId, orgId: config.identity.orgId, address: config.identity.publicKey }
|
|
79
101
|
})
|
|
80
102
|
: `{"data":${event.data},"_signature":{"signature":"${signature}","timestamp":"${timestamp}","agentId":"${config.identity.agentId}","orgId":"${config.identity.orgId}","address":"${config.identity.publicKey}"}}`;
|
|
81
|
-
|
|
82
|
-
}
|
|
103
|
+
await stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
|
|
104
|
+
},
|
|
105
|
+
isAborted,
|
|
83
106
|
};
|
|
84
107
|
await handler(signedStream);
|
|
85
108
|
});
|
|
@@ -159,11 +182,7 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
159
182
|
const isHITLPath = hitlPath && c.req.path === hitlPath;
|
|
160
183
|
if (c.req.path.startsWith("/oauth2/callback/") ||
|
|
161
184
|
c.req.path.startsWith("/addons") ||
|
|
162
|
-
c.req.path.startsWith("/getAllToolsAsJsonSchema") ||
|
|
163
|
-
c.req.path.startsWith("/getSkills") ||
|
|
164
|
-
c.req.path.startsWith("/getWebhookTriggers") ||
|
|
165
185
|
c.req.path.startsWith("/metadata") ||
|
|
166
|
-
c.req.path.startsWith("/recommendations") ||
|
|
167
186
|
c.req.path.startsWith("/ping") ||
|
|
168
187
|
isWebhookPath ||
|
|
169
188
|
isHITLPath) {
|
|
@@ -177,8 +196,9 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
177
196
|
debugLog(`[Auth Middleware] JWT present: ${!!jwtToken}, API Key present: ${!!apiKey}`);
|
|
178
197
|
if (jwtToken) {
|
|
179
198
|
// JWT Authentication (Users)
|
|
180
|
-
// Auto-detect JWT audience from
|
|
181
|
-
const host = c.req.header("x-forwarded-host") ||
|
|
199
|
+
// Auto-detect JWT audience from forwarded headers (works with localhost, path-based tunnels, production)
|
|
200
|
+
const host = getFirstForwardedValue(c.req.header("x-forwarded-host")) ||
|
|
201
|
+
getFirstForwardedValue(c.req.header("host"));
|
|
182
202
|
if (!host) {
|
|
183
203
|
throw new http_exception_1.HTTPException(400, { message: "Unable to determine service URL. Host header is required." });
|
|
184
204
|
}
|
|
@@ -186,22 +206,33 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
186
206
|
if (host.includes(' ') || host.includes('\n') || host.includes('\r')) {
|
|
187
207
|
throw new http_exception_1.HTTPException(400, { message: "Invalid Host header format" });
|
|
188
208
|
}
|
|
189
|
-
const xForwardedProto = c.req.header("x-forwarded-proto");
|
|
190
|
-
const protocol = xForwardedProto
|
|
191
|
-
|
|
209
|
+
const xForwardedProto = getFirstForwardedValue(c.req.header("x-forwarded-proto"));
|
|
210
|
+
const protocol = xForwardedProto === "http" || xForwardedProto === "https"
|
|
211
|
+
? xForwardedProto
|
|
212
|
+
: (host.includes("localhost") || host.startsWith("127.") ? "http" : "https");
|
|
213
|
+
const forwardedPrefix = normalizeForwardedPrefix(getFirstForwardedValue(c.req.header("x-forwarded-prefix")));
|
|
214
|
+
const jwtAudiences = [`${protocol}://${host}`];
|
|
215
|
+
if (forwardedPrefix) {
|
|
216
|
+
jwtAudiences.push(`${protocol}://${host}${forwardedPrefix}`);
|
|
217
|
+
}
|
|
192
218
|
const publicKeyOrUrl = config.jwtPublicKey || config.dainIdUrl || "https://id.dain.org";
|
|
193
219
|
const issuer = config.jwtIssuer || "dainid-oauth";
|
|
194
220
|
const result = await (0, auth_1.verifyJWT)(jwtToken, publicKeyOrUrl, {
|
|
195
221
|
issuer: issuer,
|
|
196
|
-
audience:
|
|
222
|
+
audience: jwtAudiences,
|
|
197
223
|
});
|
|
198
224
|
if (!result.valid) {
|
|
199
225
|
throw new http_exception_1.HTTPException(401, { message: `JWT verification failed: ${result.error}` });
|
|
200
226
|
}
|
|
201
227
|
// Defense in depth: double-check audience claim
|
|
202
|
-
|
|
228
|
+
const tokenAudiences = Array.isArray(result.payload?.aud)
|
|
229
|
+
? result.payload.aud
|
|
230
|
+
: [result.payload?.aud];
|
|
231
|
+
const expectedAudienceSet = new Set(jwtAudiences);
|
|
232
|
+
const audienceMatched = tokenAudiences.some((aud) => aud && expectedAudienceSet.has(aud));
|
|
233
|
+
if (!audienceMatched) {
|
|
203
234
|
throw new http_exception_1.HTTPException(403, {
|
|
204
|
-
message: `JWT audience mismatch. Expected: ${
|
|
235
|
+
message: `JWT audience mismatch. Expected one of: ${jwtAudiences.join(", ")}, Got: ${tokenAudiences.filter(Boolean).join(", ")}`
|
|
205
236
|
});
|
|
206
237
|
}
|
|
207
238
|
c.set('authMethod', 'jwt');
|
|
@@ -273,15 +304,73 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
273
304
|
debugLog("[getAgentInfo] API Key - Returning agent info:", agentInfo);
|
|
274
305
|
return agentInfo;
|
|
275
306
|
}
|
|
307
|
+
async function resolveHealthStatus() {
|
|
308
|
+
if (!config.healthCheck)
|
|
309
|
+
return true;
|
|
310
|
+
try {
|
|
311
|
+
return await config.healthCheck();
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
276
317
|
// Setup default ping route
|
|
277
|
-
app.get("/ping", (c) =>
|
|
318
|
+
app.get("/ping", async (c) => {
|
|
319
|
+
const healthy = await resolveHealthStatus();
|
|
320
|
+
return c.json({
|
|
321
|
+
message: healthy ? "pong" : "unhealthy",
|
|
322
|
+
platform: "DAIN",
|
|
323
|
+
service_version: metadata.version,
|
|
324
|
+
dain_sdk_version: package_json_1.default.version,
|
|
325
|
+
}, healthy ? 200 : 503);
|
|
326
|
+
});
|
|
278
327
|
// Metadata endpoint - includes computed supportsUserActions from tools
|
|
279
328
|
app.get("/metadata", (c) => {
|
|
280
329
|
// Compute service-level capability: does ANY tool support user actions (HITL)?
|
|
281
330
|
const supportsUserActions = tools.some((tool) => tool.supportsUserActions === true);
|
|
331
|
+
const requestedContract = c.req.header("x-butterfly-contract") || c.req.header("X-Butterfly-Contract");
|
|
332
|
+
const sdkMajor = Number.parseInt(String(package_json_1.default.version).split(".")[0] || "0", 10);
|
|
333
|
+
const contractVersion = Number.isFinite(sdkMajor) && sdkMajor > 0 ? `${sdkMajor}.0.0` : "0.0.0";
|
|
334
|
+
const capabilities = {
|
|
335
|
+
tools: true,
|
|
336
|
+
contexts: true,
|
|
337
|
+
widgets: true,
|
|
338
|
+
datasources: true,
|
|
339
|
+
streamingTools: true,
|
|
340
|
+
streamingDatasources: true,
|
|
341
|
+
datasourcePolicy: true,
|
|
342
|
+
widgetPolicy: true,
|
|
343
|
+
toolSafety: true,
|
|
344
|
+
};
|
|
345
|
+
const compatibility = (() => {
|
|
346
|
+
if (!requestedContract)
|
|
347
|
+
return undefined;
|
|
348
|
+
const requestedMajor = Number.parseInt(String(requestedContract).split(".")[0] || "0", 10);
|
|
349
|
+
if (!Number.isFinite(requestedMajor) || requestedMajor <= 0) {
|
|
350
|
+
return {
|
|
351
|
+
requested: requestedContract,
|
|
352
|
+
contractVersion,
|
|
353
|
+
ok: false,
|
|
354
|
+
reason: "Invalid requested contract version",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const ok = requestedMajor === sdkMajor;
|
|
358
|
+
return ok
|
|
359
|
+
? { requested: requestedContract, contractVersion, ok: true }
|
|
360
|
+
: {
|
|
361
|
+
requested: requestedContract,
|
|
362
|
+
contractVersion,
|
|
363
|
+
ok: false,
|
|
364
|
+
reason: `Requested major ${requestedMajor} does not match service major ${sdkMajor}`,
|
|
365
|
+
};
|
|
366
|
+
})();
|
|
282
367
|
return c.json({
|
|
283
368
|
...metadata,
|
|
284
369
|
supportsUserActions,
|
|
370
|
+
dainSdkVersion: package_json_1.default.version,
|
|
371
|
+
contractVersion,
|
|
372
|
+
capabilities,
|
|
373
|
+
...(compatibility ? { compatibility } : {}),
|
|
285
374
|
});
|
|
286
375
|
});
|
|
287
376
|
// Tools list endpoint
|
|
@@ -300,6 +389,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
300
389
|
outputSchema,
|
|
301
390
|
interface: tool.interface,
|
|
302
391
|
suggestConfirmation: tool.suggestConfirmation,
|
|
392
|
+
sideEffectClass: tool.sideEffectClass,
|
|
393
|
+
supportsParallel: tool.supportsParallel,
|
|
394
|
+
idempotencyScope: tool.idempotencyScope,
|
|
395
|
+
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
303
396
|
supportsUserActions: tool.supportsUserActions,
|
|
304
397
|
};
|
|
305
398
|
});
|
|
@@ -400,14 +493,26 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
400
493
|
oauth2Client,
|
|
401
494
|
app
|
|
402
495
|
});
|
|
403
|
-
|
|
496
|
+
const response = {
|
|
404
497
|
id: widget.id,
|
|
405
498
|
name: widget.name,
|
|
406
499
|
description: widget.description,
|
|
407
500
|
icon: widget.icon,
|
|
408
501
|
size: widget.size || "sm",
|
|
502
|
+
refreshIntervalMs: widget.refreshIntervalMs,
|
|
409
503
|
...widgetData
|
|
410
504
|
};
|
|
505
|
+
if (!("freshness" in response)) {
|
|
506
|
+
response.freshness = {
|
|
507
|
+
freshAt: Date.now(),
|
|
508
|
+
transport: "poll",
|
|
509
|
+
requestedPolicy: {
|
|
510
|
+
refreshIntervalMs: widget.refreshIntervalMs,
|
|
511
|
+
},
|
|
512
|
+
scope: "account",
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return response;
|
|
411
516
|
}));
|
|
412
517
|
const validWidgets = widgetsFull.filter(w => w !== null);
|
|
413
518
|
const processedResponse = await processPluginsForResponse(validWidgets, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
@@ -448,8 +553,19 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
448
553
|
description: widget.description,
|
|
449
554
|
icon: widget.icon,
|
|
450
555
|
size: widget.size || "sm",
|
|
556
|
+
refreshIntervalMs: widget.refreshIntervalMs,
|
|
451
557
|
...widgetData
|
|
452
558
|
};
|
|
559
|
+
if (!("freshness" in response)) {
|
|
560
|
+
response.freshness = {
|
|
561
|
+
freshAt: Date.now(),
|
|
562
|
+
transport: "poll",
|
|
563
|
+
requestedPolicy: {
|
|
564
|
+
refreshIntervalMs: widget.refreshIntervalMs,
|
|
565
|
+
},
|
|
566
|
+
scope: "account",
|
|
567
|
+
};
|
|
568
|
+
}
|
|
453
569
|
const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
454
570
|
return c.json(processedResponse);
|
|
455
571
|
});
|
|
@@ -479,6 +595,12 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
479
595
|
name: datasource.name,
|
|
480
596
|
description: datasource.description,
|
|
481
597
|
type: datasource.type,
|
|
598
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
599
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
600
|
+
transport: datasource.transport,
|
|
601
|
+
priority: datasource.priority,
|
|
602
|
+
scope: datasource.scope,
|
|
603
|
+
dataClass: datasource.dataClass,
|
|
482
604
|
inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(datasource.input),
|
|
483
605
|
};
|
|
484
606
|
}
|
|
@@ -516,12 +638,32 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
516
638
|
plugins: pluginsData,
|
|
517
639
|
oauth2Client
|
|
518
640
|
});
|
|
641
|
+
const requestedPolicy = {
|
|
642
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
643
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
644
|
+
transport: datasource.transport,
|
|
645
|
+
priority: datasource.priority,
|
|
646
|
+
scope: datasource.scope,
|
|
647
|
+
dataClass: datasource.dataClass,
|
|
648
|
+
};
|
|
519
649
|
const response = {
|
|
520
650
|
id: datasource.id,
|
|
521
651
|
name: datasource.name,
|
|
522
652
|
description: datasource.description,
|
|
523
653
|
type: datasource.type,
|
|
654
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
655
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
656
|
+
transport: datasource.transport,
|
|
657
|
+
priority: datasource.priority,
|
|
658
|
+
scope: datasource.scope,
|
|
659
|
+
dataClass: datasource.dataClass,
|
|
524
660
|
data,
|
|
661
|
+
freshness: {
|
|
662
|
+
freshAt: Date.now(),
|
|
663
|
+
transport: datasource.transport || "poll",
|
|
664
|
+
requestedPolicy,
|
|
665
|
+
scope: datasource.scope,
|
|
666
|
+
},
|
|
525
667
|
};
|
|
526
668
|
const processedResponse = await processPluginsForResponse(response, { plugins: pluginsData }, { extraData: { plugins: pluginsData } });
|
|
527
669
|
return c.json(processedResponse);
|
|
@@ -539,6 +681,149 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
539
681
|
throw error;
|
|
540
682
|
}
|
|
541
683
|
});
|
|
684
|
+
// Stream datasource updates over SSE. This is primarily used for "fresh" UIs
|
|
685
|
+
// (positions/orders) where clients want stream-first updates with a poll fallback.
|
|
686
|
+
//
|
|
687
|
+
// Note: This does not require services to implement a separate streaming backend.
|
|
688
|
+
// The server re-runs the datasource handler on an interval and streams results.
|
|
689
|
+
app.post("/datasources/:datasourceId/stream", async (c) => {
|
|
690
|
+
const datasource = datasources.find((ds) => ds.id === c.req.param("datasourceId"));
|
|
691
|
+
if (!datasource) {
|
|
692
|
+
throw new http_exception_1.HTTPException(404, { message: "Datasource not found" });
|
|
693
|
+
}
|
|
694
|
+
const agentInfo = await getAgentInfo(c);
|
|
695
|
+
const rawBody = await safeParseBody(c);
|
|
696
|
+
const hasWrappedParams = rawBody &&
|
|
697
|
+
typeof rawBody === "object" &&
|
|
698
|
+
!Array.isArray(rawBody) &&
|
|
699
|
+
"params" in rawBody;
|
|
700
|
+
const requestedIntervalMsRaw = hasWrappedParams ? rawBody.intervalMs : undefined;
|
|
701
|
+
const requestParamsRaw = hasWrappedParams ? rawBody.params : rawBody;
|
|
702
|
+
let params = await processPluginsForRequest(requestParamsRaw && typeof requestParamsRaw === "object" ? requestParamsRaw : {}, agentInfo);
|
|
703
|
+
const pluginsData = (params.plugins && typeof params.plugins === "object"
|
|
704
|
+
? params.plugins
|
|
705
|
+
: {});
|
|
706
|
+
delete params.plugins;
|
|
707
|
+
let parsedParams;
|
|
708
|
+
try {
|
|
709
|
+
parsedParams = datasource.input.parse(params);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
if (error instanceof zod_1.z.ZodError) {
|
|
713
|
+
const missingParams = error.issues
|
|
714
|
+
.map((issue) => issue.path.join("."))
|
|
715
|
+
.join(", ");
|
|
716
|
+
return c.json({
|
|
717
|
+
error: `Missing or invalid parameters: ${missingParams}`,
|
|
718
|
+
code: "INVALID_PARAMS"
|
|
719
|
+
}, 400);
|
|
720
|
+
}
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
const pluginRequestContext = hasWrappedParams
|
|
724
|
+
? {
|
|
725
|
+
params: parsedParams,
|
|
726
|
+
intervalMs: requestedIntervalMsRaw,
|
|
727
|
+
plugins: pluginsData,
|
|
728
|
+
}
|
|
729
|
+
: {
|
|
730
|
+
...(typeof parsedParams === "object" && parsedParams !== null
|
|
731
|
+
? parsedParams
|
|
732
|
+
: {}),
|
|
733
|
+
plugins: pluginsData,
|
|
734
|
+
};
|
|
735
|
+
const oauth2Client = app.oauth2?.getClient();
|
|
736
|
+
const requestedPolicy = {
|
|
737
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
738
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
739
|
+
transport: datasource.transport,
|
|
740
|
+
priority: datasource.priority,
|
|
741
|
+
scope: datasource.scope,
|
|
742
|
+
dataClass: datasource.dataClass,
|
|
743
|
+
};
|
|
744
|
+
const requestedIntervalMs = typeof requestedIntervalMsRaw === "number" && Number.isFinite(requestedIntervalMsRaw) && requestedIntervalMsRaw > 0
|
|
745
|
+
? requestedIntervalMsRaw
|
|
746
|
+
: null;
|
|
747
|
+
const baseIntervalMs = requestedIntervalMs ??
|
|
748
|
+
(typeof datasource.refreshIntervalMs === "number" && datasource.refreshIntervalMs > 0
|
|
749
|
+
? datasource.refreshIntervalMs
|
|
750
|
+
: 15_000);
|
|
751
|
+
const intervalMs = Math.max(1_000, Math.floor(baseIntervalMs));
|
|
752
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
753
|
+
debugLog(`[SSE] Datasource ${datasource.id} stream start (${intervalMs}ms interval)`);
|
|
754
|
+
return signedStreamSSE(c, privateKey, config, async (stream) => {
|
|
755
|
+
let eventId = 0;
|
|
756
|
+
const emitUpdate = async () => {
|
|
757
|
+
const data = await datasource.getDatasource(agentInfo, parsedParams, {
|
|
758
|
+
plugins: pluginsData,
|
|
759
|
+
oauth2Client,
|
|
760
|
+
});
|
|
761
|
+
const response = {
|
|
762
|
+
id: datasource.id,
|
|
763
|
+
name: datasource.name,
|
|
764
|
+
description: datasource.description,
|
|
765
|
+
type: datasource.type,
|
|
766
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
767
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
768
|
+
transport: datasource.transport,
|
|
769
|
+
priority: datasource.priority,
|
|
770
|
+
scope: datasource.scope,
|
|
771
|
+
dataClass: datasource.dataClass,
|
|
772
|
+
data,
|
|
773
|
+
freshness: {
|
|
774
|
+
freshAt: Date.now(),
|
|
775
|
+
transport: "stream",
|
|
776
|
+
requestedPolicy,
|
|
777
|
+
scope: datasource.scope,
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
const processedResponse = await processPluginsForResponse(response, pluginRequestContext, { extraData: { plugins: pluginsData } });
|
|
781
|
+
await stream.writeSSE({
|
|
782
|
+
event: "datasource-update",
|
|
783
|
+
data: JSON.stringify(processedResponse),
|
|
784
|
+
id: String(eventId++),
|
|
785
|
+
});
|
|
786
|
+
};
|
|
787
|
+
// Send initial snapshot immediately.
|
|
788
|
+
try {
|
|
789
|
+
await emitUpdate();
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
console.error(`[SSE] Datasource ${datasource.id} initial update failed:`, error);
|
|
793
|
+
if (!stream.isAborted()) {
|
|
794
|
+
await stream.writeSSE({
|
|
795
|
+
event: "error",
|
|
796
|
+
data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
// Continue sending updates until client disconnects.
|
|
802
|
+
// Hono exposes the underlying Request via c.req.raw, which includes an AbortSignal.
|
|
803
|
+
const signal = c.req.raw?.signal;
|
|
804
|
+
while (!stream.isAborted() && !signal?.aborted) {
|
|
805
|
+
await sleep(intervalMs);
|
|
806
|
+
if (stream.isAborted() || signal?.aborted)
|
|
807
|
+
break;
|
|
808
|
+
try {
|
|
809
|
+
await emitUpdate();
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
if (stream.isAborted() || signal?.aborted)
|
|
813
|
+
break;
|
|
814
|
+
console.error(`[SSE] Datasource ${datasource.id} update failed:`, error);
|
|
815
|
+
// Keep the stream alive; clients should rely on poll fallback if needed.
|
|
816
|
+
if (!stream.isAborted()) {
|
|
817
|
+
await stream.writeSSE({
|
|
818
|
+
event: "error",
|
|
819
|
+
data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
debugLog(`[SSE] Datasource ${datasource.id} stream end`);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
542
827
|
function mapAgentInfo(agent) {
|
|
543
828
|
return {
|
|
544
829
|
id: agent.id,
|
|
@@ -615,13 +900,23 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
615
900
|
outputSchema,
|
|
616
901
|
interface: tool.interface,
|
|
617
902
|
suggestConfirmation: tool.suggestConfirmation,
|
|
903
|
+
sideEffectClass: tool.sideEffectClass,
|
|
904
|
+
supportsParallel: tool.supportsParallel,
|
|
905
|
+
idempotencyScope: tool.idempotencyScope,
|
|
906
|
+
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
618
907
|
supportsUserActions: tool.supportsUserActions,
|
|
619
908
|
};
|
|
620
909
|
}
|
|
621
910
|
app.get("/getAllToolsAsJsonSchema", (c) => {
|
|
911
|
+
const toolInfo = tools.map(mapToolToJsonSchema);
|
|
912
|
+
const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
|
|
622
913
|
return c.json({
|
|
623
|
-
tools:
|
|
624
|
-
reccomendedPrompts
|
|
914
|
+
tools: toolInfo,
|
|
915
|
+
reccomendedPrompts,
|
|
916
|
+
schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
|
|
917
|
+
tools: toolInfo,
|
|
918
|
+
reccomendedPrompts,
|
|
919
|
+
}),
|
|
625
920
|
});
|
|
626
921
|
});
|
|
627
922
|
app.post("/getAllToolsAsJsonSchema", async (c) => {
|
|
@@ -630,9 +925,14 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
630
925
|
const processedPluginData = await processPluginsForRequest(body, agentInfo);
|
|
631
926
|
const toolInfo = tools.map(mapToolToJsonSchema);
|
|
632
927
|
debugLog(`[getAllToolsAsJsonSchema POST] Returning ${toolInfo.length} tools`);
|
|
928
|
+
const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
|
|
633
929
|
const response = {
|
|
634
930
|
tools: toolInfo,
|
|
635
|
-
reccomendedPrompts
|
|
931
|
+
reccomendedPrompts,
|
|
932
|
+
schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
|
|
933
|
+
tools: toolInfo,
|
|
934
|
+
reccomendedPrompts,
|
|
935
|
+
}),
|
|
636
936
|
};
|
|
637
937
|
const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
638
938
|
return c.json(processedResponse);
|
|
@@ -819,6 +1119,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
819
1119
|
outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
|
|
820
1120
|
interface: tool.interface,
|
|
821
1121
|
suggestConfirmation: tool.suggestConfirmation,
|
|
1122
|
+
sideEffectClass: tool.sideEffectClass,
|
|
1123
|
+
supportsParallel: tool.supportsParallel,
|
|
1124
|
+
idempotencyScope: tool.idempotencyScope,
|
|
1125
|
+
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
822
1126
|
};
|
|
823
1127
|
return c.json(toolDetails);
|
|
824
1128
|
}
|
|
@@ -846,6 +1150,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
846
1150
|
pricing: tool.pricing,
|
|
847
1151
|
inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.input),
|
|
848
1152
|
outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
|
|
1153
|
+
suggestConfirmation: tool.suggestConfirmation,
|
|
1154
|
+
sideEffectClass: tool.sideEffectClass,
|
|
1155
|
+
supportsParallel: tool.supportsParallel,
|
|
1156
|
+
idempotencyScope: tool.idempotencyScope,
|
|
1157
|
+
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
849
1158
|
}
|
|
850
1159
|
: {
|
|
851
1160
|
id: toolId,
|
|
@@ -871,7 +1180,13 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
871
1180
|
}
|
|
872
1181
|
});
|
|
873
1182
|
// Health check endpoint
|
|
874
|
-
app.get("/health", (c) =>
|
|
1183
|
+
app.get("/health", async (c) => {
|
|
1184
|
+
const healthy = await resolveHealthStatus();
|
|
1185
|
+
return c.json({
|
|
1186
|
+
status: healthy ? "healthy" : "unhealthy",
|
|
1187
|
+
timestamp: new Date().toISOString(),
|
|
1188
|
+
}, healthy ? 200 : 503);
|
|
1189
|
+
});
|
|
875
1190
|
// Setup custom routes if provided
|
|
876
1191
|
if (config.routes) {
|
|
877
1192
|
config.routes(app);
|
|
@@ -924,31 +1239,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
924
1239
|
}
|
|
925
1240
|
try {
|
|
926
1241
|
await app.oauth2.handleCallback(code, state);
|
|
927
|
-
return c.html(
|
|
928
|
-
<html>
|
|
929
|
-
<body>
|
|
930
|
-
<script>
|
|
931
|
-
window.opener.postMessage({ type: 'oauth2-success', provider: '${provider}' }, '*');
|
|
932
|
-
window.close();
|
|
933
|
-
</script>
|
|
934
|
-
<h1>Authentication successful! You can close this window.</h1>
|
|
935
|
-
</body>
|
|
936
|
-
</html>
|
|
937
|
-
`);
|
|
1242
|
+
return c.html("<html><body><script>window.opener.postMessage({ type: 'oauth2-success', provider: '" + provider + "' }, '*');window.close();</script><h1>Authentication successful! You can close this window.</h1></body></html>");
|
|
938
1243
|
}
|
|
939
1244
|
catch (error) {
|
|
940
1245
|
console.error("OAuth callback error:", error);
|
|
941
|
-
return c.html(
|
|
942
|
-
<html>
|
|
943
|
-
<body>
|
|
944
|
-
<script>
|
|
945
|
-
window.opener.postMessage({ type: 'oauth2-error', provider: '${provider}', error: '${error.message}' }, '*');
|
|
946
|
-
window.close();
|
|
947
|
-
</script>
|
|
948
|
-
<h1>Authentication failed! You can close this window.</h1>
|
|
949
|
-
</body>
|
|
950
|
-
</html>
|
|
951
|
-
`);
|
|
1246
|
+
return c.html("<html><body><script>window.opener.postMessage({ type: 'oauth2-error', provider: '" + provider + "', error: '" + error.message + "' }, '*');window.close();</script><h1>Authentication failed! You can close this window.</h1></body></html>");
|
|
952
1247
|
}
|
|
953
1248
|
});
|
|
954
1249
|
// Make oauth2Handler available to tools
|
|
@@ -1175,64 +1470,231 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
1175
1470
|
});
|
|
1176
1471
|
// Toolboxes list endpoint
|
|
1177
1472
|
app.get("/toolboxes", (c) => c.json(toolboxes));
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1473
|
+
function normalizeRecommendationText(value) {
|
|
1474
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
1475
|
+
}
|
|
1476
|
+
function tokenizeRecommendationQuery(query) {
|
|
1477
|
+
const normalized = normalizeRecommendationText(query);
|
|
1478
|
+
if (!normalized)
|
|
1479
|
+
return [];
|
|
1480
|
+
return Array.from(new Set(normalized.split(/\s+/).filter((token) => token.length >= 2)));
|
|
1481
|
+
}
|
|
1482
|
+
function buildRecommendationIndex() {
|
|
1483
|
+
const index = new Map();
|
|
1181
1484
|
for (const tool of tools) {
|
|
1182
|
-
if (tool.recommendations?.length)
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
ui: hasDynamicSchema ? undefined : card.ui,
|
|
1189
|
-
inputSchema: hasDynamicSchema ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined,
|
|
1190
|
-
toolId: tool.id,
|
|
1191
|
-
toolName: tool.name,
|
|
1192
|
-
});
|
|
1485
|
+
if (!tool.recommendations?.length)
|
|
1486
|
+
continue;
|
|
1487
|
+
for (const card of tool.recommendations) {
|
|
1488
|
+
const scopedCardId = `${tool.id}:${card.id}`;
|
|
1489
|
+
if (index.has(scopedCardId)) {
|
|
1490
|
+
throw new Error(`Duplicate recommendation card ID "${scopedCardId}". Card IDs must be unique per tool.`);
|
|
1193
1491
|
}
|
|
1492
|
+
const isDynamic = !!card.inputSchema;
|
|
1493
|
+
const inputSchema = isDynamic ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined;
|
|
1494
|
+
const searchText = normalizeRecommendationText([tool.id, tool.name, card.id, ...card.tags].join(" "));
|
|
1495
|
+
index.set(scopedCardId, {
|
|
1496
|
+
cardId: scopedCardId,
|
|
1497
|
+
localCardId: card.id,
|
|
1498
|
+
toolId: tool.id,
|
|
1499
|
+
toolName: tool.name,
|
|
1500
|
+
tags: card.tags,
|
|
1501
|
+
inputSchema,
|
|
1502
|
+
ui: isDynamic ? undefined : card.ui,
|
|
1503
|
+
isDynamic,
|
|
1504
|
+
card,
|
|
1505
|
+
searchText,
|
|
1506
|
+
});
|
|
1194
1507
|
}
|
|
1195
1508
|
}
|
|
1196
|
-
return
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
const
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1509
|
+
return index;
|
|
1510
|
+
}
|
|
1511
|
+
function scoreRecommendationCard(card, tokens) {
|
|
1512
|
+
if (tokens.length === 0)
|
|
1513
|
+
return 0;
|
|
1514
|
+
let score = 0;
|
|
1515
|
+
const tagSet = new Set(card.tags.map((tag) => normalizeRecommendationText(tag)));
|
|
1516
|
+
const scopedId = normalizeRecommendationText(card.cardId);
|
|
1517
|
+
const toolName = normalizeRecommendationText(card.toolName);
|
|
1518
|
+
for (const token of tokens) {
|
|
1519
|
+
if (scopedId === token || card.localCardId.toLowerCase() === token) {
|
|
1520
|
+
score += 6;
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
if (tagSet.has(token)) {
|
|
1524
|
+
score += 4;
|
|
1525
|
+
}
|
|
1526
|
+
if (toolName.includes(token)) {
|
|
1527
|
+
score += 3;
|
|
1528
|
+
}
|
|
1529
|
+
if (card.searchText.includes(token)) {
|
|
1530
|
+
score += 1;
|
|
1212
1531
|
}
|
|
1213
1532
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1533
|
+
return score;
|
|
1534
|
+
}
|
|
1535
|
+
const recommendationCardsById = buildRecommendationIndex();
|
|
1536
|
+
const recommendationSearchSchema = zod_1.z.object({
|
|
1537
|
+
query: zod_1.z.string().optional(),
|
|
1538
|
+
limit: zod_1.z.number().int().min(1).max(50).optional().default(12),
|
|
1539
|
+
toolIds: zod_1.z.array(zod_1.z.string()).optional(),
|
|
1540
|
+
includeStaticUI: zod_1.z.boolean().optional().default(false),
|
|
1541
|
+
});
|
|
1542
|
+
app.post("/recommendations/search", async (c) => {
|
|
1543
|
+
const agentInfo = await getAgentInfo(c);
|
|
1544
|
+
const body = await safeParseBody(c);
|
|
1545
|
+
const processedPluginData = await processPluginsForRequest(body, agentInfo);
|
|
1546
|
+
const requestData = { ...processedPluginData };
|
|
1547
|
+
delete requestData.plugins;
|
|
1548
|
+
const parsed = recommendationSearchSchema.safeParse(requestData);
|
|
1549
|
+
if (!parsed.success) {
|
|
1550
|
+
return c.json({
|
|
1551
|
+
error: "Invalid recommendation search request",
|
|
1552
|
+
details: parsed.error.format(),
|
|
1553
|
+
}, 400);
|
|
1554
|
+
}
|
|
1555
|
+
const { query, limit, toolIds, includeStaticUI, } = parsed.data;
|
|
1556
|
+
const toolIdSet = toolIds?.length ? new Set(toolIds) : null;
|
|
1557
|
+
const filteredCards = Array.from(recommendationCardsById.values()).filter((card) => !toolIdSet || toolIdSet.has(card.toolId));
|
|
1558
|
+
const tokens = tokenizeRecommendationQuery(query ?? "");
|
|
1559
|
+
const ranked = filteredCards
|
|
1560
|
+
.map((card, index) => ({
|
|
1561
|
+
...card,
|
|
1562
|
+
score: scoreRecommendationCard(card, tokens),
|
|
1563
|
+
index,
|
|
1564
|
+
}))
|
|
1565
|
+
.sort((a, b) => {
|
|
1566
|
+
if (tokens.length === 0)
|
|
1567
|
+
return a.index - b.index;
|
|
1568
|
+
if (b.score !== a.score)
|
|
1569
|
+
return b.score - a.score;
|
|
1570
|
+
if (a.toolName !== b.toolName)
|
|
1571
|
+
return a.toolName.localeCompare(b.toolName);
|
|
1572
|
+
return a.localCardId.localeCompare(b.localCardId);
|
|
1573
|
+
})
|
|
1574
|
+
.slice(0, limit);
|
|
1575
|
+
const cards = ranked.map((card) => ({
|
|
1576
|
+
cardId: card.cardId,
|
|
1577
|
+
localCardId: card.localCardId,
|
|
1578
|
+
toolId: card.toolId,
|
|
1579
|
+
toolName: card.toolName,
|
|
1580
|
+
tags: card.tags,
|
|
1581
|
+
score: card.score,
|
|
1582
|
+
inputSchema: card.inputSchema,
|
|
1583
|
+
ui: includeStaticUI && !card.isDynamic ? card.ui : undefined,
|
|
1584
|
+
isDynamic: card.isDynamic,
|
|
1585
|
+
}));
|
|
1586
|
+
const response = {
|
|
1587
|
+
cards,
|
|
1588
|
+
count: cards.length,
|
|
1589
|
+
query,
|
|
1590
|
+
};
|
|
1591
|
+
const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
1592
|
+
return c.json(processedResponse);
|
|
1593
|
+
});
|
|
1594
|
+
const recommendationRenderSchema = zod_1.z.object({
|
|
1595
|
+
cards: zod_1.z
|
|
1596
|
+
.array(zod_1.z.object({
|
|
1597
|
+
cardId: zod_1.z.string().min(1),
|
|
1598
|
+
params: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
1599
|
+
context: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
1600
|
+
}))
|
|
1601
|
+
.min(1)
|
|
1602
|
+
.max(8),
|
|
1603
|
+
continueOnError: zod_1.z.boolean().optional().default(true),
|
|
1604
|
+
});
|
|
1605
|
+
app.post("/recommendations/render", async (c) => {
|
|
1606
|
+
const agentInfo = await getAgentInfo(c);
|
|
1607
|
+
const body = await safeParseBody(c);
|
|
1608
|
+
const processedPluginData = await processPluginsForRequest(body, agentInfo);
|
|
1609
|
+
const requestData = { ...processedPluginData };
|
|
1610
|
+
delete requestData.plugins;
|
|
1611
|
+
const parsed = recommendationRenderSchema.safeParse(requestData);
|
|
1612
|
+
if (!parsed.success) {
|
|
1613
|
+
return c.json({
|
|
1614
|
+
error: "Invalid recommendation render request",
|
|
1615
|
+
details: parsed.error.format(),
|
|
1616
|
+
}, 400);
|
|
1617
|
+
}
|
|
1618
|
+
const { cards: requestedCards, continueOnError } = parsed.data;
|
|
1619
|
+
const renderedCards = [];
|
|
1620
|
+
const renderErrors = [];
|
|
1621
|
+
for (const requested of requestedCards) {
|
|
1622
|
+
const card = recommendationCardsById.get(requested.cardId);
|
|
1623
|
+
if (!card) {
|
|
1624
|
+
renderErrors.push({
|
|
1625
|
+
cardId: requested.cardId,
|
|
1626
|
+
code: "not_found",
|
|
1627
|
+
message: `Card not found: ${requested.cardId}`,
|
|
1628
|
+
});
|
|
1629
|
+
if (!continueOnError)
|
|
1630
|
+
break;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
if (!card.isDynamic) {
|
|
1634
|
+
renderedCards.push({
|
|
1635
|
+
cardId: card.cardId,
|
|
1636
|
+
localCardId: card.localCardId,
|
|
1637
|
+
toolId: card.toolId,
|
|
1638
|
+
toolName: card.toolName,
|
|
1639
|
+
tags: card.tags,
|
|
1640
|
+
ui: card.card.ui,
|
|
1641
|
+
});
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
const paramsInput = requested.params ?? {};
|
|
1645
|
+
const parseResult = card.card.inputSchema.safeParse(paramsInput);
|
|
1646
|
+
if (!parseResult.success) {
|
|
1647
|
+
renderErrors.push({
|
|
1648
|
+
cardId: card.cardId,
|
|
1649
|
+
code: "invalid_params",
|
|
1650
|
+
message: "Invalid parameters",
|
|
1651
|
+
details: parseResult.error.format(),
|
|
1652
|
+
});
|
|
1653
|
+
if (!continueOnError)
|
|
1654
|
+
break;
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
if (typeof card.card.ui !== "function") {
|
|
1658
|
+
renderErrors.push({
|
|
1659
|
+
cardId: card.cardId,
|
|
1660
|
+
code: "render_failed",
|
|
1661
|
+
message: `Card "${card.cardId}" is dynamic but has no UI generator function`,
|
|
1662
|
+
});
|
|
1663
|
+
if (!continueOnError)
|
|
1664
|
+
break;
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
try {
|
|
1668
|
+
const ui = await card.card.ui(parseResult.data, requested.context ?? {});
|
|
1669
|
+
renderedCards.push({
|
|
1670
|
+
cardId: card.cardId,
|
|
1671
|
+
localCardId: card.localCardId,
|
|
1672
|
+
toolId: card.toolId,
|
|
1673
|
+
toolName: card.toolName,
|
|
1674
|
+
tags: card.tags,
|
|
1675
|
+
ui,
|
|
1676
|
+
params: parseResult.data,
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
catch (error) {
|
|
1680
|
+
renderErrors.push({
|
|
1681
|
+
cardId: card.cardId,
|
|
1682
|
+
code: "render_failed",
|
|
1683
|
+
message: error instanceof Error ? error.message : "Failed to render recommendation card",
|
|
1684
|
+
});
|
|
1685
|
+
if (!continueOnError)
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1235
1688
|
}
|
|
1689
|
+
const response = {
|
|
1690
|
+
cards: renderedCards,
|
|
1691
|
+
errors: renderErrors,
|
|
1692
|
+
renderedCount: renderedCards.length,
|
|
1693
|
+
requestedCount: requestedCards.length,
|
|
1694
|
+
};
|
|
1695
|
+
const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
1696
|
+
const statusCode = !continueOnError && renderErrors.length > 0 ? 400 : 200;
|
|
1697
|
+
return c.json(processedResponse, statusCode);
|
|
1236
1698
|
});
|
|
1237
1699
|
// Process request with plugins
|
|
1238
1700
|
async function processPluginsForRequest(request, agentInfo) {
|