Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
d1ab0854
authored
Jun 29, 2026
by
DigHuang
Committed by
GitHub
Jun 29, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(sandbox): support ReadableStream upload and auto refresh workspace (#7209)
parent
8271ef53
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
112 additions
and
9 deletions
+112
-9
projects/app/src/pageComponents/chat/SandboxEditor/hook.tsx
+26
-2
sdk/sandbox-adapter/src/adapters/SealosDevboxAdapter/api.ts
+16
-2
sdk/sandbox-adapter/src/adapters/SealosDevboxAdapter/index.ts
+17
-5
sdk/sandbox-adapter/tests/unit/adapters/SealosDevboxAdapter.test.ts
+53
-0
No files found.
projects/app/src/pageComponents/chat/SandboxEditor/hook.tsx
View file @
d1ab0854
...
...
@@ -605,7 +605,7 @@ export const useSandboxFileStore = ({
if
(
!
hasPendingWorkspaceRefreshRef
.
current
)
{
const
filesToReload
=
openedFilesRef
.
current
?.
filter
((
f
)
=>
!
f
.
isDirty
&&
!
f
.
isLoading
&&
!
f
.
is
Binary
&&
!
f
.
is
Unknown
)
?.
filter
((
f
)
=>
!
f
.
isDirty
&&
!
f
.
isLoading
&&
!
f
.
isUnknown
)
.
map
((
f
)
=>
({
path
:
f
.
path
,
language
:
f
.
language
,
...
...
@@ -654,6 +654,14 @@ export const useSandboxFileStore = ({
item
.
content
===
snapshot
.
content
&&
item
.
etag
===
snapshot
.
etag
)
{
if
(
item
.
isBinary
&&
item
.
content
.
startsWith
(
'blob:'
)
&&
item
.
content
!==
update
.
content
)
{
URL
.
revokeObjectURL
(
item
.
content
);
}
return
{
...
item
,
content
:
update
.
content
,
...
...
@@ -1579,6 +1587,7 @@ export const useSandboxFileStore = ({
uploadTasks
.
push
({
path
,
file
});
}
let
hasUploaded
=
false
;
try
{
for
(
const
task
of
uploadTasks
)
{
await
uploadSandboxFile
({
...
...
@@ -1588,6 +1597,7 @@ export const useSandboxFileStore = ({
path
:
task
.
path
,
file
:
task
.
file
});
hasUploaded
=
true
;
}
}
catch
(
error
)
{
console
.
error
(
'Failed to upload files:'
,
error
);
...
...
@@ -1596,9 +1606,23 @@ export const useSandboxFileStore = ({
description
:
getErrText
(
error
),
status
:
'error'
});
}
finally
{
if
(
hasUploaded
)
{
await
refreshWorkspace
({
preserveExpandedDirs
:
true
});
}
}
}
,
[chatId, fileTree, maxFileBytes, maxFileSizeMB, outLinkAuthData, sandboxTarget, toast, t]
[
chatId,
fileTree,
maxFileBytes,
maxFileSizeMB,
outLinkAuthData,
refreshWorkspace,
sandboxTarget,
toast,
t
]
);
// 展开折叠目录
...
...
sdk/sandbox-adapter/src/adapters/SealosDevboxAdapter/api.ts
View file @
d1ab0854
...
...
@@ -10,6 +10,15 @@ import {
type
UploadFileParams
,
type
UploadResponseData
}
from
'./type'
;
import
{
isReadableStreamData
}
from
'@/utils/files'
;
type
StreamingRequestInit
=
RequestInit
&
{
/**
* Node fetch requires this extension when the request body is a stream.
* Browser typings do not expose it yet, but undici accepts the field.
*/
duplex
?:
'half'
;
};
export
class
DevboxApiError
extends
Error
{
constructor
(
...
...
@@ -138,11 +147,16 @@ export class DevboxApi {
if
(
params
.
timeoutSeconds
!=
null
)
queryParams
.
timeoutSeconds
=
String
(
params
.
timeoutSeconds
);
if
(
params
.
container
)
queryParams
.
container
=
params
.
container
;
return
this
.
request
(
this
.
url
(
`/api/v1/devbox/
${
name
}
/files/upload`
,
queryParams
),
{
const
requestInit
:
StreamingRequestInit
=
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/octet-stream'
},
body
:
content
});
};
if
(
isReadableStreamData
(
content
))
{
requestInit
.
duplex
=
'half'
;
}
return
this
.
request
(
this
.
url
(
`/api/v1/devbox/
${
name
}
/files/upload`
,
queryParams
),
requestInit
);
}
private
buildDownloadFileUrl
(
name
:
string
,
params
:
DownloadFileParams
):
string
{
...
...
sdk/sandbox-adapter/src/adapters/SealosDevboxAdapter/index.ts
View file @
d1ab0854
...
...
@@ -19,7 +19,7 @@ import { DevboxApi, DevboxApiError } from './api';
import
{
DevboxPhaseEnum
,
type
DevboxCreateRequest
,
type
DevboxInfoData
}
from
'./type'
;
import
{
formatImageSpec
,
parseImageSpec
}
from
'@/utils/image'
;
import
{
joinUrlPath
,
normalizePathPrefix
}
from
'@/utils/url'
;
import
{
fileDataToUint8Array
}
from
'@/utils/files'
;
import
{
fileDataToUint8Array
,
isReadableStreamData
}
from
'@/utils/files'
;
const
GET_INFO_RETRY_TIMEOUT_MS
=
30
_000
;
const
GET_INFO_RETRY_INTERVAL_MS
=
1
_000
;
...
...
@@ -319,8 +319,13 @@ export class SealosDevboxAdapter extends BaseSandboxAdapter {
for (const entry of entries) {
const normalizedPath = this.normalizePath(entry.path);
try {
const content = await fileDataToUint8Array(entry.data);
const bytesWritten = content.byteLength;
const uploadBody = await (async () => {
if (isReadableStreamData(entry.data)) {
return entry.data;
}
return fileDataToUint8Array(entry.data);
})();
let modeStr: string | undefined;
if (entry.mode !== undefined) {
...
...
@@ -333,14 +338,21 @@ export class SealosDevboxAdapter extends BaseSandboxAdapter {
path: normalizedPath,
mode: modeStr
},
content
uploadBody
);
if (res.code !== 200) {
throw new Error(res.message || `
Upload
failed
with
code
$
{
res
.
code
}
`);
}
results.push({ path: normalizedPath, bytesWritten, error: null });
const bytesWritten =
res.data?.sizeBytes ?? (uploadBody instanceof Uint8Array ? uploadBody.byteLength : 0);
results.push({
path: normalizedPath,
bytesWritten,
error: null
});
} catch (error) {
results.push({
path: normalizedPath,
...
...
sdk/sandbox-adapter/tests/unit/adapters/SealosDevboxAdapter.test.ts
View file @
d1ab0854
...
...
@@ -549,6 +549,59 @@ describe('SealosDevboxAdapter', () => {
body
:
new
TextEncoder
().
encode
(
'hello world'
)
})
);
expect
(
fetchMock
.
mock
.
calls
[
0
]?.[
1
]).
not
.
toHaveProperty
(
'duplex'
);
});
it
(
'should upload ReadableStream through SealosDevbox without buffering'
,
async
()
=>
{
const
uploadStream
=
new
ReadableStream
<
Uint8Array
>
({
start
(
controller
)
{
controller
.
enqueue
(
new
TextEncoder
().
encode
(
'streamed '
));
controller
.
enqueue
(
new
TextEncoder
().
encode
(
'content'
));
controller
.
close
();
}
});
const
fetchMock
=
vi
.
fn
(
async
()
=>
({
json
:
async
()
=>
({
code
:
200
,
message
:
'ok'
,
data
:
{
name
:
'devbox-1'
,
podName
:
'pod-1'
,
container
:
'c1'
,
path
:
'/home/devbox/workspace/stream.txt'
,
sizeBytes
:
16
,
mode
:
'0644'
,
uploadedAt
:
'2026-06-02T10:00:00Z'
,
timeoutSecond
:
300
}
})
}));
vi
.
stubGlobal
(
'fetch'
,
fetchMock
);
const
adapter
=
new
SealosDevboxAdapter
(
CONFIG
,
{
workingDir
:
'/home/devbox/workspace'
});
const
results
=
await
adapter
.
writeFiles
([
{
path
:
'stream.txt'
,
data
:
uploadStream
}
]);
expect
(
results
).
toHaveLength
(
1
);
expect
(
results
[
0
].
error
).
toBeNull
();
expect
(
results
[
0
].
bytesWritten
).
toBe
(
16
);
expect
(
results
[
0
].
path
).
toBe
(
'/home/devbox/workspace/stream.txt'
);
expect
(
fetchMock
).
toHaveBeenCalledWith
(
'https://devbox-server.example.com/api/v1/devbox/devbox-1/files/upload?path=%2Fhome%2Fdevbox%2Fworkspace%2Fstream.txt'
,
expect
.
objectContaining
({
method
:
'POST'
,
headers
:
expect
.
any
(
Headers
),
body
:
uploadStream
,
duplex
:
'half'
})
);
});
it
(
'should download file through SealosDevbox api.downloadFile'
,
async
()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment