Commit a50a9b92 by Jon Committed by GitHub

feat: Add new skill error handling and localization messages (#6761)

* feat: Add new skill error handling and localization messages

* feat(agentSkills): improve error codes and HTTP status handling
parent df8f0d56
......@@ -4,7 +4,24 @@ import { i18nT } from '../../../../web/i18n/utils';
export enum SkillErrEnum {
unExist = 'skillUnExist',
unAuthSkill = 'unAuthSkill',
canNotEditAdminPermission = 'canNotEditAdminPermission'
canNotEditAdminPermission = 'canNotEditAdminPermission',
skillNameExists = 'skillNameExists',
invalidSkillName = 'invalidSkillName',
invalidDescription = 'invalidDescription',
invalidCategory = 'invalidCategory',
invalidConfig = 'invalidConfig',
missingModel = 'missingModel',
requirementsTooLong = 'requirementsTooLong',
noStorage = 'noStorage',
noFieldsToUpdate = 'noFieldsToUpdate',
invalidArchiveFormat = 'invalidArchiveFormat',
invalidSkillPackage = 'invalidSkillPackage',
invalidSkillId = 'invalidSkillId',
archiveEmpty = 'archiveEmpty',
archiveExtractionFailed = 'archiveExtractionFailed',
archiveTooLarge = 'archiveTooLarge',
missingImageRepository = 'missingImageRepository',
skillNameTooLong = 'skillNameTooLong'
}
const skillErrList = [
{
......@@ -18,6 +35,90 @@ const skillErrList = [
{
statusText: SkillErrEnum.canNotEditAdminPermission,
message: i18nT('common:code_error.skill_error.can_not_edit_admin_permission')
},
{
statusText: SkillErrEnum.skillNameExists,
message: i18nT('common:code_error.skill_error.name_exists'),
httpStatus: 409
},
{
statusText: SkillErrEnum.invalidSkillName,
message: i18nT('common:code_error.skill_error.invalid_name'),
httpStatus: 400
},
{
statusText: SkillErrEnum.invalidDescription,
message: i18nT('common:code_error.skill_error.invalid_description'),
httpStatus: 400
},
{
statusText: SkillErrEnum.invalidCategory,
message: i18nT('common:code_error.skill_error.invalid_category'),
httpStatus: 400
},
{
statusText: SkillErrEnum.invalidConfig,
message: i18nT('common:code_error.skill_error.invalid_config'),
httpStatus: 400
},
{
statusText: SkillErrEnum.missingModel,
message: i18nT('common:code_error.skill_error.missing_model'),
httpStatus: 400
},
{
statusText: SkillErrEnum.requirementsTooLong,
message: i18nT('common:code_error.skill_error.requirements_too_long'),
httpStatus: 400
},
{
statusText: SkillErrEnum.noStorage,
message: i18nT('common:code_error.skill_error.no_storage')
},
{
statusText: SkillErrEnum.noFieldsToUpdate,
message: i18nT('common:code_error.skill_error.no_fields_to_update'),
httpStatus: 400
},
{
statusText: SkillErrEnum.invalidArchiveFormat,
message: i18nT('common:code_error.skill_error.invalid_archive_format'),
httpStatus: 400
},
{
statusText: SkillErrEnum.invalidSkillPackage,
message: i18nT('common:code_error.skill_error.invalid_package'),
httpStatus: 400
},
{
statusText: SkillErrEnum.invalidSkillId,
message: i18nT('common:code_error.skill_error.invalid_skill_id'),
httpStatus: 400
},
{
statusText: SkillErrEnum.archiveEmpty,
message: i18nT('common:code_error.skill_error.archive_empty'),
httpStatus: 400
},
{
statusText: SkillErrEnum.archiveExtractionFailed,
message: i18nT('common:code_error.skill_error.archive_extraction_failed'),
httpStatus: 400
},
{
statusText: SkillErrEnum.archiveTooLarge,
message: i18nT('common:code_error.skill_error.archive_too_large'),
httpStatus: 400
},
{
statusText: SkillErrEnum.missingImageRepository,
message: i18nT('common:code_error.skill_error.missing_image_repository'),
httpStatus: 400
},
{
statusText: SkillErrEnum.skillNameTooLong,
message: i18nT('common:code_error.skill_error.skill_name_too_long'),
httpStatus: 400
}
];
export default skillErrList.reduce((acc, cur, index) => {
......@@ -27,7 +128,8 @@ export default skillErrList.reduce((acc, cur, index) => {
code: 509000 + index,
statusText: cur.statusText,
message: cur.message,
data: null
data: null,
...(cur.httpStatus !== undefined ? { httpStatus: cur.httpStatus } : {})
}
};
}, {} as ErrType<`${SkillErrEnum}`>);
......@@ -55,6 +55,7 @@ export type ErrType<T> = Record<
statusText: T;
message: string;
data: null;
httpStatus?: number;
}
>;
......@@ -65,6 +66,7 @@ export const ERROR_RESPONSE: Record<
statusText: string;
message: string;
data?: any;
httpStatus?: number;
}
> = {
[ERROR_ENUM.unAuthorization]: {
......
......@@ -88,7 +88,9 @@ export const LogCategories = {
}),
AGENT_SKILLS: Object.assign(['agent-skills'], {
CREATION: ['agent-skills', 'create-skill'],
EXPORT: ['agent-skills', 'export-skill']
EXPORT: ['agent-skills', 'export-skill'],
DEPLOY: ['agent-skills', 'deploy-skill'],
IMPORT: ['agent-skills', 'import-skill']
}),
USER: Object.assign(['user'], {
ACCOUNT: ['user', 'account'],
......
......@@ -21,6 +21,7 @@ export interface ProcessedError {
statusText: string;
message: string;
shouldClearCookie: boolean;
httpStatus: number;
data?: any;
zodError?: any;
}
......@@ -90,6 +91,7 @@ export function processError(params: {
statusText: ERROR_RESPONSE[errResponseKey].statusText || 'error',
message: ERROR_RESPONSE[errResponseKey].message,
data: ERROR_RESPONSE[errResponseKey].data,
httpStatus: ERROR_RESPONSE[errResponseKey].httpStatus ?? 500,
shouldClearCookie
};
}
......@@ -127,6 +129,7 @@ export function processError(params: {
statusText: 'error',
message: replaceSensitiveText(msg),
shouldClearCookie: false,
httpStatus: defaultCode,
zodError
};
}
......
......@@ -12,6 +12,7 @@ import { createVersion } from './version/controller';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { getLogger, LogCategories } from '../../common/logger';
import { deleteSkillRelatedSandboxes } from './sandboxController';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
const logger = getLogger(LogCategories.MODULE.AGENT_SKILLS.CREATION);
......@@ -197,7 +198,7 @@ export async function importSkill(
// Check for duplicate name before creating
const nameExists = await checkSkillNameExists(skill.name, teamId, parentId || null);
if (nameExists) {
throw new Error('Skill with this name already exists');
throw SkillErrEnum.skillNameExists;
}
// Create skill record first
......
......@@ -139,6 +139,23 @@
"code_error.skill_error.not_exist": "Skill Does Not Exist",
"code_error.skill_error.un_auth_skill": "Unauthorized to Operate This Skill",
"code_error.skill_error.can_not_edit_admin_permission": "Can not edit admin permission",
"code_error.skill_error.name_exists": "Skill name already exists in this directory",
"code_error.skill_error.invalid_name": "Skill name must be a non-empty string",
"code_error.skill_error.skill_name_too_long": "Skill name must be 50 characters or fewer",
"code_error.skill_error.invalid_description": "Description must be less than 500 characters",
"code_error.skill_error.invalid_category": "Invalid category value",
"code_error.skill_error.invalid_config": "Config exceeds maximum allowed size (50KB)",
"code_error.skill_error.missing_model": "Model is required when requirements is provided",
"code_error.skill_error.requirements_too_long": "Requirements must be less than 8000 characters",
"code_error.skill_error.no_storage": "Skill has no storage, cannot copy",
"code_error.skill_error.no_fields_to_update": "No fields to update",
"code_error.skill_error.invalid_archive_format": "Only ZIP, TAR, and TAR.GZ files are supported",
"code_error.skill_error.invalid_package": "Invalid skill package structure",
"code_error.skill_error.invalid_skill_id": "Invalid skill ID",
"code_error.skill_error.archive_empty": "Archive is empty",
"code_error.skill_error.archive_extraction_failed": "Failed to extract archive",
"code_error.skill_error.archive_too_large": "Archive file size exceeds the maximum allowed limit",
"code_error.skill_error.missing_image_repository": "image.repository is required when image is provided",
"code_error.chat_error.un_auth": "Unauthorized to Operate This Chat Record",
"code_error.chat_error.chat_generating": "This chat is still generating. Please wait until it finishes before sending another request.",
"code_error.error_code.400": "Request Failed",
......
......@@ -139,6 +139,23 @@
"code_error.skill_error.not_exist": "技能不存在",
"code_error.skill_error.un_auth_skill": "无权操作该技能",
"code_error.skill_error.can_not_edit_admin_permission": "不能编辑管理员权限",
"code_error.skill_error.name_exists": "该目录下已存在同名技能",
"code_error.skill_error.invalid_name": "技能名称不能为空",
"code_error.skill_error.skill_name_too_long": "技能名称不能超过 50 字符",
"code_error.skill_error.invalid_description": "描述不能超过 500 字符",
"code_error.skill_error.invalid_category": "无效的技能分类",
"code_error.skill_error.invalid_config": "配置超过最大限制(50KB)",
"code_error.skill_error.missing_model": "提供需求描述时必须指定 model",
"code_error.skill_error.requirements_too_long": "需求描述不能超过 8000 字符",
"code_error.skill_error.no_storage": "技能没有存储,无法复制",
"code_error.skill_error.no_fields_to_update": "没有需要更新的字段",
"code_error.skill_error.invalid_archive_format": "仅支持 ZIP、TAR、TAR.GZ 格式",
"code_error.skill_error.invalid_package": "无效的技能包结构",
"code_error.skill_error.invalid_skill_id": "无效的技能 ID",
"code_error.skill_error.archive_empty": "压缩包内容为空",
"code_error.skill_error.archive_extraction_failed": "压缩包解压失败",
"code_error.skill_error.archive_too_large": "压缩包大小超过最大限制",
"code_error.skill_error.missing_image_repository": "提供 image 时必须指定 image.repository",
"code_error.chat_error.un_auth": "没有权限操作此对话记录",
"code_error.chat_error.chat_generating": "当前对话正在生成中,请等待完成后再发起新的请求",
"code_error.error_code.400": "请求失败",
......
......@@ -138,6 +138,23 @@
"code_error.skill_error.not_exist": "技能不存在",
"code_error.skill_error.un_auth_skill": "無權操作該技能",
"code_error.skill_error.can_not_edit_admin_permission": "不能編輯管理員權限",
"code_error.skill_error.name_exists": "該目錄下已存在同名技能",
"code_error.skill_error.invalid_name": "技能名稱不能為空",
"code_error.skill_error.skill_name_too_long": "技能名稱不能超過 50 字元",
"code_error.skill_error.invalid_description": "描述不能超過 500 字元",
"code_error.skill_error.invalid_category": "無效的技能分類",
"code_error.skill_error.invalid_config": "配置超過最大限制(50KB)",
"code_error.skill_error.missing_model": "提供需求描述時必須指定 model",
"code_error.skill_error.requirements_too_long": "需求描述不能超過 8000 字元",
"code_error.skill_error.no_storage": "技能沒有存儲,無法複製",
"code_error.skill_error.no_fields_to_update": "沒有需要更新的欄位",
"code_error.skill_error.invalid_archive_format": "僅支援 ZIP、TAR、TAR.GZ 格式",
"code_error.skill_error.invalid_package": "無效的技能包結構",
"code_error.skill_error.invalid_skill_id": "無效的技能 ID",
"code_error.skill_error.archive_empty": "壓縮包內容為空",
"code_error.skill_error.archive_extraction_failed": "壓縮包解壓失敗",
"code_error.skill_error.archive_too_large": "壓縮包大小超過最大限制",
"code_error.skill_error.missing_image_repository": "提供 image 時必須指定 image.repository",
"code_error.chat_error.un_auth": "沒有權限操作此對話記錄",
"code_error.chat_error.chat_generating": "目前對話仍在生成中,請等待完成後再發起新的請求",
"code_error.error_code.400": "請求失敗",
......
......@@ -18,6 +18,7 @@ import type {
ListAppsBySkillIdQuery,
AppsBySkillIdItem
} from '@fastgpt/global/core/agentSkills/api';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
async function handler(
req: ApiRequestProps<unknown, ListAppsBySkillIdQuery>
......@@ -25,7 +26,7 @@ async function handler(
const { skillId } = req.query;
if (!skillId) {
return Promise.reject(new Error('skillId is required'));
return Promise.reject(SkillErrEnum.invalidSkillId);
}
const {
......
......@@ -18,6 +18,7 @@ import { copyAvatarImage } from '@fastgpt/service/common/file/image/controller';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { CopySkillBody, CopySkillResponse } from '@fastgpt/global/core/agentSkills/api';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
async function handler(req: ApiRequestProps<CopySkillBody>): Promise<CopySkillResponse> {
const { skillId } = req.body;
......@@ -51,7 +52,7 @@ async function handler(req: ApiRequestProps<CopySkillBody>): Promise<CopySkillRe
const copyName = `${skill.name} Copy`;
if (!skill.currentStorage) {
return Promise.reject(new Error('Skill has no storage, cannot copy'));
return Promise.reject(SkillErrEnum.noStorage);
}
// 4. Transaction: copy avatar → create skill record → copy MinIO package → create version → write owner record
......
......@@ -32,6 +32,7 @@ import { i18nT } from '@fastgpt/web/i18n/utils';
import { getLogger, LogCategories } from '@fastgpt/service/common/logger';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
const logger = getLogger(LogCategories.MODULE.AGENT_SKILLS.CREATION);
......@@ -65,33 +66,33 @@ async function handler(req: ApiRequestProps<CreateSkillBody>): Promise<CreateSki
// Validate required fields
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return Promise.reject({ code: 400, error: 'Skill name is required' });
return Promise.reject(SkillErrEnum.invalidSkillName);
}
if (name.length > 50) {
return Promise.reject({ code: 400, error: 'Skill name must be less than 50 characters' });
return Promise.reject(SkillErrEnum.skillNameTooLong);
}
if (description && description.length > 500) {
return Promise.reject({ code: 400, error: 'Description must be less than 500 characters' });
return Promise.reject(SkillErrEnum.invalidDescription);
}
if (requirements && !model) {
return Promise.reject({ code: 400, error: 'Model is required when requirements is provided' });
return Promise.reject(SkillErrEnum.missingModel);
}
if (requirements && requirements.length > 8000) {
return Promise.reject({ code: 400, error: 'Requirements must be less than 8000 characters' });
return Promise.reject(SkillErrEnum.requirementsTooLong);
}
const validCategories = Object.values(AgentSkillCategoryEnum) as string[];
if (category.length > 0 && category.some((c) => !validCategories.includes(c))) {
return Promise.reject({ code: 400, error: 'Invalid category value' });
return Promise.reject(SkillErrEnum.invalidCategory);
}
if (config && JSON.stringify(config).length > 50_000) {
return Promise.reject({ code: 400, error: 'Config exceeds maximum allowed size (50KB)' });
return Promise.reject(SkillErrEnum.invalidConfig);
}
// Check if skill name already exists in the same parent folder
const nameExists = await checkSkillNameExists(name.trim(), teamId, parentId || null);
if (nameExists) {
return Promise.reject({ code: 409, error: 'Skill name already exists in this directory' });
return Promise.reject(SkillErrEnum.skillNameExists);
}
// Generate SKILL.md content
......
......@@ -8,12 +8,13 @@ import { addAuditLog, getI18nSkillType } from '@fastgpt/service/support/user/aud
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { isValidObjectId } from 'mongoose';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
async function handler(req: ApiRequestProps<{}, DeleteSkillQuery>) {
const { skillId } = req.query;
if (!skillId || !isValidObjectId(skillId)) {
return Promise.reject({ code: 400, error: 'Invalid skill ID' });
return Promise.reject(SkillErrEnum.invalidSkillId);
}
const { teamId, tmbId, skill } = await authSkill({
......
......@@ -9,6 +9,7 @@ import { isValidObjectId } from 'mongoose';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
async function handler(
req: ApiRequestProps<{}, GetSkillDetailQuery>
......@@ -16,7 +17,7 @@ async function handler(
const { skillId } = req.query;
if (!skillId || !isValidObjectId(skillId)) {
return Promise.reject({ code: 400, error: 'Invalid skill ID' });
return Promise.reject(SkillErrEnum.invalidSkillId);
}
const { skill, permission } = await authSkill({
......
......@@ -8,6 +8,7 @@ import type { CreateEditDebugSandboxBody } from '@fastgpt/global/core/agentSkill
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import type { SandboxStatusItemType } from '@fastgpt/global/core/chat/type';
import { isValidObjectId } from 'mongoose';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
/**
* Create an edit-debug sandbox for a skill.
......@@ -32,13 +33,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Validate required parameters
if (!skillId) {
sseErrRes(res, new Error('skillId is required'));
sseErrRes(res, SkillErrEnum.invalidSkillId);
res.end();
return;
}
if (!isValidObjectId(skillId)) {
sseErrRes(res, new Error('Invalid skill ID format'));
sseErrRes(res, SkillErrEnum.invalidSkillId);
res.end();
return;
}
......@@ -54,7 +55,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Validate optional parameters
if (image && !image.repository) {
sseErrRes(res, new Error('image.repository is required when image is provided'));
sseErrRes(res, SkillErrEnum.missingImageRepository);
res.end();
return;
}
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { NextAPI } from '@/service/middleware/entry';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { authSkill } from '@fastgpt/service/support/permission/agentSkill/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
......@@ -22,6 +21,11 @@ import { getSkillSizeLimits } from '@fastgpt/service/core/agentSkills/sandboxCon
import fs from 'fs/promises';
import { addAuditLog, getI18nSkillType } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { getLogger, LogCategories } from '@fastgpt/service/common/logger';
const logger = getLogger(LogCategories.MODULE.AGENT_SKILLS.IMPORT);
export const config = {
api: {
......@@ -29,14 +33,10 @@ export const config = {
}
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: ApiRequestProps<ImportSkillBody>): Promise<ImportSkillResponse> {
const filepaths: string[] = [];
try {
if (req.method !== 'POST') {
return jsonRes(res, { code: 405, error: 'Method not allowed' });
}
// Read env limit before multer so both use the same value
const { maxUploadBytes: maxArchiveSize, maxUncompressedBytes } = getSkillSizeLimits();
// Convert bytes to MB for multer (multer expects MB)
......@@ -60,10 +60,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const format = getSupportedArchiveFormat(file.originalname ?? '');
if (!format) {
return jsonRes(res, {
code: 400,
error: 'Only ZIP, TAR, and TAR.GZ files are supported'
});
return Promise.reject(SkillErrEnum.invalidArchiveFormat);
}
// Authenticate user and check permission
......@@ -99,10 +96,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Check archive size (multer already enforces the limit, this is a secondary guard)
const stats = await fs.stat(file.path);
if (stats.size > maxArchiveSize) {
return jsonRes(res, {
code: 400,
error: `Archive file size (${(stats.size / 1024 / 1024).toFixed(2)}MB) exceeds maximum (${(maxArchiveSize / 1024 / 1024).toFixed(2)}MB)`
logger.warn('Archive file size exceeds maximum', {
sizeMB: (stats.size / 1024 / 1024).toFixed(2),
maxMB: (maxArchiveSize / 1024 / 1024).toFixed(2)
});
return Promise.reject(SkillErrEnum.archiveTooLarge);
}
// Extract archive to file map
......@@ -110,13 +108,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try {
fileMap = await extractToFileMap(file.path, maxUncompressedBytes);
} catch (err: any) {
return jsonRes(res, {
code: 400,
error: `Failed to extract archive: ${err.message || 'Unknown error'}`
});
logger.warn('Failed to extract archive', { error: err.message });
return Promise.reject(SkillErrEnum.archiveExtractionFailed);
}
if (Object.keys(fileMap).length === 0) {
return jsonRes(res, { code: 400, error: 'Archive is empty' });
return Promise.reject(SkillErrEnum.archiveEmpty);
}
// Derive package-level name from caller-supplied value or archive filename
......@@ -166,13 +162,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
})();
jsonRes<ImportSkillResponse>(res, { data: skillId });
} catch (err: any) {
if (err.message?.includes('already exists')) {
return jsonRes(res, { code: 409, error: err.message });
}
jsonRes(res, { code: 500, error: err });
return skillId;
} finally {
multer.clearDiskTempFiles(filepaths);
}
}
export default NextAPI(handler);
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { NextAPI } from '@/service/middleware/entry';
import { authSkill } from '@fastgpt/service/support/permission/agentSkill/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { updateCurrentStorage } from '@fastgpt/service/core/agentSkills/controller';
......@@ -28,24 +27,23 @@ import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'
import { addAuditLog, getI18nSkillType } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { isValidObjectId } from 'mongoose';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/agentSkill';
import { UserError } from '@fastgpt/global/common/error/utils';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { getLogger, LogCategories } from '@fastgpt/service/common/logger';
const logger = getLogger(LogCategories.MODULE.AGENT_SKILLS.DEPLOY);
/**
* Package and deploy a skill from sandbox, creating a new version.
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== 'POST') {
return jsonRes(res, { code: 405, error: 'Method not allowed' });
}
async function handler(
req: ApiRequestProps<SaveDeploySkillBody>
): Promise<SaveDeploySkillResponse> {
const { skillId, versionName } = req.body;
const { skillId, versionName } = req.body as SaveDeploySkillBody;
if (!skillId) {
return jsonRes(res, { code: 400, error: 'skillId is required' });
}
if (!isValidObjectId(skillId)) {
return jsonRes(res, { code: 400, error: 'Invalid skill ID format' });
if (!skillId || !isValidObjectId(skillId)) {
return Promise.reject(SkillErrEnum.invalidSkillId);
}
// Verify write permission via authSkill (replaces authUserPer + canModifySkill)
......@@ -66,10 +64,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
if (!sandboxInfo || sandboxInfo.status !== SandboxStatusEnum.running) {
return jsonRes(res, {
code: 404,
error: 'Edit sandbox not found or not running'
});
return Promise.reject(new UserError('Edit sandbox not found or not running'));
}
// Package the skill directory from the sandbox
......@@ -79,25 +74,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
providerSandboxId: sandboxInfo.metadata?.providerSandboxId ?? sandboxInfo.sandboxId
});
} catch (error: any) {
return jsonRes(res, {
code: 500,
error: `Failed to package skill directory: ${error.message || 'Unknown error'}`
});
return Promise.reject(
new UserError(`Failed to package skill directory: ${error.message || 'Unknown error'}`)
);
}
// Validate the ZIP structure
const validation = await validateZipStructure(packageBuffer);
if (!validation.valid) {
return jsonRes(res, {
code: 500,
error: `Invalid skill package structure: ${validation.error || 'Unknown error'}`
});
logger.warn('Invalid skill package structure', { error: validation.error });
return Promise.reject(SkillErrEnum.invalidSkillPackage);
}
// Extract SKILL.md from the ZIP
const extractResult = await extractSkillPackage(packageBuffer);
if (!extractResult.success || !extractResult.skillMd) {
return jsonRes(res, { code: 500, error: 'SKILL.md not found in package' });
logger.warn('SKILL.md not found or extraction failed in skill package', {
success: extractResult.success,
hasSkillMd: !!extractResult.skillMd
});
return Promise.reject(SkillErrEnum.invalidSkillPackage);
}
// Parse skill metadata from SKILL.md frontmatter
......@@ -105,10 +101,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
extractResult.skillMd
);
if (parseError || !skillMetadata) {
return jsonRes(res, {
code: 500,
error: `Failed to parse SKILL.md: ${parseError || 'Unknown error'}`
});
return Promise.reject(
new UserError(`Failed to parse SKILL.md: ${parseError || 'Unknown error'}`)
);
}
// Standardize the ZIP package (ensure the root folder is named after the skill)
......@@ -117,10 +112,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { buffer } = await standardizeSkillPackage(packageBuffer, skillMetadata.name);
standardizedPackageBuffer = buffer;
} catch (error: any) {
return jsonRes(res, {
code: 500,
error: `Failed to standardize skill package: ${error.message || 'Unknown error'}`
});
return Promise.reject(
new UserError(`Failed to standardize skill package: ${error.message || 'Unknown error'}`)
);
}
// Transaction: create version record and upload package
......@@ -136,7 +130,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
zipBuffer: standardizedPackageBuffer
});
} catch (error: any) {
throw new Error(`Failed to upload package: ${error.message || 'Unknown error'}`);
throw new UserError(`Failed to upload package: ${error.message || 'Unknown error'}`);
}
await createVersion(
......@@ -182,12 +176,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
})();
jsonRes<SaveDeploySkillResponse>(res, { data: response });
} catch (err: any) {
console.error('[API] Save-deploy skill error:', err);
jsonRes(res, {
code: 500,
error: err.message || 'Failed to save and deploy skill'
});
}
return response;
}
export default NextAPI(handler);
......@@ -37,11 +37,11 @@ async function handler(req: ApiRequestProps<UpdateSkillBody>) {
req.body as UpdateSkillBody;
if (!skillId) {
return Promise.reject(SkillErrEnum.unExist);
return Promise.reject(SkillErrEnum.invalidSkillId);
}
if (!isValidObjectId(skillId)) {
return Promise.reject(SkillErrEnum.unExist);
return Promise.reject(SkillErrEnum.invalidSkillId);
}
const isMove = parentId !== undefined;
......@@ -99,10 +99,10 @@ async function handler(req: ApiRequestProps<UpdateSkillBody>) {
// Field validation for normal update
if (name !== undefined) {
if (typeof name !== 'string' || name.trim().length === 0) {
return Promise.reject({ code: 400, error: 'Skill name cannot be empty' });
return Promise.reject(SkillErrEnum.invalidSkillName);
}
if (name.length > 50) {
return Promise.reject({ code: 400, error: 'Skill name must be less than 50 characters' });
return Promise.reject(SkillErrEnum.invalidSkillName);
}
const nameExists = await checkSkillNameExists(
name.trim(),
......@@ -111,23 +111,23 @@ async function handler(req: ApiRequestProps<UpdateSkillBody>) {
skillId
);
if (nameExists) {
return Promise.reject({ code: 409, error: 'Skill name already exists' });
return Promise.reject(SkillErrEnum.skillNameExists);
}
}
if (description !== undefined && description.length > 500) {
return Promise.reject({ code: 400, error: 'Description must be less than 500 characters' });
return Promise.reject(SkillErrEnum.invalidDescription);
}
if (category !== undefined) {
const validCategories = Object.values(AgentSkillCategoryEnum) as string[];
if (category.some((c) => !validCategories.includes(c))) {
return Promise.reject({ code: 400, error: 'Invalid category value' });
return Promise.reject(SkillErrEnum.invalidCategory);
}
}
if (config !== undefined && JSON.stringify(config).length > 50_000) {
return Promise.reject({ code: 400, error: 'Config exceeds maximum allowed size (50KB)' });
return Promise.reject(SkillErrEnum.invalidConfig);
}
const updateData: Record<string, any> = {};
......@@ -138,7 +138,7 @@ async function handler(req: ApiRequestProps<UpdateSkillBody>) {
if (avatar !== undefined) updateData.avatar = avatar;
if (Object.keys(updateData).length === 0) {
return Promise.reject({ code: 400, error: 'No fields to update' });
return Promise.reject(SkillErrEnum.noFieldsToUpdate);
}
await mongoSessionRun(async (session) => {
......
......@@ -412,7 +412,7 @@ describe('AgentSkill Controller', () => {
// Second import should fail
await expect(
importSkill(packageData, testTeamId, testTmbId, testUserId, mockZipBuffer)
).rejects.toThrow('Skill with this name already exists');
).rejects.toThrow('skillNameExists');
});
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment