mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-05 01:02:59 +08:00
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:
@@ -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
|
||||
},
|
||||
// 唯一 id,chat 模式下,由 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
|
||||
);
|
||||
@@ -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>;
|
||||
Reference in New Issue
Block a user