Skip to content

Commit a2b50e8

Browse files
committed
feat: Renew sandbox expiration and support custom images
1 parent bec8016 commit a2b50e8

File tree

2 files changed

+56
-11
lines changed

2 files changed

+56
-11
lines changed

packages/service/core/agentSkills/sandboxController.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,49 @@ export async function createEditDebugSandbox(
131131
});
132132

133133
if (existingSandbox) {
134-
addLog.info('[Sandbox] Found existing edit-debug sandbox, soft deleting', {
135-
sandboxId: existingSandbox._id
134+
const now = new Date();
135+
const expiresAt = existingSandbox.sandbox.expiresAt;
136+
const isExpired = expiresAt != null && expiresAt <= now;
137+
138+
if (!isExpired) {
139+
// Reuse existing sandbox - renew expiration and return early
140+
addLog.info('[Sandbox] Found existing edit-debug sandbox (not expired), reusing', {
141+
sandboxId: existingSandbox._id,
142+
expiresAt
143+
});
144+
145+
const newExpiresAt = await renewSandboxExpiration({
146+
sandboxId: existingSandbox._id.toString(),
147+
teamId,
148+
additionalSeconds: sandboxTimeout
149+
});
150+
151+
const endpointInfo = existingSandbox.endpoint!;
152+
153+
onProgress?.({
154+
sandboxId: skillId,
155+
phase: 'ready',
156+
endpoint: endpointInfo,
157+
providerSandboxId: existingSandbox.sandbox.sandboxId,
158+
expiresAt: newExpiresAt?.toISOString()
159+
});
160+
161+
return {
162+
sandboxId: existingSandbox._id.toString(),
163+
providerSandboxId: existingSandbox.sandbox.sandboxId,
164+
endpoint: endpointInfo,
165+
status: {
166+
state: existingSandbox.sandbox.status.state,
167+
message: existingSandbox.sandbox.status.message
168+
},
169+
expiresAt: newExpiresAt
170+
};
171+
}
172+
173+
// Sandbox has expired - soft delete and recreate
174+
addLog.info('[Sandbox] Found existing edit-debug sandbox but it has expired, soft deleting', {
175+
sandboxId: existingSandbox._id,
176+
expiredAt: expiresAt
136177
});
137178

138179
existingSandbox.deleteTime = new Date();

packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/lifecycle.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {
3232
import { SandboxTypeEnum } from '@fastgpt/global/core/agentSkills/constants';
3333
import type {
3434
AgentSkillSchemaType,
35-
AgentSkillsVersionSchemaType
35+
AgentSkillsVersionSchemaType,
36+
SandboxImageConfigType
3637
} from '@fastgpt/global/core/agentSkills/type';
3738
import type { AgentSandboxContext, DeployedSkillInfo } from './types';
3839
import { getLogger, LogCategories } from '../../../../../../../common/logger';
@@ -44,6 +45,7 @@ type CreateAgentSandboxParams = {
4445
tmbId: string;
4546
sessionId: string; // chat 模式 = chatId,debug 模式 = 构造的 key,决定 MinIO 数据路径
4647
entrypoint?: string; // override default entrypoint for this request
48+
image?: SandboxImageConfigType; // override default image for this request
4749
onProgress?: (status: SandboxStatusItemType) => void; // lifecycle progress callback
4850
};
4951

@@ -102,19 +104,21 @@ function mergeSkillWithVersion(
102104
* /home/sandbox/workspace/projects/deep-research/SKILL.md
103105
* /home/sandbox/workspace/projects/science/SKILL.md
104106
*
105-
* Runs `find` inside the sandbox to locate SKILL.md files up to maxdepth 2,
107+
* Runs `find` inside the sandbox to locate SKILL.md files up to maxdepth 5,
106108
* then reads each file and parses the frontmatter for name/description.
107109
* This replaces the pre-scan approach and works with arbitrary ZIP structures.
108110
*/
109111
async function discoverSkillsInSandbox(
110112
sandbox: ISandbox,
111113
workDirectory: string
112114
): Promise<DeployedSkillInfo[]> {
113-
// search 可能存在耗时性能问题,可以引用find命令加上 maxdepth 深度目录约束
114-
const searchResults = await sandbox.search('SKILL.md', workDirectory);
115-
if (!searchResults || searchResults.length === 0) return [];
115+
// Use `find` with -maxdepth 5 to avoid deep recursion performance issues.
116+
const findResult = await sandbox.execute(
117+
`find "${workDirectory}" -name "SKILL.md" -maxdepth 5 2>/dev/null`
118+
);
119+
if (findResult.exitCode !== 0 || !findResult.stdout.trim()) return [];
116120

117-
const paths = searchResults.map((r) => r.path);
121+
const paths = findResult.stdout.trim().split('\n').filter(Boolean);
118122
const files = await sandbox.readFiles(paths);
119123

120124
const result: DeployedSkillInfo[] = [];
@@ -187,7 +191,7 @@ async function deploySkillsToSandbox(
187191
export async function createAgentSandbox(
188192
params: CreateAgentSandboxParams
189193
): Promise<AgentSandboxContext> {
190-
const { skillIds, teamId, tmbId, sessionId, entrypoint, onProgress } = params;
194+
const { skillIds, teamId, tmbId, sessionId, entrypoint, image, onProgress } = params;
191195

192196
const providerConfig = getSandboxProviderConfig();
193197
const defaults = getSandboxDefaults();
@@ -274,7 +278,7 @@ export async function createAgentSandbox(
274278
onProgress?.({ sandboxId: sessionId, phase: 'creatingContainer', isWarmStart: false });
275279
if (providerConfig.runtime === 'kubernetes') {
276280
await sandbox.create({
277-
image: defaults.defaultImage,
281+
image: image ?? defaults.defaultImage,
278282
timeout: defaults.timeout,
279283
entrypoint: [entrypoint ?? defaults.entrypoint.sessionKubernetes],
280284
env: buildDockerSyncEnv(sessionId, defaults.workDirectory, false),
@@ -289,7 +293,7 @@ export async function createAgentSandbox(
289293
} else {
290294
// Docker 模式:通过环境变量注入 SESSION_ID,sync agent 据此确定 MinIO 数据路径
291295
await sandbox.create({
292-
image: { repository: 'fastgpt-agent-sandbox', tag: 'docker' },
296+
image: image ?? defaults.defaultImage,
293297
timeout: defaults.timeout,
294298
entrypoint: [entrypoint ?? defaults.entrypoint.docker],
295299
env: buildDockerSyncEnv(sessionId, defaults.workDirectory, false),

0 commit comments

Comments
 (0)