V4.14.9 dev (#6555)

* feat: encapsulate logger (#6535)

* feat: encapsulate logger

* update engines

---------

Co-authored-by: archer <545436317@qq.com>

* next config

* dev shell

* Agent sandbox (#6532)

* docs: switch to docs layout and apply black theme (#6533)

* feat: add Gemini 3.1 models

- Add gemini-3.1-pro-preview (released February 19, 2026)
- Add gemini-3.1-flash-lite-preview (released March 3, 2026)

Both models support:
- 1M context window
- 64k max response
- Vision
- Tool choice

* docs: switch to docs layout and apply black theme

- Change layout from notebook to docs
- Update logo to icon + text format
- Apply fumadocs black theme
- Simplify global.css (keep only navbar and TOC styles)
- Fix icon components to properly accept className props
- Add mobile text overflow handling
- Update Node engine requirement to >=20.x

* doc

* doc

* lock

* fix: ts

* doc

* doc

---------

Co-authored-by: archer <archer@archerdeMac-mini.local>
Co-authored-by: archer <545436317@qq.com>

* Doc (#6493)

* cloud doc

* doc refactor

* doc move

* seo

* remove doc

* yml

* doc

* fix: tsconfig

* fix: tsconfig

* sandbox version (#6497)

* sandbox version

* add sandbox log

* update lock

* fix

* fix: sandbox

* doc

* add console

* i18n

* sandbxo in agent

* feat: agent sandbox

* lock

* feat: sandbox ui

* sandbox check exists

* env tempalte

* doc

* lock

* sandbox in chat window

* sandbox entry

* fix: test

* rename var

* sandbox config tip

* update sandbox lifecircle

* update prompt

* rename provider test

* sandbox logger

* yml

---------

Co-authored-by: Archer <archer@fastgpt.io>
Co-authored-by: archer <archer@archerdeMac-mini.local>

* perf: sandbox error tip

* Add sandbox limit and fix some issue (#6550)

* sandbox in plan

* fix: some issue

* fix: test

* editor default path

* fix: comment

* perf: sandbox worksapce

* doc

* perf: del sandbox

* sandbox build

* fix: test

* fix: pr comment

---------

Co-authored-by: Ryo <whoeverimf5@gmail.com>
Co-authored-by: Archer <archer@fastgpt.io>
Co-authored-by: archer <archer@archerdeMac-mini.local>
This commit is contained in:
Archer
2026-03-16 17:09:25 +08:00
committed by GitHub
parent 21b3f8549a
commit aaa7d17ef1
258 changed files with 6844 additions and 6162 deletions
@@ -0,0 +1,197 @@
import {
generateSandboxId,
SandboxStatusEnum,
SANDBOX_SUSPEND_MINUTES
} from '@fastgpt/global/core/ai/sandbox/constants';
import { env } from '../../../env';
import { MongoSandboxInstance } from './schema';
import {
createSandbox,
type ExecuteResult,
type ISandbox,
type ResourceLimits
} from '@fastgpt-sdk/sandbox-adapter';
import { getLogger, LogCategories } from '../../../common/logger';
import { setCron } from '../../../common/system/cron';
import { subMinutes } from 'date-fns';
import { batchRun } from '@fastgpt/global/common/system/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
const logger = getLogger(LogCategories.MODULE.AI.SANDBOX);
type UnionIdType = {
appId: string;
userId: string;
chatId: string;
};
export class SandboxClient {
private appId?: string;
private userId?: string;
private chatId?: string;
private sandboxId: string;
readonly provider: ISandbox;
constructor(
props:
| {
sandboxId: string;
}
| UnionIdType,
opts: {
resourceLimits?: ResourceLimits;
} = {}
) {
if ('sandboxId' in props) {
this.sandboxId = props.sandboxId;
} else {
this.appId = props.appId;
this.userId = props.userId;
this.chatId = props.chatId;
this.sandboxId = generateSandboxId(this.appId, this.userId, this.chatId);
}
const providerName = env.AGENT_SANDBOX_PROVIDER;
const params = (() => {
if (providerName === 'sealosdevbox') {
if (!env.AGENT_SANDBOX_SEALOS_BASEURL || !env.AGENT_SANDBOX_SEALOS_TOKEN) {
throw new Error('AGENT_SANDBOX_SEALOS_BASEURL / AGENT_SANDBOX_SEALOS_TOKEN required');
}
return {
provider: 'sealosdevbox' as const,
config: {
baseUrl: env.AGENT_SANDBOX_SEALOS_BASEURL,
token: env.AGENT_SANDBOX_SEALOS_TOKEN,
sandboxId: this.sandboxId
},
createConfig: undefined
};
} else if (!providerName) {
throw new Error(
'AGENT_SANDBOX_PROVIDER is not configured. Please set it in your environment variables.'
);
} else {
throw new Error(`Unsupported sandbox provider: ${env.AGENT_SANDBOX_PROVIDER}`);
}
})();
this.provider = createSandbox(params.provider, params.config, params.createConfig);
}
async ensureAvailable() {
await MongoSandboxInstance.findOneAndUpdate(
{ provider: this.provider.provider, sandboxId: this.sandboxId },
{
$set: {
status: SandboxStatusEnum.running,
lastActiveAt: new Date()
},
$setOnInsert: {
...(this.appId ? { appId: this.appId } : {}),
...(this.userId ? { userId: this.userId } : {}),
...(this.chatId ? { chatId: this.chatId } : {}),
createdAt: new Date()
}
},
{ upsert: true }
);
await this.provider.ensureRunning();
}
async exec(command: string, timeout?: number): Promise<ExecuteResult> {
try {
await this.ensureAvailable();
} catch (err) {
logger.error('Failed to ensure sandbox available', { sandboxId: this.sandboxId, error: err });
return {
stdout: '',
stderr: `Sandbox service is not available: ${getErrText(err)}`,
exitCode: -1
};
}
return await this.provider
.execute(command, {
timeoutMs: timeout ? timeout * 1000 : undefined
})
.catch((err) => {
logger.error('Failed to execute sandbox', { sandboxId: this.sandboxId, error: err });
return {
stdout: '',
stderr: `Failed to execute sandbox: ${getErrText(err)}`,
exitCode: -1
};
});
}
async delete() {
await this.provider.delete();
await MongoSandboxInstance.deleteOne({ sandboxId: this.sandboxId });
}
async stop() {
await this.provider.stop();
await MongoSandboxInstance.updateOne(
{ sandboxId: this.sandboxId },
{ $set: { status: SandboxStatusEnum.stoped } }
);
}
}
// ==== Delete Sandboxes ====
export const deleteSandboxesByChatIds = async ({
appId,
chatIds
}: {
appId: string;
chatIds: string[];
}) => {
const instances = await MongoSandboxInstance.find({ appId, chatId: { $in: chatIds } }).lean();
if (!instances.length) return;
await Promise.allSettled(
instances.map((doc) =>
new SandboxClient({
sandboxId: doc.sandboxId
})
.delete()
.catch((err) => {
logger.error('Failed to delete sandbox', { sandboxId: doc.sandboxId, error: err });
})
)
);
};
export const deleteSandboxesByAppId = async (appId: string) => {
const instances = await MongoSandboxInstance.find({ appId }).lean();
if (!instances.length) return;
await Promise.allSettled(
instances.map((doc) =>
new SandboxClient({
sandboxId: doc.sandboxId
}).delete()
)
);
};
// 5 分钟检查一遍,暂停
export const cronJob = async () => {
setCron('*/5 * * * *', async () => {
const instances = await MongoSandboxInstance.find({
status: SandboxStatusEnum.running,
lastActiveAt: { $lt: subMinutes(new Date(), SANDBOX_SUSPEND_MINUTES) }
}).lean();
if (!instances.length) return;
logger.info('Found running sandboxes inactive > 5 min', { count: instances.length });
await batchRun(instances, (doc) =>
new SandboxClient({
sandboxId: doc.sandboxId
})
.stop()
.catch((error) => {
logger.error('Failed to stop sandbox', { sandboxId: doc.sandboxId, error });
})
);
});
};
@@ -0,0 +1 @@
export type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
@@ -0,0 +1,67 @@
import { connectionMongo, getMongoModel } from '../../../common/mongo';
const { Schema } = connectionMongo;
import type { SandboxInstanceSchemaType } from './type';
import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import { AppCollectionName } from '../../app/schema';
import { SandboxLimitSchema, SandboxProviderSchema } from './type';
export const collectionName = 'agent_sandbox_instances';
const SandboxInstanceSchema = new Schema({
provider: {
type: String,
enum: SandboxProviderSchema.options,
required: true
},
// 唯一 idchat 模式下,由 3 个 id hash 获取。
sandboxId: {
type: String,
required: true
},
// Chat 模式下会关联会话。Skill editor 不需要 appId,userId,chatId
appId: {
type: Schema.Types.ObjectId,
ref: AppCollectionName
},
userId: String,
chatId: String,
status: {
type: String,
enum: Object.values(SandboxStatusEnum),
default: SandboxStatusEnum.running,
required: true
},
lastActiveAt: {
type: Date,
default: () => new Date(),
required: true
},
createdAt: {
type: Date,
default: () => new Date(),
required: true
},
limit: {
type: SandboxLimitSchema.shape
}
});
SandboxInstanceSchema.index(
{ appId: 1, userId: 1, chatId: 1 },
{
unique: true,
partialFilterExpression: {
appId: { $exists: true, $ne: null },
userId: { $exists: true, $ne: null },
chatId: { $exists: true, $ne: null }
}
}
);
SandboxInstanceSchema.index({ status: 1, lastActiveAt: 1 });
SandboxInstanceSchema.index({ provider: 1, sandboxId: 1 }, { unique: true });
export const MongoSandboxInstance = getMongoModel<SandboxInstanceSchemaType>(
collectionName,
SandboxInstanceSchema
);
+26
View File
@@ -0,0 +1,26 @@
import z from 'zod';
import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
// ---- 沙盒实例 DB 类型 ----
export const SandboxProviderSchema = z.enum(['sealosdevbox']);
export type SandboxProviderType = z.infer<typeof SandboxProviderSchema>;
export const SandboxLimitSchema = z.object({
cpuCount: z.number(),
memoryMiB: z.number(),
diskGiB: z.number()
});
export const SandboxInstanceZodSchema = z.object({
_id: z.string(),
sandboxId: z.string(),
appId: z.string().nullish(),
userId: z.string().nullish(),
chatId: z.string().nullish(),
status: z.enum(SandboxStatusEnum),
lastActiveAt: z.date(),
createdAt: z.date(),
limit: SandboxLimitSchema.nullish(),
provider: SandboxProviderSchema
});
export type SandboxInstanceSchemaType = z.infer<typeof SandboxInstanceZodSchema>;