@wingman-ai/gateway 0.2.5 → 0.3.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.
- package/.wingman/agents/coding/agent.md +5 -0
- package/.wingman/agents/coding-v2/agent.md +58 -0
- package/.wingman/agents/game-dev/agent.md +94 -0
- package/.wingman/agents/game-dev/art-generation.md +37 -0
- package/.wingman/agents/game-dev/asset-refinement.md +17 -0
- package/.wingman/agents/game-dev/planning-idea.md +17 -0
- package/.wingman/agents/game-dev/ui-specialist.md +17 -0
- package/.wingman/agents/main/agent.md +2 -0
- package/README.md +1 -0
- package/dist/agent/config/agentConfig.d.ts +4 -0
- package/dist/agent/config/mcpClientManager.cjs +44 -10
- package/dist/agent/config/mcpClientManager.d.ts +6 -2
- package/dist/agent/config/mcpClientManager.js +44 -10
- package/dist/agent/config/toolRegistry.cjs +3 -1
- package/dist/agent/config/toolRegistry.js +3 -1
- package/dist/agent/tests/mcpClientManager.test.cjs +124 -0
- package/dist/agent/tests/mcpClientManager.test.d.ts +1 -0
- package/dist/agent/tests/mcpClientManager.test.js +118 -0
- package/dist/agent/tools/command_execute.cjs +1 -1
- package/dist/agent/tools/command_execute.js +1 -1
- package/dist/cli/config/schema.d.ts +2 -0
- package/dist/cli/core/agentInvoker.cjs +55 -66
- package/dist/cli/core/agentInvoker.d.ts +10 -13
- package/dist/cli/core/agentInvoker.js +42 -62
- package/dist/cli/core/imagePersistence.cjs +125 -0
- package/dist/cli/core/imagePersistence.d.ts +24 -0
- package/dist/cli/core/imagePersistence.js +85 -0
- package/dist/cli/core/sessionManager.cjs +297 -40
- package/dist/cli/core/sessionManager.d.ts +9 -0
- package/dist/cli/core/sessionManager.js +297 -40
- package/dist/debug/terminalProbe.cjs +57 -0
- package/dist/debug/terminalProbe.d.ts +10 -0
- package/dist/debug/terminalProbe.js +20 -0
- package/dist/debug/terminalProbeAuth.cjs +140 -0
- package/dist/debug/terminalProbeAuth.d.ts +20 -0
- package/dist/debug/terminalProbeAuth.js +97 -0
- package/dist/gateway/http/fs.cjs +19 -0
- package/dist/gateway/http/fs.js +19 -0
- package/dist/gateway/http/sessions.cjs +25 -5
- package/dist/gateway/http/sessions.js +25 -5
- package/dist/gateway/server.cjs +112 -11
- package/dist/gateway/server.d.ts +2 -0
- package/dist/gateway/server.js +112 -11
- package/dist/tests/agentInvokerSummarization.test.cjs +56 -37
- package/dist/tests/agentInvokerSummarization.test.js +58 -39
- package/dist/tests/agentInvokerWorkdir.test.cjs +50 -0
- package/dist/tests/agentInvokerWorkdir.test.js +52 -2
- package/dist/tests/cli-init.test.cjs +36 -0
- package/dist/tests/cli-init.test.js +36 -0
- package/dist/tests/falRuntime.test.cjs +78 -0
- package/dist/tests/falRuntime.test.d.ts +1 -0
- package/dist/tests/falRuntime.test.js +72 -0
- package/dist/tests/falSummary.test.cjs +51 -0
- package/dist/tests/falSummary.test.d.ts +1 -0
- package/dist/tests/falSummary.test.js +45 -0
- package/dist/tests/gateway.test.cjs +109 -1
- package/dist/tests/gateway.test.js +109 -1
- package/dist/tests/imagePersistence.test.cjs +143 -0
- package/dist/tests/imagePersistence.test.d.ts +1 -0
- package/dist/tests/imagePersistence.test.js +137 -0
- package/dist/tests/sessionMessageAttachments.test.cjs +30 -0
- package/dist/tests/sessionMessageAttachments.test.js +30 -0
- package/dist/tests/sessionStateMessages.test.cjs +126 -0
- package/dist/tests/sessionStateMessages.test.js +126 -0
- package/dist/tests/sessions-api.test.cjs +117 -3
- package/dist/tests/sessions-api.test.js +118 -4
- package/dist/tests/terminalProbe.test.cjs +45 -0
- package/dist/tests/terminalProbe.test.d.ts +1 -0
- package/dist/tests/terminalProbe.test.js +39 -0
- package/dist/tests/terminalProbeAuth.test.cjs +85 -0
- package/dist/tests/terminalProbeAuth.test.d.ts +1 -0
- package/dist/tests/terminalProbeAuth.test.js +79 -0
- package/dist/tools/fal/runtime.cjs +103 -0
- package/dist/tools/fal/runtime.d.ts +10 -0
- package/dist/tools/fal/runtime.js +60 -0
- package/dist/tools/fal/summary.cjs +78 -0
- package/dist/tools/fal/summary.d.ts +22 -0
- package/dist/tools/fal/summary.js +41 -0
- package/dist/tools/mcp-fal-ai.cjs +1041 -0
- package/dist/tools/mcp-fal-ai.d.ts +1 -0
- package/dist/tools/mcp-fal-ai.js +1025 -0
- package/dist/types/mcp.cjs +2 -0
- package/dist/types/mcp.d.ts +8 -0
- package/dist/types/mcp.js +3 -1
- package/dist/webui/assets/index-0nUBsUUq.js +278 -0
- package/dist/webui/assets/index-kk7OrD-G.css +11 -0
- package/dist/webui/index.html +2 -2
- package/package.json +11 -8
- package/dist/webui/assets/index-C7EuTbnE.js +0 -270
- package/dist/webui/assets/index-DVWQluit.css +0 -11
|
@@ -24,6 +24,9 @@ function _define_property(obj, key, value) {
|
|
|
24
24
|
}
|
|
25
25
|
const WORKDIR_VIRTUAL_PATH = "/workdir/";
|
|
26
26
|
const OUTPUT_VIRTUAL_PATH = "/output/";
|
|
27
|
+
const AGENTS_MEMORY_VIRTUAL_PATHS = [
|
|
28
|
+
"/AGENTS.md"
|
|
29
|
+
];
|
|
27
30
|
const DEFAULT_DEEPAGENT_MODEL = "claude-sonnet-4-5-20250929";
|
|
28
31
|
const isPathWithinRoot = (targetPath, rootPath)=>{
|
|
29
32
|
const normalizedTarget = normalize(targetPath);
|
|
@@ -35,6 +38,11 @@ const resolveExecutionWorkspace = (workspace, workdir)=>{
|
|
|
35
38
|
if (isAbsolute(workdir)) return normalize(workdir);
|
|
36
39
|
return normalize(join(workspace, workdir));
|
|
37
40
|
};
|
|
41
|
+
const resolveAgentExecutionWorkspace = (workspace, workdir, defaultOutputDir)=>{
|
|
42
|
+
const preferredWorkdir = workdir || defaultOutputDir || null;
|
|
43
|
+
return resolveExecutionWorkspace(workspace, preferredWorkdir);
|
|
44
|
+
};
|
|
45
|
+
const resolveAgentMemorySources = (executionWorkspace)=>AGENTS_MEMORY_VIRTUAL_PATHS.filter((memoryPath)=>existsSync(join(executionWorkspace, memoryPath.replace(/^\/+/, ""))));
|
|
38
46
|
const toWorkspaceAliasVirtualPath = (absolutePath)=>{
|
|
39
47
|
const normalized = normalize(absolutePath);
|
|
40
48
|
if (!isAbsolute(normalized)) return null;
|
|
@@ -238,24 +246,19 @@ const trackRootLangGraphRunId = (currentRootLangGraphRunId, chunk)=>{
|
|
|
238
246
|
return extractEventRunId(eventRecord) || currentRootLangGraphRunId;
|
|
239
247
|
};
|
|
240
248
|
const isRootLangGraphTerminalEvent = (chunk, rootLangGraphRunId)=>{
|
|
241
|
-
if (!rootLangGraphRunId) return false;
|
|
242
249
|
const eventRecord = extractStreamEventRecord(chunk);
|
|
243
250
|
if (!eventRecord || !isRootLangGraphChainEvent(eventRecord, "on_chain_end")) return false;
|
|
251
|
+
if (!rootLangGraphRunId) return true;
|
|
244
252
|
const chunkRunId = extractEventRunId(eventRecord);
|
|
253
|
+
if (!chunkRunId) return true;
|
|
245
254
|
return Boolean(chunkRunId && chunkRunId === rootLangGraphRunId);
|
|
246
255
|
};
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
message
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
return {
|
|
257
|
-
status: "ok"
|
|
258
|
-
};
|
|
256
|
+
const emitCompletionAndContinuePostProcessing = (input)=>{
|
|
257
|
+
input.outputManager.emitAgentComplete(input.result);
|
|
258
|
+
if (!input.postProcess) return;
|
|
259
|
+
input.postProcess().catch((error)=>{
|
|
260
|
+
input.logger?.debug("Failed post-completion processing for streamed agent response", error);
|
|
261
|
+
});
|
|
259
262
|
};
|
|
260
263
|
class AgentInvoker {
|
|
261
264
|
findAllAgents() {
|
|
@@ -269,14 +272,11 @@ class AgentInvoker {
|
|
|
269
272
|
let cancellationHandled = false;
|
|
270
273
|
let activeToolName = null;
|
|
271
274
|
let lastToolName = null;
|
|
272
|
-
let sawAssistantText = false;
|
|
273
|
-
let streamErrorMessage;
|
|
274
275
|
let rootLangGraphRunId;
|
|
275
|
-
let preInvocationMessages = null;
|
|
276
276
|
const isCancelled = ()=>options?.signal?.aborted === true;
|
|
277
277
|
try {
|
|
278
278
|
const hookSessionId = sessionId || v4();
|
|
279
|
-
const executionWorkspace =
|
|
279
|
+
const executionWorkspace = resolveAgentExecutionWorkspace(this.workspace, this.workdir, this.defaultOutputDir);
|
|
280
280
|
const effectiveWorkdir = this.workdir ? executionWorkspace : null;
|
|
281
281
|
const loader = new AgentLoader(this.configDir, this.workspace, this.wingmanConfig, executionWorkspace, {
|
|
282
282
|
terminalOwnerId: `${agentName}:${hookSessionId}`,
|
|
@@ -295,7 +295,9 @@ class AgentInvoker {
|
|
|
295
295
|
if (targetAgent.mcpUseGlobal && this.wingmanConfig.mcp) mcpConfigs.push(this.wingmanConfig.mcp);
|
|
296
296
|
if (mcpConfigs.length > 0) {
|
|
297
297
|
this.logger.debug("Initializing MCP client for agent invocation");
|
|
298
|
-
this.mcpManager = new MCPClientManager(mcpConfigs, this.logger
|
|
298
|
+
this.mcpManager = new MCPClientManager(mcpConfigs, this.logger, {
|
|
299
|
+
executionWorkspace
|
|
300
|
+
});
|
|
299
301
|
await this.mcpManager.initialize();
|
|
300
302
|
const mcpTools = await this.mcpManager.getTools();
|
|
301
303
|
if (mcpTools.length > 0) {
|
|
@@ -312,6 +314,7 @@ class AgentInvoker {
|
|
|
312
314
|
const normalizedSkillsDirectory = skillsDirectory.replace(/^\/+|\/+$/g, "");
|
|
313
315
|
const skillsVirtualPath = `/${normalizedSkillsDirectory}/`;
|
|
314
316
|
const outputMount = resolveExternalOutputMount(executionWorkspace, effectiveWorkdir, this.defaultOutputDir);
|
|
317
|
+
const memorySources = resolveAgentMemorySources(executionWorkspace);
|
|
315
318
|
const middleware = [
|
|
316
319
|
mediaCompatibilityMiddleware({
|
|
317
320
|
model: targetAgent.model
|
|
@@ -396,6 +399,7 @@ class AgentInvoker {
|
|
|
396
399
|
middleware: middleware,
|
|
397
400
|
interruptOn: hitlSettings?.interruptOn,
|
|
398
401
|
skills: skillsSources,
|
|
402
|
+
memory: memorySources,
|
|
399
403
|
subagents: targetAgent.subagents || [],
|
|
400
404
|
checkpointer: checkpointer
|
|
401
405
|
});
|
|
@@ -404,15 +408,6 @@ class AgentInvoker {
|
|
|
404
408
|
const userContent = buildUserContent(prompt, attachments, targetAgent.model);
|
|
405
409
|
if (this.sessionManager && sessionId) {
|
|
406
410
|
this.logger.debug(`Using streaming with session: ${sessionId}`);
|
|
407
|
-
try {
|
|
408
|
-
const messages = await this.sessionManager.listMessages(sessionId);
|
|
409
|
-
preInvocationMessages = messages.map((message)=>({
|
|
410
|
-
role: message.role,
|
|
411
|
-
content: message.content
|
|
412
|
-
}));
|
|
413
|
-
} catch (stateError) {
|
|
414
|
-
this.logger.debug("Failed to capture pre-invocation session state", stateError);
|
|
415
|
-
}
|
|
416
411
|
const stream = await standaloneAgent.streamEvents({
|
|
417
412
|
messages: [
|
|
418
413
|
{
|
|
@@ -430,8 +425,6 @@ class AgentInvoker {
|
|
|
430
425
|
});
|
|
431
426
|
for await (const chunk of stream){
|
|
432
427
|
rootLangGraphRunId = trackRootLangGraphRunId(rootLangGraphRunId, chunk);
|
|
433
|
-
if (!sawAssistantText && chunkHasAssistantText(chunk)) sawAssistantText = true;
|
|
434
|
-
if (!streamErrorMessage) streamErrorMessage = detectStreamErrorMessage(chunk);
|
|
435
428
|
const toolEvent = detectToolEventContext(chunk);
|
|
436
429
|
if (toolEvent) {
|
|
437
430
|
lastToolName = toolEvent.toolName;
|
|
@@ -461,37 +454,15 @@ class AgentInvoker {
|
|
|
461
454
|
cancelled: true
|
|
462
455
|
};
|
|
463
456
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
this.logger
|
|
473
|
-
}
|
|
474
|
-
const completionOutcome = evaluateStreamingCompletion({
|
|
475
|
-
sawAssistantText,
|
|
476
|
-
fallbackText,
|
|
477
|
-
streamErrorMessage
|
|
478
|
-
});
|
|
479
|
-
if ("blocked" === completionOutcome.status) {
|
|
480
|
-
this.logger.warn(completionOutcome.message);
|
|
481
|
-
this.outputManager.emitAgentError(completionOutcome.message);
|
|
482
|
-
return {
|
|
483
|
-
blocked: true,
|
|
484
|
-
reason: completionOutcome.reason
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
this.logger.info("Agent streaming completed successfully", {
|
|
488
|
-
usedFallbackText: Boolean(fallbackText)
|
|
489
|
-
});
|
|
490
|
-
this.outputManager.emitAgentComplete({
|
|
491
|
-
streaming: true,
|
|
492
|
-
...fallbackText ? {
|
|
493
|
-
fallbackText
|
|
494
|
-
} : {}
|
|
457
|
+
this.logger.info("Agent streaming completed successfully");
|
|
458
|
+
const completionPayload = {
|
|
459
|
+
streaming: true
|
|
460
|
+
};
|
|
461
|
+
emitCompletionAndContinuePostProcessing({
|
|
462
|
+
outputManager: this.outputManager,
|
|
463
|
+
result: completionPayload,
|
|
464
|
+
postProcess: ()=>this.materializeSessionImages(sessionId),
|
|
465
|
+
logger: this.logger
|
|
495
466
|
});
|
|
496
467
|
return {
|
|
497
468
|
streaming: true
|
|
@@ -527,7 +498,12 @@ class AgentInvoker {
|
|
|
527
498
|
};
|
|
528
499
|
}
|
|
529
500
|
this.logger.info("Agent completed successfully");
|
|
530
|
-
|
|
501
|
+
emitCompletionAndContinuePostProcessing({
|
|
502
|
+
outputManager: this.outputManager,
|
|
503
|
+
result,
|
|
504
|
+
postProcess: ()=>this.materializeSessionImages(sessionId),
|
|
505
|
+
logger: this.logger
|
|
506
|
+
});
|
|
531
507
|
return result;
|
|
532
508
|
}
|
|
533
509
|
} catch (error) {
|
|
@@ -559,6 +535,10 @@ class AgentInvoker {
|
|
|
559
535
|
description: a.description
|
|
560
536
|
}));
|
|
561
537
|
}
|
|
538
|
+
async materializeSessionImages(sessionId) {
|
|
539
|
+
if (!this.sessionManager || !sessionId) return;
|
|
540
|
+
await this.sessionManager.listMessages(sessionId);
|
|
541
|
+
}
|
|
562
542
|
constructor(options){
|
|
563
543
|
_define_property(this, "loader", void 0);
|
|
564
544
|
_define_property(this, "outputManager", void 0);
|
|
@@ -769,4 +749,4 @@ function buildAttachmentPreview(attachments) {
|
|
|
769
749
|
if (hasImage) return "[image]";
|
|
770
750
|
return "";
|
|
771
751
|
}
|
|
772
|
-
export { AgentInvoker, OUTPUT_VIRTUAL_PATH, WORKDIR_VIRTUAL_PATH, buildUserContent, chunkHasAssistantText, configureDeepAgentSummarizationMiddleware, detectStreamErrorMessage, detectToolEventContext,
|
|
752
|
+
export { AGENTS_MEMORY_VIRTUAL_PATHS, AgentInvoker, OUTPUT_VIRTUAL_PATH, WORKDIR_VIRTUAL_PATH, buildUserContent, chunkHasAssistantText, configureDeepAgentSummarizationMiddleware, detectStreamErrorMessage, detectToolEventContext, emitCompletionAndContinuePostProcessing, isRootLangGraphTerminalEvent, resolveAgentExecutionWorkspace, resolveAgentMemorySources, resolveExecutionWorkspace, resolveExternalOutputMount, resolveHumanInTheLoopSettings, resolveModelRetryMiddlewareSettings, resolveSummarizationMiddlewareSettings, resolveToolRetryMiddlewareSettings, selectStreamingFallbackText, toWorkspaceAliasVirtualPath, trackRootLangGraphRunId };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
5
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: definition[key]
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
})();
|
|
11
|
+
(()=>{
|
|
12
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
13
|
+
})();
|
|
14
|
+
(()=>{
|
|
15
|
+
__webpack_require__.r = (exports1)=>{
|
|
16
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
17
|
+
value: 'Module'
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
20
|
+
value: true
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
})();
|
|
24
|
+
var __webpack_exports__ = {};
|
|
25
|
+
__webpack_require__.r(__webpack_exports__);
|
|
26
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
27
|
+
persistAssistantImagesToDisk: ()=>persistAssistantImagesToDisk,
|
|
28
|
+
parseBase64DataUrl: ()=>parseBase64DataUrl,
|
|
29
|
+
resolveImageExtension: ()=>resolveImageExtension
|
|
30
|
+
});
|
|
31
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
32
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
33
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
34
|
+
const DATA_URL_BASE64_PATTERN = /^data:([^;,]+);base64,(.+)$/i;
|
|
35
|
+
function persistAssistantImagesToDisk(input) {
|
|
36
|
+
if (!input.messages.length) return;
|
|
37
|
+
const mediaRoot = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(input.dbPath), "media", sanitizePathSegment(input.sessionId));
|
|
38
|
+
for (const message of input.messages)if ("assistant" === message.role) {
|
|
39
|
+
if (Array.isArray(message.attachments) && 0 !== message.attachments.length) for (const attachment of message.attachments){
|
|
40
|
+
if (!attachment || "image" !== attachment.kind) continue;
|
|
41
|
+
if (attachment.path) continue;
|
|
42
|
+
const parsed = parseBase64DataUrl(attachment.dataUrl);
|
|
43
|
+
if (!parsed) continue;
|
|
44
|
+
if (!parsed.mimeType.toLowerCase().startsWith("image/")) continue;
|
|
45
|
+
let bytes;
|
|
46
|
+
try {
|
|
47
|
+
bytes = Buffer.from(parsed.data, "base64");
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (0 === bytes.length) continue;
|
|
52
|
+
const extension = resolveImageExtension(parsed.mimeType);
|
|
53
|
+
const hash = (0, external_node_crypto_namespaceObject.createHash)("sha256").update(bytes).digest("hex").slice(0, 20);
|
|
54
|
+
const filename = `${hash}.${extension}`;
|
|
55
|
+
const outputPath = (0, external_node_path_namespaceObject.join)(mediaRoot, filename);
|
|
56
|
+
if (!(0, external_node_fs_namespaceObject.existsSync)(outputPath)) {
|
|
57
|
+
(0, external_node_fs_namespaceObject.mkdirSync)(mediaRoot, {
|
|
58
|
+
recursive: true
|
|
59
|
+
});
|
|
60
|
+
(0, external_node_fs_namespaceObject.writeFileSync)(outputPath, bytes);
|
|
61
|
+
}
|
|
62
|
+
attachment.path = outputPath;
|
|
63
|
+
if (!attachment.mimeType) attachment.mimeType = parsed.mimeType;
|
|
64
|
+
if ("number" != typeof attachment.size || attachment.size <= 0) attachment.size = bytes.length;
|
|
65
|
+
if (!attachment.name) attachment.name = `image-${hash.slice(0, 8)}.${extension}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function parseBase64DataUrl(dataUrl) {
|
|
70
|
+
if ("string" != typeof dataUrl) return null;
|
|
71
|
+
const match = dataUrl.match(DATA_URL_BASE64_PATTERN);
|
|
72
|
+
if (!match) return null;
|
|
73
|
+
return {
|
|
74
|
+
mimeType: match[1].trim().toLowerCase(),
|
|
75
|
+
data: match[2].trim()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function resolveImageExtension(mimeType) {
|
|
79
|
+
const normalized = (mimeType || "").trim().toLowerCase();
|
|
80
|
+
switch(normalized){
|
|
81
|
+
case "image/jpeg":
|
|
82
|
+
return "jpg";
|
|
83
|
+
case "image/png":
|
|
84
|
+
return "png";
|
|
85
|
+
case "image/webp":
|
|
86
|
+
return "webp";
|
|
87
|
+
case "image/gif":
|
|
88
|
+
return "gif";
|
|
89
|
+
case "image/svg+xml":
|
|
90
|
+
return "svg";
|
|
91
|
+
case "image/bmp":
|
|
92
|
+
return "bmp";
|
|
93
|
+
case "image/tiff":
|
|
94
|
+
return "tiff";
|
|
95
|
+
case "image/heic":
|
|
96
|
+
return "heic";
|
|
97
|
+
case "image/heif":
|
|
98
|
+
return "heif";
|
|
99
|
+
case "image/avif":
|
|
100
|
+
return "avif";
|
|
101
|
+
default:
|
|
102
|
+
{
|
|
103
|
+
const subtype = normalized.split("/")[1] || "";
|
|
104
|
+
const sanitized = subtype.replace(/[^a-z0-9]/g, "");
|
|
105
|
+
return sanitized || "img";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function sanitizePathSegment(value) {
|
|
110
|
+
const normalized = (value || "").trim();
|
|
111
|
+
if (!normalized) return "default-session";
|
|
112
|
+
const sanitized = normalized.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
113
|
+
return sanitized.slice(0, 120) || "default-session";
|
|
114
|
+
}
|
|
115
|
+
exports.parseBase64DataUrl = __webpack_exports__.parseBase64DataUrl;
|
|
116
|
+
exports.persistAssistantImagesToDisk = __webpack_exports__.persistAssistantImagesToDisk;
|
|
117
|
+
exports.resolveImageExtension = __webpack_exports__.resolveImageExtension;
|
|
118
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
119
|
+
"parseBase64DataUrl",
|
|
120
|
+
"persistAssistantImagesToDisk",
|
|
121
|
+
"resolveImageExtension"
|
|
122
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
123
|
+
Object.defineProperty(exports, '__esModule', {
|
|
124
|
+
value: true
|
|
125
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface PersistableAttachment {
|
|
2
|
+
kind: "image" | "audio" | "file";
|
|
3
|
+
dataUrl: string;
|
|
4
|
+
mimeType?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PersistableMessage {
|
|
10
|
+
role: "user" | "assistant";
|
|
11
|
+
attachments?: PersistableAttachment[];
|
|
12
|
+
}
|
|
13
|
+
type ParsedDataUrl = {
|
|
14
|
+
mimeType: string;
|
|
15
|
+
data: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function persistAssistantImagesToDisk(input: {
|
|
18
|
+
dbPath: string;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
messages: PersistableMessage[];
|
|
21
|
+
}): void;
|
|
22
|
+
export declare function parseBase64DataUrl(dataUrl: string): ParsedDataUrl | null;
|
|
23
|
+
export declare function resolveImageExtension(mimeType: string): string;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const DATA_URL_BASE64_PATTERN = /^data:([^;,]+);base64,(.+)$/i;
|
|
5
|
+
function persistAssistantImagesToDisk(input) {
|
|
6
|
+
if (!input.messages.length) return;
|
|
7
|
+
const mediaRoot = join(dirname(input.dbPath), "media", sanitizePathSegment(input.sessionId));
|
|
8
|
+
for (const message of input.messages)if ("assistant" === message.role) {
|
|
9
|
+
if (Array.isArray(message.attachments) && 0 !== message.attachments.length) for (const attachment of message.attachments){
|
|
10
|
+
if (!attachment || "image" !== attachment.kind) continue;
|
|
11
|
+
if (attachment.path) continue;
|
|
12
|
+
const parsed = parseBase64DataUrl(attachment.dataUrl);
|
|
13
|
+
if (!parsed) continue;
|
|
14
|
+
if (!parsed.mimeType.toLowerCase().startsWith("image/")) continue;
|
|
15
|
+
let bytes;
|
|
16
|
+
try {
|
|
17
|
+
bytes = Buffer.from(parsed.data, "base64");
|
|
18
|
+
} catch {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (0 === bytes.length) continue;
|
|
22
|
+
const extension = resolveImageExtension(parsed.mimeType);
|
|
23
|
+
const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 20);
|
|
24
|
+
const filename = `${hash}.${extension}`;
|
|
25
|
+
const outputPath = join(mediaRoot, filename);
|
|
26
|
+
if (!existsSync(outputPath)) {
|
|
27
|
+
mkdirSync(mediaRoot, {
|
|
28
|
+
recursive: true
|
|
29
|
+
});
|
|
30
|
+
writeFileSync(outputPath, bytes);
|
|
31
|
+
}
|
|
32
|
+
attachment.path = outputPath;
|
|
33
|
+
if (!attachment.mimeType) attachment.mimeType = parsed.mimeType;
|
|
34
|
+
if ("number" != typeof attachment.size || attachment.size <= 0) attachment.size = bytes.length;
|
|
35
|
+
if (!attachment.name) attachment.name = `image-${hash.slice(0, 8)}.${extension}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function parseBase64DataUrl(dataUrl) {
|
|
40
|
+
if ("string" != typeof dataUrl) return null;
|
|
41
|
+
const match = dataUrl.match(DATA_URL_BASE64_PATTERN);
|
|
42
|
+
if (!match) return null;
|
|
43
|
+
return {
|
|
44
|
+
mimeType: match[1].trim().toLowerCase(),
|
|
45
|
+
data: match[2].trim()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function resolveImageExtension(mimeType) {
|
|
49
|
+
const normalized = (mimeType || "").trim().toLowerCase();
|
|
50
|
+
switch(normalized){
|
|
51
|
+
case "image/jpeg":
|
|
52
|
+
return "jpg";
|
|
53
|
+
case "image/png":
|
|
54
|
+
return "png";
|
|
55
|
+
case "image/webp":
|
|
56
|
+
return "webp";
|
|
57
|
+
case "image/gif":
|
|
58
|
+
return "gif";
|
|
59
|
+
case "image/svg+xml":
|
|
60
|
+
return "svg";
|
|
61
|
+
case "image/bmp":
|
|
62
|
+
return "bmp";
|
|
63
|
+
case "image/tiff":
|
|
64
|
+
return "tiff";
|
|
65
|
+
case "image/heic":
|
|
66
|
+
return "heic";
|
|
67
|
+
case "image/heif":
|
|
68
|
+
return "heif";
|
|
69
|
+
case "image/avif":
|
|
70
|
+
return "avif";
|
|
71
|
+
default:
|
|
72
|
+
{
|
|
73
|
+
const subtype = normalized.split("/")[1] || "";
|
|
74
|
+
const sanitized = subtype.replace(/[^a-z0-9]/g, "");
|
|
75
|
+
return sanitized || "img";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function sanitizePathSegment(value) {
|
|
80
|
+
const normalized = (value || "").trim();
|
|
81
|
+
if (!normalized) return "default-session";
|
|
82
|
+
const sanitized = normalized.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
83
|
+
return sanitized.slice(0, 120) || "default-session";
|
|
84
|
+
}
|
|
85
|
+
export { parseBase64DataUrl, persistAssistantImagesToDisk, resolveImageExtension };
|