@wingman-ai/gateway 0.1.5 → 0.2.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/README.md +7 -0
- package/.wingman/agents/coding/agent.md +1 -0
- package/.wingman/agents/main/agent.md +1 -0
- package/.wingman/agents/researcher/agent.md +1 -0
- package/.wingman/agents/stock-trader/agent.md +1 -0
- package/dist/agent/config/agentConfig.cjs +12 -1
- package/dist/agent/config/agentConfig.d.ts +14 -0
- package/dist/agent/config/agentConfig.js +12 -1
- package/dist/agent/config/agentLoader.cjs +37 -1
- package/dist/agent/config/agentLoader.d.ts +2 -1
- package/dist/agent/config/agentLoader.js +37 -1
- package/dist/agent/config/modelFactory.cjs +2 -1
- package/dist/agent/config/modelFactory.js +2 -1
- package/dist/agent/config/toolRegistry.cjs +6 -4
- package/dist/agent/config/toolRegistry.d.ts +1 -0
- package/dist/agent/config/toolRegistry.js +6 -4
- package/dist/agent/middleware/additional-messages.cjs +8 -1
- package/dist/agent/middleware/additional-messages.d.ts +1 -0
- package/dist/agent/middleware/additional-messages.js +8 -1
- package/dist/agent/tests/agentConfig.test.cjs +25 -0
- package/dist/agent/tests/agentConfig.test.js +25 -0
- package/dist/agent/tests/agentLoader.test.cjs +18 -0
- package/dist/agent/tests/agentLoader.test.js +18 -0
- package/dist/agent/tests/modelFactory.test.cjs +13 -0
- package/dist/agent/tests/modelFactory.test.js +14 -1
- package/dist/agent/tests/toolRegistry.test.cjs +15 -0
- package/dist/agent/tests/toolRegistry.test.js +15 -0
- package/dist/agent/tools/code_search.cjs +1 -1
- package/dist/agent/tools/code_search.js +1 -1
- package/dist/agent/tools/command_execute.cjs +1 -1
- package/dist/agent/tools/command_execute.js +1 -1
- package/dist/agent/tools/ui_registry.d.ts +3 -3
- package/dist/cli/core/agentInvoker.cjs +212 -21
- package/dist/cli/core/agentInvoker.d.ts +55 -20
- package/dist/cli/core/agentInvoker.js +197 -21
- package/dist/cli/core/sessionManager.cjs +93 -4
- package/dist/cli/core/sessionManager.d.ts +1 -1
- package/dist/cli/core/sessionManager.js +93 -4
- package/dist/gateway/http/agents.cjs +121 -10
- package/dist/gateway/http/agents.js +121 -10
- package/dist/gateway/index.cjs +2 -2
- package/dist/gateway/server.cjs +55 -17
- package/dist/gateway/server.js +55 -17
- package/dist/gateway/types.d.ts +9 -1
- package/dist/tests/additionalMessageMiddleware.test.cjs +26 -0
- package/dist/tests/additionalMessageMiddleware.test.js +26 -0
- package/dist/tests/agentInvokerAttachments.test.cjs +123 -0
- package/dist/tests/agentInvokerAttachments.test.js +123 -0
- package/dist/tests/agentInvokerWorkdir.test.cjs +100 -0
- package/dist/tests/agentInvokerWorkdir.test.d.ts +1 -0
- package/dist/tests/agentInvokerWorkdir.test.js +72 -0
- package/dist/tests/agents-api.test.cjs +232 -0
- package/dist/tests/agents-api.test.d.ts +1 -0
- package/dist/tests/agents-api.test.js +226 -0
- package/dist/tests/gateway.test.cjs +21 -0
- package/dist/tests/gateway.test.js +21 -0
- package/dist/tests/sessionMessageAttachments.test.cjs +59 -0
- package/dist/tests/sessionMessageAttachments.test.js +59 -0
- package/dist/types/agents.d.ts +5 -0
- package/dist/webui/assets/index-BytPznA_.css +1 -0
- package/dist/webui/assets/index-u_5qlVip.js +176 -0
- package/dist/webui/index.html +2 -2
- package/package.json +3 -3
- package/.wingman/agents/wingman/agent.json +0 -12
- package/dist/webui/assets/index-CyE7T5pV.js +0 -162
- package/dist/webui/assets/index-DMEHdune.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CompositeBackend, FilesystemBackend, createDeepAgent } from "deepagents";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { isAbsolute, join, normalize, sep } from "node:path";
|
|
4
4
|
import { v4 } from "uuid";
|
|
5
5
|
import { AgentLoader } from "../../agent/config/agentLoader.js";
|
|
6
6
|
import { WingmanConfigLoader } from "../config/loader.js";
|
|
@@ -20,6 +20,40 @@ function _define_property(obj, key, value) {
|
|
|
20
20
|
else obj[key] = value;
|
|
21
21
|
return obj;
|
|
22
22
|
}
|
|
23
|
+
const WORKDIR_VIRTUAL_PATH = "/workdir/";
|
|
24
|
+
const OUTPUT_VIRTUAL_PATH = "/output/";
|
|
25
|
+
const isPathWithinRoot = (targetPath, rootPath)=>{
|
|
26
|
+
const normalizedTarget = normalize(targetPath);
|
|
27
|
+
const normalizedRoot = normalize(rootPath);
|
|
28
|
+
return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(normalizedRoot + sep);
|
|
29
|
+
};
|
|
30
|
+
const resolveExecutionWorkspace = (workspace, workdir)=>{
|
|
31
|
+
if (!workdir) return normalize(workspace);
|
|
32
|
+
if (isAbsolute(workdir)) return normalize(workdir);
|
|
33
|
+
return normalize(join(workspace, workdir));
|
|
34
|
+
};
|
|
35
|
+
const toWorkspaceAliasVirtualPath = (absolutePath)=>{
|
|
36
|
+
const normalized = normalize(absolutePath);
|
|
37
|
+
if (!isAbsolute(normalized)) return null;
|
|
38
|
+
const posixPath = normalized.replace(/\\/g, "/");
|
|
39
|
+
const trimmed = posixPath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
40
|
+
if (!trimmed) return null;
|
|
41
|
+
return `/${trimmed}/`;
|
|
42
|
+
};
|
|
43
|
+
const resolveExternalOutputMount = (workspace, workdir, defaultOutputDir)=>{
|
|
44
|
+
if (workdir && !isPathWithinRoot(workdir, workspace)) return {
|
|
45
|
+
virtualPath: WORKDIR_VIRTUAL_PATH,
|
|
46
|
+
absolutePath: workdir
|
|
47
|
+
};
|
|
48
|
+
if (!workdir && defaultOutputDir && !isPathWithinRoot(defaultOutputDir, workspace)) return {
|
|
49
|
+
virtualPath: OUTPUT_VIRTUAL_PATH,
|
|
50
|
+
absolutePath: defaultOutputDir
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
virtualPath: null,
|
|
54
|
+
absolutePath: null
|
|
55
|
+
};
|
|
56
|
+
};
|
|
23
57
|
class AgentInvoker {
|
|
24
58
|
findAllAgents() {
|
|
25
59
|
const agentConfigs = this.loader.loadAllAgentConfigs();
|
|
@@ -30,7 +64,10 @@ class AgentInvoker {
|
|
|
30
64
|
}
|
|
31
65
|
async invokeAgent(agentName, prompt, sessionId, attachments) {
|
|
32
66
|
try {
|
|
33
|
-
const
|
|
67
|
+
const executionWorkspace = resolveExecutionWorkspace(this.workspace, this.workdir);
|
|
68
|
+
const effectiveWorkdir = this.workdir ? executionWorkspace : null;
|
|
69
|
+
const loader = normalize(executionWorkspace) === normalize(this.workspace) ? this.loader : new AgentLoader(this.configDir, this.workspace, this.wingmanConfig, executionWorkspace);
|
|
70
|
+
const targetAgent = await loader.loadAgent(agentName);
|
|
34
71
|
if (!targetAgent) throw new Error(`Agent "${agentName}" not found`);
|
|
35
72
|
this.logger.info(`Invoking agent: ${agentName}`);
|
|
36
73
|
const preview = prompt.trim() || (attachments && attachments.length > 0 ? buildAttachmentPreview(attachments) : "");
|
|
@@ -58,43 +95,65 @@ class AgentInvoker {
|
|
|
58
95
|
}
|
|
59
96
|
}
|
|
60
97
|
const skillsDirectory = this.wingmanConfig?.skills?.skillsDirectory || "skills";
|
|
98
|
+
const normalizedSkillsDirectory = skillsDirectory.replace(/^\/+|\/+$/g, "");
|
|
99
|
+
const skillsVirtualPath = `/${normalizedSkillsDirectory}/`;
|
|
100
|
+
const outputMount = resolveExternalOutputMount(executionWorkspace, effectiveWorkdir, this.defaultOutputDir);
|
|
61
101
|
const middleware = [
|
|
62
102
|
mediaCompatibilityMiddleware({
|
|
63
103
|
model: targetAgent.model
|
|
64
104
|
}),
|
|
65
105
|
additionalMessageMiddleware({
|
|
66
|
-
workspaceRoot:
|
|
67
|
-
workdir:
|
|
106
|
+
workspaceRoot: executionWorkspace,
|
|
107
|
+
workdir: effectiveWorkdir,
|
|
68
108
|
defaultOutputDir: this.defaultOutputDir,
|
|
109
|
+
outputVirtualPath: outputMount.virtualPath,
|
|
69
110
|
dynamicUiEnabled: this.wingmanConfig?.gateway?.dynamicUiEnabled !== false,
|
|
70
111
|
skillsDirectory
|
|
71
112
|
})
|
|
72
113
|
];
|
|
73
114
|
if (mergedHooks) {
|
|
74
115
|
this.logger.debug(`Adding hooks middleware with ${mergedHooks.PreToolUse?.length || 0} PreToolUse hooks, ${mergedHooks.PostToolUse?.length || 0} PostToolUse hooks, and ${mergedHooks.Stop?.length || 0} Stop hooks`);
|
|
75
|
-
middleware.push(createHooksMiddleware(mergedHooks,
|
|
116
|
+
middleware.push(createHooksMiddleware(mergedHooks, executionWorkspace, hookSessionId, this.logger));
|
|
76
117
|
}
|
|
77
118
|
const checkpointer = this.sessionManager?.getCheckpointer();
|
|
78
119
|
const bundledSkillsPath = getBundledSkillsPath();
|
|
79
120
|
const skillsSources = [];
|
|
80
121
|
if (existsSync(bundledSkillsPath)) skillsSources.push("/skills-bundled/");
|
|
81
|
-
skillsSources.push(
|
|
122
|
+
skillsSources.push(skillsVirtualPath);
|
|
82
123
|
const backendOverrides = {
|
|
83
124
|
"/memories/": new FilesystemBackend({
|
|
84
125
|
rootDir: join(this.workspace, this.configDir, "memories"),
|
|
85
126
|
virtualMode: true
|
|
86
127
|
})
|
|
87
128
|
};
|
|
129
|
+
const executionWorkspaceAlias = toWorkspaceAliasVirtualPath(executionWorkspace);
|
|
130
|
+
if (executionWorkspaceAlias) backendOverrides[executionWorkspaceAlias] = new FilesystemBackend({
|
|
131
|
+
rootDir: executionWorkspace,
|
|
132
|
+
virtualMode: true
|
|
133
|
+
});
|
|
134
|
+
if (effectiveWorkdir) backendOverrides[WORKDIR_VIRTUAL_PATH] = new FilesystemBackend({
|
|
135
|
+
rootDir: executionWorkspace,
|
|
136
|
+
virtualMode: true
|
|
137
|
+
});
|
|
138
|
+
const workspaceSkillsPath = join(this.workspace, normalizedSkillsDirectory);
|
|
139
|
+
if (existsSync(workspaceSkillsPath)) backendOverrides[skillsVirtualPath] = new FilesystemBackend({
|
|
140
|
+
rootDir: workspaceSkillsPath,
|
|
141
|
+
virtualMode: true
|
|
142
|
+
});
|
|
88
143
|
if (existsSync(bundledSkillsPath)) backendOverrides["/skills-bundled/"] = new FilesystemBackend({
|
|
89
144
|
rootDir: bundledSkillsPath,
|
|
90
145
|
virtualMode: true
|
|
91
146
|
});
|
|
147
|
+
if (outputMount.virtualPath && outputMount.absolutePath) backendOverrides[outputMount.virtualPath] = new FilesystemBackend({
|
|
148
|
+
rootDir: outputMount.absolutePath,
|
|
149
|
+
virtualMode: true
|
|
150
|
+
});
|
|
92
151
|
const standaloneAgent = createDeepAgent({
|
|
93
152
|
systemPrompt: targetAgent.systemPrompt,
|
|
94
153
|
tools: targetAgent.tools,
|
|
95
154
|
model: targetAgent.model,
|
|
96
155
|
backend: ()=>new CompositeBackend(new FilesystemBackend({
|
|
97
|
-
rootDir:
|
|
156
|
+
rootDir: executionWorkspace,
|
|
98
157
|
virtualMode: true
|
|
99
158
|
}), backendOverrides),
|
|
100
159
|
middleware: middleware,
|
|
@@ -103,7 +162,7 @@ class AgentInvoker {
|
|
|
103
162
|
checkpointer: checkpointer
|
|
104
163
|
});
|
|
105
164
|
this.logger.debug("Agent created, sending message");
|
|
106
|
-
const userContent = buildUserContent(prompt, attachments);
|
|
165
|
+
const userContent = buildUserContent(prompt, attachments, targetAgent.model);
|
|
107
166
|
if (this.sessionManager && sessionId) {
|
|
108
167
|
this.logger.debug(`Using streaming with session: ${sessionId}`);
|
|
109
168
|
const stream = await standaloneAgent.streamEvents({
|
|
@@ -187,7 +246,7 @@ class AgentInvoker {
|
|
|
187
246
|
this.loader = new AgentLoader(this.configDir, this.workspace, this.wingmanConfig);
|
|
188
247
|
}
|
|
189
248
|
}
|
|
190
|
-
function buildUserContent(prompt, attachments) {
|
|
249
|
+
function buildUserContent(prompt, attachments, model) {
|
|
191
250
|
const text = prompt?.trim() ?? "";
|
|
192
251
|
if (!attachments || 0 === attachments.length) return text;
|
|
193
252
|
const parts = [];
|
|
@@ -195,18 +254,32 @@ function buildUserContent(prompt, attachments) {
|
|
|
195
254
|
type: "text",
|
|
196
255
|
text
|
|
197
256
|
});
|
|
198
|
-
for (const attachment of attachments)if (attachment
|
|
199
|
-
if (
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
257
|
+
for (const attachment of attachments)if (attachment) {
|
|
258
|
+
if (isFileAttachment(attachment)) {
|
|
259
|
+
const nativePdfPart = buildNativePdfPart(attachment, model);
|
|
260
|
+
if (nativePdfPart) {
|
|
261
|
+
parts.push(nativePdfPart);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
parts.push({
|
|
265
|
+
type: "text",
|
|
266
|
+
text: buildFileAttachmentText(attachment)
|
|
267
|
+
});
|
|
202
268
|
continue;
|
|
203
269
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
270
|
+
if (attachment.dataUrl) {
|
|
271
|
+
if (isAudioAttachment(attachment)) {
|
|
272
|
+
const audioPart = buildAudioPart(attachment);
|
|
273
|
+
if (audioPart) parts.push(audioPart);
|
|
274
|
+
continue;
|
|
208
275
|
}
|
|
209
|
-
|
|
276
|
+
parts.push({
|
|
277
|
+
type: "image_url",
|
|
278
|
+
image_url: {
|
|
279
|
+
url: attachment.dataUrl
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
210
283
|
}
|
|
211
284
|
if (0 === parts.length) {
|
|
212
285
|
if (!text) throw new Error("Attachment payload is empty or invalid.");
|
|
@@ -214,12 +287,93 @@ function buildUserContent(prompt, attachments) {
|
|
|
214
287
|
}
|
|
215
288
|
return parts;
|
|
216
289
|
}
|
|
290
|
+
function supportsNativePdfInputs(model) {
|
|
291
|
+
if (!model || "object" != typeof model) return false;
|
|
292
|
+
try {
|
|
293
|
+
const profile = model.profile;
|
|
294
|
+
if (!profile || "object" != typeof profile) return false;
|
|
295
|
+
return true === profile.pdfInputs;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function isPdfName(name) {
|
|
301
|
+
return (name || "").trim().toLowerCase().endsWith(".pdf");
|
|
302
|
+
}
|
|
303
|
+
function resolveFileMimeType(attachment) {
|
|
304
|
+
const direct = attachment.mimeType?.trim().toLowerCase();
|
|
305
|
+
if (direct) return direct.split(";")[0] || "";
|
|
306
|
+
const parsed = parseDataUrl(attachment.dataUrl);
|
|
307
|
+
return (parsed.mimeType || "").trim().toLowerCase().split(";")[0] || "";
|
|
308
|
+
}
|
|
309
|
+
function buildFileMetadata(attachment, defaultName) {
|
|
310
|
+
const filename = attachment.name?.trim() || defaultName;
|
|
311
|
+
return {
|
|
312
|
+
filename,
|
|
313
|
+
name: filename,
|
|
314
|
+
title: filename
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function buildNativePdfPart(attachment, model) {
|
|
318
|
+
if (!supportsNativePdfInputs(model)) return null;
|
|
319
|
+
const mimeType = resolveFileMimeType(attachment);
|
|
320
|
+
const isPdf = "application/pdf" === mimeType || isPdfName(attachment.name);
|
|
321
|
+
if (!isPdf) return null;
|
|
322
|
+
const metadata = buildFileMetadata(attachment, "document.pdf");
|
|
323
|
+
const parsed = parseDataUrl(attachment.dataUrl);
|
|
324
|
+
const useResponsesInputFile = shouldUseResponsesInputFile(model);
|
|
325
|
+
if (useResponsesInputFile) {
|
|
326
|
+
if (parsed.data) {
|
|
327
|
+
const fileDataMime = parsed.mimeType || mimeType || "application/pdf";
|
|
328
|
+
return {
|
|
329
|
+
type: "input_file",
|
|
330
|
+
file_data: `data:${fileDataMime};base64,${parsed.data}`,
|
|
331
|
+
filename: metadata.filename
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const fileDataUrl = attachment.dataUrl?.trim();
|
|
335
|
+
if (!fileDataUrl || !fileDataUrl.startsWith("data:")) return null;
|
|
336
|
+
return {
|
|
337
|
+
type: "input_file",
|
|
338
|
+
file_data: fileDataUrl,
|
|
339
|
+
filename: metadata.filename
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (parsed.data) return {
|
|
343
|
+
type: "file",
|
|
344
|
+
source_type: "base64",
|
|
345
|
+
mime_type: parsed.mimeType || mimeType || "application/pdf",
|
|
346
|
+
data: parsed.data,
|
|
347
|
+
metadata
|
|
348
|
+
};
|
|
349
|
+
const url = attachment.dataUrl?.trim();
|
|
350
|
+
if (!url || !url.startsWith("data:")) return null;
|
|
351
|
+
return {
|
|
352
|
+
type: "file",
|
|
353
|
+
source_type: "url",
|
|
354
|
+
mime_type: mimeType || "application/pdf",
|
|
355
|
+
url,
|
|
356
|
+
metadata
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function shouldUseResponsesInputFile(model) {
|
|
360
|
+
if (!model || "object" != typeof model) return false;
|
|
361
|
+
try {
|
|
362
|
+
const flag = model.useResponsesApi;
|
|
363
|
+
if ("boolean" == typeof flag) return flag;
|
|
364
|
+
} catch {}
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
217
367
|
function isAudioAttachment(attachment) {
|
|
218
368
|
if ("audio" === attachment.kind) return true;
|
|
219
369
|
if (attachment.mimeType?.startsWith("audio/")) return true;
|
|
220
370
|
if (attachment.dataUrl?.startsWith("data:audio/")) return true;
|
|
221
371
|
return false;
|
|
222
372
|
}
|
|
373
|
+
function isFileAttachment(attachment) {
|
|
374
|
+
if ("file" === attachment.kind) return true;
|
|
375
|
+
return "string" == typeof attachment.textContent;
|
|
376
|
+
}
|
|
223
377
|
function buildAudioPart(attachment) {
|
|
224
378
|
const parsed = parseDataUrl(attachment.dataUrl);
|
|
225
379
|
const mimeType = attachment.mimeType || parsed.mimeType;
|
|
@@ -245,14 +399,36 @@ function parseDataUrl(dataUrl) {
|
|
|
245
399
|
data: match[2]
|
|
246
400
|
};
|
|
247
401
|
}
|
|
402
|
+
function buildFileAttachmentText(attachment) {
|
|
403
|
+
const name = attachment.name?.trim() || "file";
|
|
404
|
+
const mime = attachment.mimeType?.trim();
|
|
405
|
+
const sizeLabel = "number" == typeof attachment.size && attachment.size >= 0 ? `, ${attachment.size} bytes` : "";
|
|
406
|
+
const meta = mime || sizeLabel ? ` (${[
|
|
407
|
+
mime,
|
|
408
|
+
sizeLabel.replace(/^, /, "")
|
|
409
|
+
].filter(Boolean).join(", ")})` : "";
|
|
410
|
+
const header = `[Attached file: ${name}${meta}]`;
|
|
411
|
+
const text = attachment.textContent?.trim();
|
|
412
|
+
if (!text) return `${header}\n[No extractable text content provided.]`;
|
|
413
|
+
return `${header}\n${text}`;
|
|
414
|
+
}
|
|
248
415
|
function buildAttachmentPreview(attachments) {
|
|
416
|
+
let hasFile = false;
|
|
249
417
|
let hasAudio = false;
|
|
250
418
|
let hasImage = false;
|
|
251
|
-
for (const attachment of attachments)
|
|
252
|
-
|
|
419
|
+
for (const attachment of attachments){
|
|
420
|
+
if (isFileAttachment(attachment)) {
|
|
421
|
+
hasFile = true;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (isAudioAttachment(attachment)) hasAudio = true;
|
|
425
|
+
else hasImage = true;
|
|
426
|
+
}
|
|
427
|
+
if (hasFile && (hasAudio || hasImage)) return "[files and media]";
|
|
253
428
|
if (hasAudio && hasImage) return "[attachments]";
|
|
429
|
+
if (hasFile) return "[file]";
|
|
254
430
|
if (hasAudio) return "[audio]";
|
|
255
431
|
if (hasImage) return "[image]";
|
|
256
432
|
return "";
|
|
257
433
|
}
|
|
258
|
-
export { AgentInvoker, buildUserContent };
|
|
434
|
+
export { AgentInvoker, OUTPUT_VIRTUAL_PATH, WORKDIR_VIRTUAL_PATH, buildUserContent, resolveExecutionWorkspace, resolveExternalOutputMount, toWorkspaceAliasVirtualPath };
|
|
@@ -480,10 +480,15 @@ function extractAttachments(blocks) {
|
|
|
480
480
|
continue;
|
|
481
481
|
}
|
|
482
482
|
const audioUrl = extractAudioUrl(block);
|
|
483
|
-
if (audioUrl)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
483
|
+
if (audioUrl) {
|
|
484
|
+
attachments.push({
|
|
485
|
+
kind: "audio",
|
|
486
|
+
dataUrl: audioUrl
|
|
487
|
+
});
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const fileAttachment = extractFileAttachment(block);
|
|
491
|
+
if (fileAttachment) attachments.push(fileAttachment);
|
|
487
492
|
}
|
|
488
493
|
return attachments;
|
|
489
494
|
}
|
|
@@ -538,6 +543,90 @@ function extractAudioUrl(block) {
|
|
|
538
543
|
}
|
|
539
544
|
return null;
|
|
540
545
|
}
|
|
546
|
+
function parseDataUrlMime(dataUrl) {
|
|
547
|
+
if ("string" != typeof dataUrl || !dataUrl.startsWith("data:")) return;
|
|
548
|
+
const match = dataUrl.match(/^data:([^;,]+)[;,]/i);
|
|
549
|
+
return match?.[1];
|
|
550
|
+
}
|
|
551
|
+
function extractString(...values) {
|
|
552
|
+
for (const value of values)if ("string" == typeof value && value.trim().length > 0) return value.trim();
|
|
553
|
+
}
|
|
554
|
+
function extractFileAttachment(block) {
|
|
555
|
+
if (!block || "object" != typeof block) return null;
|
|
556
|
+
if ("file" === block.type) {
|
|
557
|
+
const sourceType = block.source_type || block.sourceType;
|
|
558
|
+
const metadata = block.metadata && "object" == typeof block.metadata ? block.metadata : {};
|
|
559
|
+
const name = extractString(block.name, block.filename, metadata.filename, metadata.name, metadata.title);
|
|
560
|
+
const declaredMime = extractString(block.mime_type, block.mimeType, block.media_type, block.mediaType);
|
|
561
|
+
if ("base64" === sourceType && "string" == typeof block.data) {
|
|
562
|
+
const mimeType = declaredMime || "application/octet-stream";
|
|
563
|
+
return {
|
|
564
|
+
kind: "file",
|
|
565
|
+
dataUrl: `data:${mimeType};base64,${block.data}`,
|
|
566
|
+
name,
|
|
567
|
+
mimeType
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if ("url" === sourceType && "string" == typeof block.url) {
|
|
571
|
+
const mimeType = declaredMime || parseDataUrlMime(block.url);
|
|
572
|
+
return {
|
|
573
|
+
kind: "file",
|
|
574
|
+
dataUrl: block.url,
|
|
575
|
+
name,
|
|
576
|
+
mimeType
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const openAiFile = block.file;
|
|
580
|
+
if (openAiFile && "object" == typeof openAiFile) {
|
|
581
|
+
const fileData = extractString(openAiFile.file_data, openAiFile.data);
|
|
582
|
+
const fileUrl = extractString(openAiFile.file_url, openAiFile.url);
|
|
583
|
+
const fileName = extractString(openAiFile.filename, name);
|
|
584
|
+
if (fileData) return {
|
|
585
|
+
kind: "file",
|
|
586
|
+
dataUrl: fileData,
|
|
587
|
+
name: fileName,
|
|
588
|
+
mimeType: parseDataUrlMime(fileData)
|
|
589
|
+
};
|
|
590
|
+
if (fileUrl) return {
|
|
591
|
+
kind: "file",
|
|
592
|
+
dataUrl: fileUrl,
|
|
593
|
+
name: fileName,
|
|
594
|
+
mimeType: parseDataUrlMime(fileUrl)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if ("input_file" === block.type) {
|
|
599
|
+
const dataUrl = extractString(block.file_data, block.file_url);
|
|
600
|
+
if (!dataUrl) return null;
|
|
601
|
+
return {
|
|
602
|
+
kind: "file",
|
|
603
|
+
dataUrl,
|
|
604
|
+
name: extractString(block.filename),
|
|
605
|
+
mimeType: parseDataUrlMime(dataUrl)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
if ("document" === block.type && block.source && "object" == typeof block.source) {
|
|
609
|
+
const source = block.source;
|
|
610
|
+
const sourceType = extractString(source.type);
|
|
611
|
+
const name = extractString(block.title);
|
|
612
|
+
if ("base64" === sourceType && "string" == typeof source.data) {
|
|
613
|
+
const mimeType = extractString(source.media_type, "application/pdf");
|
|
614
|
+
return {
|
|
615
|
+
kind: "file",
|
|
616
|
+
dataUrl: `data:${mimeType};base64,${source.data}`,
|
|
617
|
+
name,
|
|
618
|
+
mimeType
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if ("url" === sourceType && "string" == typeof source.url) return {
|
|
622
|
+
kind: "file",
|
|
623
|
+
dataUrl: source.url,
|
|
624
|
+
name,
|
|
625
|
+
mimeType: parseDataUrlMime(source.url)
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
541
630
|
function resolveAudioMimeType(format) {
|
|
542
631
|
const normalized = format.toLowerCase();
|
|
543
632
|
switch(normalized){
|
|
@@ -447,10 +447,15 @@ function extractAttachments(blocks) {
|
|
|
447
447
|
continue;
|
|
448
448
|
}
|
|
449
449
|
const audioUrl = extractAudioUrl(block);
|
|
450
|
-
if (audioUrl)
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
450
|
+
if (audioUrl) {
|
|
451
|
+
attachments.push({
|
|
452
|
+
kind: "audio",
|
|
453
|
+
dataUrl: audioUrl
|
|
454
|
+
});
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const fileAttachment = extractFileAttachment(block);
|
|
458
|
+
if (fileAttachment) attachments.push(fileAttachment);
|
|
454
459
|
}
|
|
455
460
|
return attachments;
|
|
456
461
|
}
|
|
@@ -505,6 +510,90 @@ function extractAudioUrl(block) {
|
|
|
505
510
|
}
|
|
506
511
|
return null;
|
|
507
512
|
}
|
|
513
|
+
function parseDataUrlMime(dataUrl) {
|
|
514
|
+
if ("string" != typeof dataUrl || !dataUrl.startsWith("data:")) return;
|
|
515
|
+
const match = dataUrl.match(/^data:([^;,]+)[;,]/i);
|
|
516
|
+
return match?.[1];
|
|
517
|
+
}
|
|
518
|
+
function extractString(...values) {
|
|
519
|
+
for (const value of values)if ("string" == typeof value && value.trim().length > 0) return value.trim();
|
|
520
|
+
}
|
|
521
|
+
function extractFileAttachment(block) {
|
|
522
|
+
if (!block || "object" != typeof block) return null;
|
|
523
|
+
if ("file" === block.type) {
|
|
524
|
+
const sourceType = block.source_type || block.sourceType;
|
|
525
|
+
const metadata = block.metadata && "object" == typeof block.metadata ? block.metadata : {};
|
|
526
|
+
const name = extractString(block.name, block.filename, metadata.filename, metadata.name, metadata.title);
|
|
527
|
+
const declaredMime = extractString(block.mime_type, block.mimeType, block.media_type, block.mediaType);
|
|
528
|
+
if ("base64" === sourceType && "string" == typeof block.data) {
|
|
529
|
+
const mimeType = declaredMime || "application/octet-stream";
|
|
530
|
+
return {
|
|
531
|
+
kind: "file",
|
|
532
|
+
dataUrl: `data:${mimeType};base64,${block.data}`,
|
|
533
|
+
name,
|
|
534
|
+
mimeType
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if ("url" === sourceType && "string" == typeof block.url) {
|
|
538
|
+
const mimeType = declaredMime || parseDataUrlMime(block.url);
|
|
539
|
+
return {
|
|
540
|
+
kind: "file",
|
|
541
|
+
dataUrl: block.url,
|
|
542
|
+
name,
|
|
543
|
+
mimeType
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const openAiFile = block.file;
|
|
547
|
+
if (openAiFile && "object" == typeof openAiFile) {
|
|
548
|
+
const fileData = extractString(openAiFile.file_data, openAiFile.data);
|
|
549
|
+
const fileUrl = extractString(openAiFile.file_url, openAiFile.url);
|
|
550
|
+
const fileName = extractString(openAiFile.filename, name);
|
|
551
|
+
if (fileData) return {
|
|
552
|
+
kind: "file",
|
|
553
|
+
dataUrl: fileData,
|
|
554
|
+
name: fileName,
|
|
555
|
+
mimeType: parseDataUrlMime(fileData)
|
|
556
|
+
};
|
|
557
|
+
if (fileUrl) return {
|
|
558
|
+
kind: "file",
|
|
559
|
+
dataUrl: fileUrl,
|
|
560
|
+
name: fileName,
|
|
561
|
+
mimeType: parseDataUrlMime(fileUrl)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if ("input_file" === block.type) {
|
|
566
|
+
const dataUrl = extractString(block.file_data, block.file_url);
|
|
567
|
+
if (!dataUrl) return null;
|
|
568
|
+
return {
|
|
569
|
+
kind: "file",
|
|
570
|
+
dataUrl,
|
|
571
|
+
name: extractString(block.filename),
|
|
572
|
+
mimeType: parseDataUrlMime(dataUrl)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if ("document" === block.type && block.source && "object" == typeof block.source) {
|
|
576
|
+
const source = block.source;
|
|
577
|
+
const sourceType = extractString(source.type);
|
|
578
|
+
const name = extractString(block.title);
|
|
579
|
+
if ("base64" === sourceType && "string" == typeof source.data) {
|
|
580
|
+
const mimeType = extractString(source.media_type, "application/pdf");
|
|
581
|
+
return {
|
|
582
|
+
kind: "file",
|
|
583
|
+
dataUrl: `data:${mimeType};base64,${source.data}`,
|
|
584
|
+
name,
|
|
585
|
+
mimeType
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
if ("url" === sourceType && "string" == typeof source.url) return {
|
|
589
|
+
kind: "file",
|
|
590
|
+
dataUrl: source.url,
|
|
591
|
+
name,
|
|
592
|
+
mimeType: parseDataUrlMime(source.url)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
508
597
|
function resolveAudioMimeType(format) {
|
|
509
598
|
const normalized = format.toLowerCase();
|
|
510
599
|
switch(normalized){
|