@agent-native/core 0.101.3 → 0.101.5
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +15 -0
- package/corpus/core/src/agent/production-agent.ts +215 -19
- package/corpus/core/src/client/RunStuckBanner.tsx +12 -5
- package/corpus/core/src/client/sse-event-processor.ts +36 -1
- package/corpus/core/src/client/use-run-stuck-detection.ts +162 -25
- package/corpus/core/src/coding-tools/run-code.ts +11 -5
- package/corpus/core/src/deploy/build.ts +78 -25
- package/corpus/core/src/extensions/actions.ts +15 -0
- package/corpus/core/src/extensions/url-safety.ts +11 -1
- package/corpus/core/src/notifications/channels.ts +61 -30
- package/corpus/core/src/server/action-discovery.ts +4 -1
- package/corpus/core/src/server/agent-chat/context-tools.ts +4 -0
- package/corpus/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
- package/corpus/templates/clips/app/components/player/media-duration.ts +24 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +9 -18
- package/corpus/templates/clips/changelog/2026-07-14-paused-time-no-longer-counts-toward-chrome-extension-recordi.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -4
- package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +32 -0
- package/corpus/templates/slides/actions/_uploaded-files.ts +13 -2
- package/corpus/templates/slides/actions/import-docx.ts +3 -9
- package/corpus/templates/slides/actions/import-file.ts +11 -14
- package/corpus/templates/slides/actions/import-pptx.ts +3 -9
- package/corpus/templates/slides/changelog/2026-07-14-powerpoint-template-uploads-now-work-in-hosted-slides-deploy.md +6 -0
- package/corpus/templates/slides/server/handlers/uploads.ts +100 -50
- package/corpus/templates/slides/server/lib/tenant-files.ts +38 -11
- package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +108 -0
- package/dist/action.d.ts +12 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +2 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +24 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +175 -18
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +9 -5
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +33 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +4 -4
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +104 -19
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts +5 -2
- package/dist/coding-tools/run-code.d.ts.map +1 -1
- package/dist/coding-tools/run-code.js +11 -5
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +15 -14
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +64 -23
- package/dist/deploy/build.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +14 -0
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/url-safety.d.ts +6 -0
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +6 -0
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/notifications/channels.d.ts.map +1 -1
- package/dist/notifications/channels.js +38 -22
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +4 -1
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-chat/context-tools.d.ts.map +1 -1
- package/dist/server/agent-chat/context-tools.js +4 -0
- package/dist/server/agent-chat/context-tools.js.map +1 -1
- package/package.json +3 -3
|
@@ -192,6 +192,9 @@ function preserveActionFlags(entry: Record<string, any>): Partial<ActionEntry> {
|
|
|
192
192
|
if (typeof entry.parallelSafe === "boolean") {
|
|
193
193
|
out.parallelSafe = entry.parallelSafe;
|
|
194
194
|
}
|
|
195
|
+
if (typeof entry.dedupe === "boolean") {
|
|
196
|
+
out.dedupe = entry.dedupe;
|
|
197
|
+
}
|
|
195
198
|
if (typeof entry.toolCallable === "boolean") {
|
|
196
199
|
out.toolCallable = entry.toolCallable;
|
|
197
200
|
}
|
|
@@ -716,7 +719,7 @@ export async function mergeCoreSharingActions(
|
|
|
716
719
|
run: def.run,
|
|
717
720
|
...(def.http !== undefined ? { http: def.http } : {}),
|
|
718
721
|
// Carry security-relevant flags (toolCallable, publicAgent, link,
|
|
719
|
-
// mcpApp) plus readOnly/parallelSafe. Without this, the sharing
|
|
722
|
+
// mcpApp) plus readOnly/parallelSafe/dedupe. Without this, the sharing
|
|
720
723
|
// actions' `toolCallable: false` (audit-H5) is dropped and the
|
|
721
724
|
// tools-iframe bridge 403 in action-routes.ts never fires.
|
|
722
725
|
...preserveActionFlags(def),
|
|
@@ -241,6 +241,10 @@ export function createRefreshScreenEntry(): Record<string, ActionEntry> {
|
|
|
241
241
|
// distinct `screen-refresh` poll event. Don't double-emit a generic
|
|
242
242
|
// `action` event on top of that.
|
|
243
243
|
readOnly: true,
|
|
244
|
+
// Refetching volatile on-screen state is the entire point of this
|
|
245
|
+
// tool — an identical repeat call (even with the same scope) is a
|
|
246
|
+
// legitimate re-refresh, not a redundant read to skip.
|
|
247
|
+
dedupe: false,
|
|
244
248
|
tool: {
|
|
245
249
|
description:
|
|
246
250
|
"Manually refresh the user's current screen. The framework ALREADY auto-refreshes after any successful mutating action tool call (template actions and any enabled raw DB write tools) — you do NOT need to call this after a normal action. Use it only when (a) you mutated data via a path the framework can't detect (e.g. a direct write to an external system the app mirrors), or (b) you want to pass a `scope` hint so the UI narrows which queries to refetch. The UI re-fetches its queries without a full page reload.",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const MEDIARECORDER_DURATION_DRIFT_MS = 3_000;
|
|
2
|
+
|
|
3
|
+
function finitePositiveMs(value: number): number {
|
|
4
|
+
return Number.isFinite(value) && value > 0 ? Math.round(value) : 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prefer recorder metadata for normal MediaRecorder timeslice drift, but use
|
|
9
|
+
* the playable media's duration when the two values clearly describe
|
|
10
|
+
* different timelines (for example, metadata that accidentally counted a
|
|
11
|
+
* long pause).
|
|
12
|
+
*/
|
|
13
|
+
export function resolveMediaDurationMs(
|
|
14
|
+
recordedDurationMs: number,
|
|
15
|
+
mediaDurationSeconds: number,
|
|
16
|
+
): number {
|
|
17
|
+
const recordedMs = finitePositiveMs(recordedDurationMs);
|
|
18
|
+
const mediaMs = finitePositiveMs(mediaDurationSeconds * 1000);
|
|
19
|
+
if (mediaMs === 0) return recordedMs;
|
|
20
|
+
if (recordedMs === 0) return mediaMs;
|
|
21
|
+
return Math.abs(recordedMs - mediaMs) > MEDIARECORDER_DURATION_DRIFT_MS
|
|
22
|
+
? mediaMs
|
|
23
|
+
: recordedMs;
|
|
24
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
useState,
|
|
20
20
|
} from "react";
|
|
21
21
|
|
|
22
|
+
import { resolveMediaDurationMs } from "@/components/player/media-duration";
|
|
22
23
|
import {
|
|
23
24
|
DropdownMenu,
|
|
24
25
|
DropdownMenuContent,
|
|
@@ -824,22 +825,16 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
824
825
|
durationProbedRef.current = false;
|
|
825
826
|
}, [activeVideoSrc, durationMs]);
|
|
826
827
|
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
// shorter length than the actual recording on first watch.
|
|
832
|
-
const hasReliableDurationProp =
|
|
833
|
-
Number.isFinite(durationMs) && durationMs > 0;
|
|
834
|
-
|
|
828
|
+
// Prefer the recorder's elapsed-time counter for ordinary WebM timeslice
|
|
829
|
+
// drift, but let clearly different playable-media metadata win. This also
|
|
830
|
+
// repairs playback controls for older clips whose stored duration counted
|
|
831
|
+
// time spent paused.
|
|
835
832
|
const probeDurationIfNeeded = useCallback(
|
|
836
833
|
(v: HTMLVideoElement) => {
|
|
837
834
|
if (durationProbedRef.current) return;
|
|
838
835
|
if (Number.isFinite(v.duration) && v.duration > 0) {
|
|
839
836
|
durationProbedRef.current = true;
|
|
840
|
-
|
|
841
|
-
setResolvedDurationMs(Math.round(v.duration * 1000));
|
|
842
|
-
}
|
|
837
|
+
setResolvedDurationMs(resolveMediaDurationMs(durationMs, v.duration));
|
|
843
838
|
return;
|
|
844
839
|
}
|
|
845
840
|
if (playAttemptPendingRef.current || !v.paused) return;
|
|
@@ -855,7 +850,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
855
850
|
// picks up the real duration.
|
|
856
851
|
}
|
|
857
852
|
},
|
|
858
|
-
[
|
|
853
|
+
[durationMs],
|
|
859
854
|
);
|
|
860
855
|
|
|
861
856
|
// Resolve the WebM-duration-is-Infinity Chrome quirk: when a video created
|
|
@@ -873,11 +868,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
873
868
|
|
|
874
869
|
const onDurationChange = () => {
|
|
875
870
|
if (Number.isFinite(v.duration) && v.duration > 0) {
|
|
876
|
-
|
|
877
|
-
// cluster-estimated WebM duration; only adopt it as a fallback.
|
|
878
|
-
if (!hasReliableDurationProp) {
|
|
879
|
-
setResolvedDurationMs(Math.round(v.duration * 1000));
|
|
880
|
-
}
|
|
871
|
+
setResolvedDurationMs(resolveMediaDurationMs(durationMs, v.duration));
|
|
881
872
|
// After we've resolved the real duration, rewind back to 0 so the
|
|
882
873
|
// user isn't sitting at the end of the clip.
|
|
883
874
|
if (durationProbedRef.current && v.currentTime > v.duration) {
|
|
@@ -900,7 +891,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
900
891
|
v.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
901
892
|
v.removeEventListener("durationchange", onDurationChange);
|
|
902
893
|
};
|
|
903
|
-
}, [activeVideoSrc,
|
|
894
|
+
}, [activeVideoSrc, durationMs, probeDurationIfNeeded]);
|
|
904
895
|
|
|
905
896
|
// Reset the thumbnail-capture flag when the source changes (e.g. the
|
|
906
897
|
// player is reused for a different recording via React Router).
|
|
@@ -36,6 +36,13 @@ import {
|
|
|
36
36
|
import { MAX_UPLOAD_BYTES } from "@shared/upload-limits";
|
|
37
37
|
|
|
38
38
|
import { waitForReadyRecordingAfterFinalizeError } from "./finalize-recovery";
|
|
39
|
+
import {
|
|
40
|
+
createRecordingDurationState,
|
|
41
|
+
pauseRecordingDuration,
|
|
42
|
+
resumeRecordingDuration,
|
|
43
|
+
startRecordingDuration,
|
|
44
|
+
type RecordingDurationState,
|
|
45
|
+
} from "./recording-duration";
|
|
39
46
|
import { captureExtensionError, initExtensionSentry } from "./sentry";
|
|
40
47
|
|
|
41
48
|
initExtensionSentry("offscreen");
|
|
@@ -187,6 +194,7 @@ type ActiveRecording = {
|
|
|
187
194
|
authToken: string | null;
|
|
188
195
|
mode: CaptureMode;
|
|
189
196
|
startedAtMs: number;
|
|
197
|
+
duration: RecordingDurationState;
|
|
190
198
|
mimeType: string;
|
|
191
199
|
recorder: MediaRecorder;
|
|
192
200
|
outputStream: MediaStream;
|
|
@@ -1218,6 +1226,7 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
1218
1226
|
authToken: message.authToken ?? null,
|
|
1219
1227
|
mode: ready.mode,
|
|
1220
1228
|
startedAtMs: 0,
|
|
1229
|
+
duration: createRecordingDurationState(),
|
|
1221
1230
|
mimeType,
|
|
1222
1231
|
recorder,
|
|
1223
1232
|
outputStream,
|
|
@@ -1350,6 +1359,7 @@ function startRecorderNow(recording: ActiveRecording): void {
|
|
|
1350
1359
|
playStartChime();
|
|
1351
1360
|
recording.recorder.start(2000);
|
|
1352
1361
|
recording.startedAtMs = Date.now();
|
|
1362
|
+
recording.duration = startRecordingDuration(recording.startedAtMs);
|
|
1353
1363
|
console.log("[clips-offscreen] recorder.start ok");
|
|
1354
1364
|
reportStatus(recording.sessionId, "recording", {
|
|
1355
1365
|
recordingId: recording.recordingId,
|
|
@@ -1446,6 +1456,11 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1446
1456
|
recording.resolveStopped({ ok: true, status: "cancelled" });
|
|
1447
1457
|
return;
|
|
1448
1458
|
}
|
|
1459
|
+
// Freeze the media duration as soon as MediaRecorder stops. Waiting for
|
|
1460
|
+
// outstanding uploads below must not make the clip appear longer, and time
|
|
1461
|
+
// spent paused is excluded by pauseRecordingDuration().
|
|
1462
|
+
recording.duration = pauseRecordingDuration(recording.duration, Date.now());
|
|
1463
|
+
const durationMs = recording.duration.elapsedMs;
|
|
1449
1464
|
reportStatus(recording.sessionId, "uploading", {
|
|
1450
1465
|
recordingId: recording.recordingId,
|
|
1451
1466
|
});
|
|
@@ -1460,7 +1475,6 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1460
1475
|
? rejected.reason
|
|
1461
1476
|
: new Error(String(rejected.reason));
|
|
1462
1477
|
}
|
|
1463
|
-
const durationMs = Math.max(0, Date.now() - recording.startedAtMs);
|
|
1464
1478
|
// Surface WHY a finalize might fail before the server's cryptic "No chunks
|
|
1465
1479
|
// found": 0 chunks means the recorder emitted no non-empty data (empty
|
|
1466
1480
|
// capture / never started), which is a different problem than an auth 401.
|
|
@@ -1549,7 +1563,7 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1549
1563
|
recordingId: recording.recordingId,
|
|
1550
1564
|
chunkCount: recording.chunkIndex,
|
|
1551
1565
|
mimeType: recording.mimeType,
|
|
1552
|
-
durationMs
|
|
1566
|
+
durationMs,
|
|
1553
1567
|
},
|
|
1554
1568
|
});
|
|
1555
1569
|
// The upload failed — save the buffered recording to disk so it isn't lost.
|
|
@@ -1574,7 +1588,13 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1574
1588
|
function pause(message: SimpleMessage): { ok: boolean } {
|
|
1575
1589
|
const recording = activeRecording;
|
|
1576
1590
|
if (recording && recording.sessionId === message.sessionId) {
|
|
1577
|
-
if (recording.recorder.state === "recording")
|
|
1591
|
+
if (recording.recorder.state === "recording") {
|
|
1592
|
+
recording.recorder.pause();
|
|
1593
|
+
recording.duration = pauseRecordingDuration(
|
|
1594
|
+
recording.duration,
|
|
1595
|
+
Date.now(),
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1578
1598
|
reportStatus(recording.sessionId, "paused", {
|
|
1579
1599
|
recordingId: recording.recordingId,
|
|
1580
1600
|
});
|
|
@@ -1585,7 +1605,13 @@ function pause(message: SimpleMessage): { ok: boolean } {
|
|
|
1585
1605
|
function resume(message: SimpleMessage): { ok: boolean } {
|
|
1586
1606
|
const recording = activeRecording;
|
|
1587
1607
|
if (recording && recording.sessionId === message.sessionId) {
|
|
1588
|
-
if (recording.recorder.state === "paused")
|
|
1608
|
+
if (recording.recorder.state === "paused") {
|
|
1609
|
+
recording.recorder.resume();
|
|
1610
|
+
recording.duration = resumeRecordingDuration(
|
|
1611
|
+
recording.duration,
|
|
1612
|
+
Date.now(),
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1589
1615
|
reportStatus(recording.sessionId, "recording", {
|
|
1590
1616
|
recordingId: recording.recordingId,
|
|
1591
1617
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type RecordingDurationState = {
|
|
2
|
+
elapsedMs: number;
|
|
3
|
+
activeSinceMs: number | null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function createRecordingDurationState(): RecordingDurationState {
|
|
7
|
+
return { elapsedMs: 0, activeSinceMs: null };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function startRecordingDuration(nowMs: number): RecordingDurationState {
|
|
11
|
+
return { elapsedMs: 0, activeSinceMs: nowMs };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function pauseRecordingDuration(
|
|
15
|
+
state: RecordingDurationState,
|
|
16
|
+
nowMs: number,
|
|
17
|
+
): RecordingDurationState {
|
|
18
|
+
if (state.activeSinceMs === null) return state;
|
|
19
|
+
return {
|
|
20
|
+
elapsedMs:
|
|
21
|
+
state.elapsedMs + Math.max(0, Math.round(nowMs - state.activeSinceMs)),
|
|
22
|
+
activeSinceMs: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resumeRecordingDuration(
|
|
27
|
+
state: RecordingDurationState,
|
|
28
|
+
nowMs: number,
|
|
29
|
+
): RecordingDurationState {
|
|
30
|
+
if (state.activeSinceMs !== null) return state;
|
|
31
|
+
return { ...state, activeSinceMs: nowMs };
|
|
32
|
+
}
|
|
@@ -4,11 +4,19 @@ import path from "path";
|
|
|
4
4
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
5
5
|
|
|
6
6
|
import { tenantUploadDir } from "../server/lib/tenant-files.js";
|
|
7
|
+
import { readUploadedReferenceBlob } from "../server/lib/uploaded-reference-storage.js";
|
|
7
8
|
|
|
8
|
-
export function
|
|
9
|
+
export async function readUserUploadedFile(
|
|
10
|
+
filePath: string,
|
|
11
|
+
): Promise<{ data: Buffer; filename: string }> {
|
|
9
12
|
const email = getRequestUserEmail();
|
|
10
13
|
if (!email) throw new Error("no authenticated user");
|
|
11
14
|
|
|
15
|
+
const privateUpload = await readUploadedReferenceBlob(filePath, email);
|
|
16
|
+
if (privateUpload) {
|
|
17
|
+
return privateUpload;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
const allowedDir = tenantUploadDir(email);
|
|
13
21
|
const absPath = path.isAbsolute(filePath)
|
|
14
22
|
? filePath
|
|
@@ -23,5 +31,8 @@ export function resolveUserUploadedFile(filePath: string): string {
|
|
|
23
31
|
if (!fs.existsSync(resolved)) {
|
|
24
32
|
throw new Error(`File not found: ${filePath}`);
|
|
25
33
|
}
|
|
26
|
-
return
|
|
34
|
+
return {
|
|
35
|
+
data: await fs.promises.readFile(resolved),
|
|
36
|
+
filename: path.basename(resolved),
|
|
37
|
+
};
|
|
27
38
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
|
|
3
1
|
import { defineAction } from "@agent-native/core";
|
|
4
2
|
import { z } from "zod";
|
|
5
3
|
|
|
6
4
|
import { parseDocx } from "../server/handlers/import/docx-parser.js";
|
|
7
5
|
import { convertSectionsToSlides } from "../server/handlers/import/html-converter.js";
|
|
8
|
-
import {
|
|
6
|
+
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
9
7
|
|
|
10
8
|
export default defineAction({
|
|
11
9
|
description:
|
|
@@ -16,9 +14,7 @@ export default defineAction({
|
|
|
16
14
|
schema: z.object({
|
|
17
15
|
filePath: z
|
|
18
16
|
.string()
|
|
19
|
-
.describe(
|
|
20
|
-
"Server path to the uploaded DOCX file (e.g. data/uploads/document.docx)",
|
|
21
|
-
),
|
|
17
|
+
.describe("Uploaded DOCX path or opaque hosted upload reference"),
|
|
22
18
|
convertToSlides: z
|
|
23
19
|
.boolean()
|
|
24
20
|
.optional()
|
|
@@ -28,9 +24,7 @@ export default defineAction({
|
|
|
28
24
|
),
|
|
29
25
|
}),
|
|
30
26
|
run: async ({ filePath, convertToSlides }) => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const fileBuffer = await fs.promises.readFile(absPath);
|
|
27
|
+
const { data: fileBuffer } = await readUserUploadedFile(filePath);
|
|
34
28
|
const doc = await parseDocx(fileBuffer);
|
|
35
29
|
|
|
36
30
|
const result: Record<string, unknown> = {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
1
|
import path from "path";
|
|
3
2
|
|
|
4
3
|
import { defineAction } from "@agent-native/core";
|
|
@@ -15,7 +14,7 @@ import { z } from "zod";
|
|
|
15
14
|
import { getDb, schema } from "../server/db/index.js";
|
|
16
15
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
17
16
|
import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-system-proxy.js";
|
|
18
|
-
import {
|
|
17
|
+
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
19
18
|
|
|
20
19
|
const DEFAULT_MAX_SOURCE_CHARS = 60_000;
|
|
21
20
|
|
|
@@ -30,9 +29,7 @@ export default defineAction({
|
|
|
30
29
|
schema: z.object({
|
|
31
30
|
filePath: z
|
|
32
31
|
.string()
|
|
33
|
-
.describe(
|
|
34
|
-
"Server path to the uploaded file (e.g. data/uploads/file.pptx)",
|
|
35
|
-
),
|
|
32
|
+
.describe("Uploaded file path or opaque hosted upload reference"),
|
|
36
33
|
format: z
|
|
37
34
|
.enum(["pptx", "docx", "pdf", "fig", "auto"])
|
|
38
35
|
.optional()
|
|
@@ -60,15 +57,15 @@ export default defineAction({
|
|
|
60
57
|
),
|
|
61
58
|
}),
|
|
62
59
|
run: async ({ filePath, format, deckId, importIntoDeck, maxChars }) => {
|
|
63
|
-
const
|
|
60
|
+
const uploaded = await readUserUploadedFile(filePath);
|
|
64
61
|
const sourceLimit = maxChars ?? DEFAULT_MAX_SOURCE_CHARS;
|
|
65
|
-
|
|
66
|
-
const
|
|
62
|
+
const fileBuffer = uploaded.data;
|
|
63
|
+
const filename = uploaded.filename;
|
|
67
64
|
|
|
68
65
|
// Detect format from extension if auto
|
|
69
66
|
let detectedFormat = format;
|
|
70
67
|
if (detectedFormat === "auto") {
|
|
71
|
-
const ext = path.extname(
|
|
68
|
+
const ext = path.extname(filename).toLowerCase();
|
|
72
69
|
if (ext === ".pptx") detectedFormat = "pptx";
|
|
73
70
|
else if (ext === ".docx") detectedFormat = "docx";
|
|
74
71
|
else if (ext === ".pdf") detectedFormat = "pdf";
|
|
@@ -86,12 +83,12 @@ export default defineAction({
|
|
|
86
83
|
"Figma .fig imports start Builder design-system indexing, not slide replacements. Re-run without importIntoDeck.",
|
|
87
84
|
);
|
|
88
85
|
}
|
|
89
|
-
const title = titleFromPath(
|
|
86
|
+
const title = titleFromPath(filename);
|
|
90
87
|
const result = await startBuilderDesignSystemIndex({
|
|
91
88
|
projectName: title,
|
|
92
89
|
files: [
|
|
93
90
|
{
|
|
94
|
-
name: path.basename(
|
|
91
|
+
name: path.basename(filename),
|
|
95
92
|
data: fileBuffer,
|
|
96
93
|
mimeType: "application/octet-stream",
|
|
97
94
|
},
|
|
@@ -126,7 +123,7 @@ export default defineAction({
|
|
|
126
123
|
const { convertToSlideHtml } =
|
|
127
124
|
await import("../server/handlers/import/html-converter.js");
|
|
128
125
|
const presentation = await parsePptx(fileBuffer);
|
|
129
|
-
const title = presentation.title || titleFromPath(
|
|
126
|
+
const title = presentation.title || titleFromPath(filename);
|
|
130
127
|
|
|
131
128
|
if (importIntoDeck) {
|
|
132
129
|
if (!deckId) throw new Error("deckId is required to import into deck");
|
|
@@ -172,7 +169,7 @@ export default defineAction({
|
|
|
172
169
|
await import("../server/handlers/import/html-converter.js");
|
|
173
170
|
const doc = await parseDocx(fileBuffer);
|
|
174
171
|
const slideHtmlArray = convertSectionsToSlides(doc.sections);
|
|
175
|
-
const title = doc.title || titleFromPath(
|
|
172
|
+
const title = doc.title || titleFromPath(filename);
|
|
176
173
|
|
|
177
174
|
if (importIntoDeck) {
|
|
178
175
|
if (!deckId) throw new Error("deckId is required to import into deck");
|
|
@@ -221,7 +218,7 @@ export default defineAction({
|
|
|
221
218
|
const result = await pdf.getText();
|
|
222
219
|
const pages = normalizePdfPages(result);
|
|
223
220
|
const textPages = pages.filter((p) => p.text.trim());
|
|
224
|
-
const title = titleFromPath(
|
|
221
|
+
const title = titleFromPath(filename);
|
|
225
222
|
|
|
226
223
|
if (textPages.length === 0) {
|
|
227
224
|
throw new Error(
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
|
|
3
1
|
import { defineAction } from "@agent-native/core";
|
|
4
2
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
5
3
|
import {
|
|
@@ -15,7 +13,7 @@ import { notifyClients } from "../server/handlers/decks.js";
|
|
|
15
13
|
import { convertToSlideHtml } from "../server/handlers/import/html-converter.js";
|
|
16
14
|
import { parsePptx } from "../server/handlers/import/pptx-parser.js";
|
|
17
15
|
import { getDeckUrl } from "./_app-url.js";
|
|
18
|
-
import {
|
|
16
|
+
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
19
17
|
|
|
20
18
|
export default defineAction({
|
|
21
19
|
description:
|
|
@@ -26,9 +24,7 @@ export default defineAction({
|
|
|
26
24
|
schema: z.object({
|
|
27
25
|
filePath: z
|
|
28
26
|
.string()
|
|
29
|
-
.describe(
|
|
30
|
-
"Server path to the uploaded PPTX file (e.g. data/uploads/presentation.pptx)",
|
|
31
|
-
),
|
|
27
|
+
.describe("Uploaded PPTX path or opaque hosted upload reference"),
|
|
32
28
|
deckId: z
|
|
33
29
|
.string()
|
|
34
30
|
.optional()
|
|
@@ -43,9 +39,7 @@ export default defineAction({
|
|
|
43
39
|
),
|
|
44
40
|
}),
|
|
45
41
|
run: async ({ filePath, deckId, title }) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const fileBuffer = await fs.promises.readFile(absPath);
|
|
42
|
+
const { data: fileBuffer } = await readUserUploadedFile(filePath);
|
|
49
43
|
const presentation = await parsePptx(fileBuffer);
|
|
50
44
|
|
|
51
45
|
const deckTitle = title || presentation.title || "Imported Presentation";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
|
-
import { getSession } from "@agent-native/core/server";
|
|
5
4
|
import {
|
|
6
5
|
defineEventHandler,
|
|
7
6
|
setResponseStatus,
|
|
@@ -14,7 +13,15 @@ import {
|
|
|
14
13
|
isSlidesReferenceFileExtension,
|
|
15
14
|
} from "../../shared/upload-types";
|
|
16
15
|
import { tenantUploadDir } from "../lib/tenant-files.js";
|
|
16
|
+
import {
|
|
17
|
+
isHostedSlidesRuntime,
|
|
18
|
+
storeUploadedReferenceBlob,
|
|
19
|
+
} from "../lib/uploaded-reference-storage.js";
|
|
17
20
|
import { canSaveAsUploadedAsset, uploadImageAsset } from "./assets.js";
|
|
21
|
+
import {
|
|
22
|
+
resolveSlidesRequestAuthContext,
|
|
23
|
+
withSlidesRequestContext,
|
|
24
|
+
} from "./request-auth-context.js";
|
|
18
25
|
|
|
19
26
|
export const MAX_REFERENCE_FILE_BYTES = 50 * 1024 * 1024;
|
|
20
27
|
export const MAX_FIG_REFERENCE_FILE_BYTES = 200 * 1024 * 1024;
|
|
@@ -113,6 +120,7 @@ function pathForAgent(absPath: string): string {
|
|
|
113
120
|
|
|
114
121
|
export async function saveUploadedReferenceFile(args: {
|
|
115
122
|
email: string;
|
|
123
|
+
orgId?: string | null;
|
|
116
124
|
originalName: string;
|
|
117
125
|
data: Uint8Array;
|
|
118
126
|
type?: string;
|
|
@@ -127,14 +135,43 @@ export async function saveUploadedReferenceFile(args: {
|
|
|
127
135
|
if (!hasExpectedSignature(ext, args.data)) {
|
|
128
136
|
throw new Error(`File contents do not match ${ext} upload type`);
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
let uploadedPath: string;
|
|
139
|
+
if (isHostedSlidesRuntime()) {
|
|
140
|
+
let reference: string | null;
|
|
141
|
+
try {
|
|
142
|
+
reference = await storeUploadedReferenceBlob({
|
|
143
|
+
email: args.email,
|
|
144
|
+
orgId: args.orgId,
|
|
145
|
+
data: args.data,
|
|
146
|
+
filename,
|
|
147
|
+
mimeType: args.type || "application/octet-stream",
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
throw Object.assign(
|
|
151
|
+
new Error("Private file storage failed while saving the upload."),
|
|
152
|
+
{ statusCode: 503 },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (!reference) {
|
|
156
|
+
throw Object.assign(
|
|
157
|
+
new Error(
|
|
158
|
+
"Private file storage is not configured. Connect Builder.io or another file provider before uploading reference files in a hosted Slides deployment.",
|
|
159
|
+
),
|
|
160
|
+
{ statusCode: 503 },
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
uploadedPath = reference;
|
|
164
|
+
} else {
|
|
165
|
+
const uploadDir = tenantUploadDir(args.email);
|
|
166
|
+
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
167
|
+
const destPath = path.join(uploadDir, filename);
|
|
168
|
+
await fs.promises.writeFile(destPath, args.data);
|
|
169
|
+
uploadedPath = pathForAgent(destPath);
|
|
170
|
+
}
|
|
171
|
+
// For images, also push to the public file-upload provider so the agent can
|
|
172
|
+
// embed a hosted URL (in slide HTML, chat replies, etc.). The `path` above
|
|
173
|
+
// remains the private import source: a tenant path locally and an encrypted,
|
|
174
|
+
// owner-scoped blob reference in hosted deployments.
|
|
138
175
|
let url: string | undefined;
|
|
139
176
|
if (
|
|
140
177
|
canSaveAsUploadedAsset({
|
|
@@ -159,7 +196,7 @@ export async function saveUploadedReferenceFile(args: {
|
|
|
159
196
|
}
|
|
160
197
|
}
|
|
161
198
|
return {
|
|
162
|
-
path:
|
|
199
|
+
path: uploadedPath,
|
|
163
200
|
url,
|
|
164
201
|
originalName: args.originalName,
|
|
165
202
|
filename,
|
|
@@ -170,54 +207,67 @@ export async function saveUploadedReferenceFile(args: {
|
|
|
170
207
|
|
|
171
208
|
// Upload one or more files
|
|
172
209
|
export const uploadFiles = defineEventHandler(async (event) => {
|
|
173
|
-
const
|
|
174
|
-
|
|
210
|
+
const authContext = await resolveSlidesRequestAuthContext(event);
|
|
211
|
+
const email = authContext.email;
|
|
212
|
+
if (!email) {
|
|
175
213
|
setResponseStatus(event, 401);
|
|
176
214
|
return { error: "Unauthorized" };
|
|
177
215
|
}
|
|
178
216
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
217
|
+
return withSlidesRequestContext(
|
|
218
|
+
event,
|
|
219
|
+
async ({ orgId }) => {
|
|
220
|
+
const parts = await readMultipartFormData(event);
|
|
221
|
+
const fileParts =
|
|
222
|
+
parts?.filter(
|
|
223
|
+
(p) => (p.name === "files" || p.name === "file") && p.data,
|
|
224
|
+
) ?? [];
|
|
183
225
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
if (fileParts.length === 0) {
|
|
227
|
+
setResponseStatus(event, 400);
|
|
228
|
+
return { error: "No files uploaded" };
|
|
229
|
+
}
|
|
188
230
|
|
|
189
|
-
|
|
231
|
+
const MAX_FILES = 20;
|
|
190
232
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
233
|
+
if (fileParts.length > MAX_FILES) {
|
|
234
|
+
setResponseStatus(event, 413);
|
|
235
|
+
return { error: `Too many files (max ${MAX_FILES})` };
|
|
236
|
+
}
|
|
195
237
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
238
|
+
const oversized = fileParts.find(
|
|
239
|
+
(p) => p.data.length > maxReferenceFileBytes(p.filename),
|
|
240
|
+
);
|
|
241
|
+
if (oversized) {
|
|
242
|
+
const limit = maxReferenceFileBytes(oversized.filename);
|
|
243
|
+
setResponseStatus(event, 413);
|
|
244
|
+
return { error: `File too large (max ${formatMaxFileSize(limit)})` };
|
|
245
|
+
}
|
|
204
246
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
247
|
+
let results;
|
|
248
|
+
try {
|
|
249
|
+
results = await Promise.all(
|
|
250
|
+
fileParts.map(async (part) => {
|
|
251
|
+
return saveUploadedReferenceFile({
|
|
252
|
+
email,
|
|
253
|
+
orgId,
|
|
254
|
+
originalName: part.filename || "upload",
|
|
255
|
+
data: part.data,
|
|
256
|
+
type: part.type,
|
|
257
|
+
});
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
const statusCode =
|
|
262
|
+
typeof (err as { statusCode?: unknown })?.statusCode === "number"
|
|
263
|
+
? (err as { statusCode: number }).statusCode
|
|
264
|
+
: 400;
|
|
265
|
+
setResponseStatus(event, statusCode);
|
|
266
|
+
return { error: err instanceof Error ? err.message : "Invalid upload" };
|
|
267
|
+
}
|
|
221
268
|
|
|
222
|
-
|
|
269
|
+
return results;
|
|
270
|
+
},
|
|
271
|
+
authContext,
|
|
272
|
+
);
|
|
223
273
|
});
|