| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- #!/usr/bin/env node
- const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
- const MASTER_KEY = process.env.XIAOSHU_MASTER_KEY || '';
- const PARSE_URL = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, '');
- const validateOnly = process.argv.includes('--validate');
- const adminGatewayCode = String.raw`
- const CLASS_LABELS = {
- _User: '用户管理', Company: '帐套管理', Profile: '员工档案', Group: '用户组', Role: '角色', Permission: '权限', Department: '组织部门',
- CourseBinding: '课程绑定', CourseAppointment: '课程预约', LessonRecord: '上课记录', DailyStudyRecord: '每日学习记录', PracticeRecord: '练习记录', MemoryPracticeRecord: '抗遗忘记录',
- VocabularyWord: '词库', CommonModel: '通用内容', ContentArticle: '文章内容', ContentPublish: '内容发布', Node: '栏目节点', NodeAuth: '节点权限', Model: '内容模型', ModelField: '模型字段',
- ExamClass: '考试班级', ExamSysQuestions: '考试题库', ExamSysPapers: '试卷', ExamType: '考试类型', ExamPoint: '知识点', PaperQuestions: '试卷题目',
- Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
- GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科', Search: '搜索记录', Agency: '代理机构', DeliveryCenter: '交付中心'
- };
- const ALLOWED_CLASSES = new Set([
- 'PageTemplate','PaperQuestions','PayPlat','Permission','PlatComp','App','DesignAnswer','DesignAsk','Attachment','DesignPage','DesignQuestion','DesignRes','Feedback','DesignScence','StudentAchieve','ContentArticle','DesignSiteInfo','Profile','AdInfo','AdZone','ARoleAuth','Baike','ExamSysQuestions','ContactInfo','VocabularyWord','AssessmentProfile','Agency','MemoryPracticeRecord','DeliveryCenter','CourseBinding','PracticeRecord','CourseAppointment','Account','SurveyItem','SurveyLog','LessonRecord','DailyStudyRecord','CommonModel','ContentPublish','Company','_Role','_User','CRMSAttr','Currency','Datadic','Datadiccategory','DesignTlp','DocModel','DocPermission','ExamClass','ExamSysPapers','ExamType','ExamPoint','ExTeacher','FontPicShape','FontPicShapeType','Grade','GradeCate','Group','GroupModel','GuestBar','Guestbook','Guestcate','MailTemp','Manager','MisProcedure','MisProLevel','MisSign','MisType','PageStyle','Model','ModelField','Node','NodeAuth','NodeModelTemplate','Product','PlatUserRole','Pub','PubTw','PubWTHD','PubZXDC','PublishNode','QuestionsKnowledge','Role','StoreProduct','SafeMobile','Search','SenTask','ServiceSeat','ShopFareTlp','ShopMoneyRegular','Special','StoreApplication','StoreStyle','SysCSSManage','SysHoliday','SysLog','Temp','ThirdPlatInfo','UserCredit','UserDummyPoint','UserFriendGroup','UserLevel','UserSIcon','UserUserPoint','UserExpDomP','UserExpHis'
- ]);
- const READ_ONLY_CLASSES = new Set(['_Role','Permission','PayPlat','ThirdPlatInfo']);
- const SYSTEM_FIELDS = new Set(['objectId','createdAt','updatedAt','ACL','company','password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','adminRoleKey','isAdmin','roles','role']);
- const HIDDEN_FIELDS = new Set(['password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','apiKey','appSecret','secret','token']);
- function inputOf(request) {
- const body = request.body || {};
- return body.params && typeof body.params === 'object' ? body.params : body;
- }
- function fail(status, message) { const error = new Error(message); error.status = status; throw error; }
- function pointerId(value) { return value && (value.id || value.objectId || (value.__type === 'Pointer' && value.objectId)); }
- function escapeRegex(value) { return String(value).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); }
- function safeValue(value, depth = 0) {
- if (value == null || depth > 4) return value;
- if (Array.isArray(value)) return value.map((item) => safeValue(item, depth + 1));
- if (value instanceof Date) return value.toISOString();
- if (value && typeof value.toJSON === 'function') return safeValue(value.toJSON(), depth + 1);
- if (typeof value === 'object') {
- const output = {};
- for (const [key, item] of Object.entries(value)) {
- if (HIDDEN_FIELDS.has(key) || /(?:password|secret|sessiontoken|masterkey|privatekey)/i.test(key)) continue;
- output[key] = safeValue(item, depth + 1);
- }
- return output;
- }
- return value;
- }
- async function requireAdmin(request) {
- const current = request.user || (typeof user !== 'undefined' ? user : null);
- if (!current) fail(401, '管理员会话已失效');
- await current.fetch({ useMasterKey: true });
- const roles = Array.isArray(current.get('roles')) ? current.get('roles').map(String) : [];
- const role = String(current.get('role') || '');
- const roleKey = String(current.get('adminRoleKey') || '');
- const isSuperAdmin = roleKey === 'super-admin' || roles.includes('super-admin');
- const isAdmin = current.get('isAdmin') === true || role === 'admin' || roles.includes('admin') || isSuperAdmin;
- if (!isAdmin) fail(403, '当前账号未被授权为后台管理员');
- const company = request.company || current.get('company') || null;
- if (!company && !isSuperAdmin) fail(403, '管理员账号尚未分配帐套');
- return { current, roles, isSuperAdmin, company };
- }
- function assertClass(className) {
- if (!ALLOWED_CLASSES.has(className)) fail(400, '不允许访问该数据类');
- }
- async function schemaFor(className) {
- assertClass(className);
- const schema = await new Parse.Schema(className).get({ useMasterKey: true });
- return schema && schema.fields ? schema.fields : {};
- }
- function applyTenant(query, fields, context, requestedCompanyId) {
- if (!fields.company) return;
- if (context.isSuperAdmin && requestedCompanyId) {
- query.equalTo('company', Parse.Object.createWithoutData('Company', String(requestedCompanyId)));
- } else if (context.company) {
- query.equalTo('company', context.company);
- }
- }
- function serializeObject(object) { return safeValue(object.toJSON()); }
- function toParseValue(field, value) {
- if (value == null || value === '') return value;
- if (field.type === 'Pointer') {
- const objectId = pointerId(value) || value;
- return Parse.Object.createWithoutData(field.targetClass, String(objectId));
- }
- if (field.type === 'Date') {
- const date = new Date(value.iso || value);
- if (Number.isNaN(date.getTime())) fail(400, '日期字段格式无效');
- return date;
- }
- if (field.type === 'Number') {
- const number = Number(value);
- if (!Number.isFinite(number)) fail(400, '数字字段格式无效');
- return number;
- }
- if (field.type === 'Boolean') return value === true || value === 'true';
- return value;
- }
- async function countClass(className, context) {
- const fields = await schemaFor(className);
- const query = new Parse.Query(className);
- applyTenant(query, fields, context);
- if (fields.isDeleted) query.notEqualTo('isDeleted', true);
- return query.count({ useMasterKey: true });
- }
- async function audit(context, action, className, objectId) {
- try {
- const log = new Parse.Object('SysLog');
- const schema = await new Parse.Schema('SysLog').get({ useMasterKey: true });
- const fields = schema.fields || {};
- if (fields.company && context.company) log.set('company', context.company);
- if (fields.userId) log.set('userId', context.current.id);
- if (fields.userName) log.set('userName', String(context.current.get('username') || ''));
- if (fields.remind) log.set('remind', '[AngularAdmin] ' + action + ' ' + className + '/' + objectId);
- if (fields.logType) log.set('logType', 'angular-admin');
- await log.save(null, { useMasterKey: true });
- } catch (_) { /* 审计表字段来自旧系统,失败不能覆盖主操作结果。 */ }
- }
- async function handler(request, response) {
- try {
- const input = inputOf(request);
- const operation = String(input.operation || 'meta');
- const context = await requireAdmin(request);
- if (operation === 'meta') {
- const functionQuery = new Parse.Query('Function');
- functionQuery.notEqualTo('enabled', false);
- const cloudFunctions = await functionQuery.count({ useMasterKey: true });
- return response.json({ success: true, data: {
- identity: { objectId: context.current.id, username: String(context.current.get('username') || ''), displayName: String(context.current.get('realName') || context.current.get('realname') || context.current.get('nickname') || context.current.get('username') || ''), company: safeValue(context.company), roles: context.roles, isSuperAdmin: context.isSuperAdmin },
- classes: ALLOWED_CLASSES.size, cloudFunctions
- }});
- }
- if (operation === 'dashboard') {
- const metrics = [
- ['_User','用户总数'],['CourseBinding','课程绑定'],['CourseAppointment','课程预约'],['PracticeRecord','练习记录'],['LessonRecord','上课记录'],['DailyStudyRecord','每日学习'],['VocabularyWord','词库单词'],['CommonModel','内容记录']
- ];
- const counts = await Promise.all(metrics.map(async ([className, label]) => ({ className, label, count: await countClass(className, context), route: '/admin/resources/' + className })));
- const functionQuery = new Parse.Query('Function'); functionQuery.notEqualTo('enabled', false);
- let registeredRecords = 195452;
- try { const row = await Psql.oneOrNone('SELECT COUNT(*)::int AS count FROM cms.record_registry'); if (row) registeredRecords = Number(row.count); } catch (_) {}
- return response.json({ success: true, data: { identity: { objectId: context.current.id, username: String(context.current.get('username') || ''), displayName: String(context.current.get('realName') || context.current.get('nickname') || context.current.get('username') || ''), company: safeValue(context.company), roles: context.roles, isSuperAdmin: context.isSuperAdmin }, metrics: counts, migration: { sourceObjects: 338, migratedClasses: 96, registeredRecords, cloudFunctions: await functionQuery.count({ useMasterKey: true }) } } });
- }
- const className = String(input.className || '');
- assertClass(className);
- const fields = await schemaFor(className);
- const classWritable = !READ_ONLY_CLASSES.has(className);
- if (operation === 'schema') {
- const fieldList = Object.entries(fields).filter(([name]) => !HIDDEN_FIELDS.has(name) && !/(?:password|secret|sessiontoken|masterkey|privatekey)/i.test(name)).map(([name, field]) => ({ name, type: field.type, targetClass: field.targetClass, required: field.required === true, writable: classWritable && !SYSTEM_FIELDS.has(name) }));
- return response.json({ success: true, data: { className, label: CLASS_LABELS[className] || className, fields: fieldList, writable: classWritable, supportsSoftDelete: Boolean(fields.isDeleted) } });
- }
- if (operation === 'list') {
- const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20));
- let query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId); if (fields.isDeleted) query.notEqualTo('isDeleted', true);
- const search = String(input.search || '').trim();
- if (search) { const searchField = ['name','title','username','realName','mobile','sourceKey'].find((name) => fields[name] && fields[name].type === 'String'); if (searchField) query.matches(searchField, escapeRegex(search), 'i'); }
- const sort = fields[input.sort] ? String(input.sort) : fields.updatedAt ? 'updatedAt' : 'createdAt'; if (input.order === 'asc') query.ascending(sort); else query.descending(sort);
- const total = await query.count({ useMasterKey: true }); query.skip((page - 1) * pageSize); query.limit(pageSize);
- const results = await query.find({ useMasterKey: true });
- return response.json({ success: true, data: { className, page, pageSize, total, results: results.map(serializeObject) } });
- }
- const objectId = String(input.objectId || '');
- if (operation === 'get') {
- if (!objectId) fail(400, '缺少 objectId'); const query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId); const object = await query.get(objectId, { useMasterKey: true });
- return response.json({ success: true, data: serializeObject(object) });
- }
- if (operation === 'save') {
- if (!classWritable) fail(403, '该系统类不允许通用编辑');
- if (className === '_User' && !objectId) fail(400, '新增用户必须走专用开户流程');
- let object;
- if (objectId) { const query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId); object = await query.get(objectId, { useMasterKey: true }); } else object = new Parse.Object(className);
- const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};
- for (const [name, value] of Object.entries(payload)) { if (!fields[name] || SYSTEM_FIELDS.has(name)) continue; if (value === null) object.unset(name); else object.set(name, toParseValue(fields[name], value)); }
- if (fields.company && context.company) object.set('company', context.company); if (fields.isDeleted && !objectId) object.set('isDeleted', false);
- await object.save(null, { useMasterKey: true }); await audit(context, objectId ? 'update' : 'create', className, object.id);
- return response.json({ success: true, data: serializeObject(object) });
- }
- if (operation === 'delete') {
- if (!classWritable || !objectId) fail(400, '该记录不允许删除'); const query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId); const object = await query.get(objectId, { useMasterKey: true });
- if (fields.isDeleted) { object.set('isDeleted', true); await object.save(null, { useMasterKey: true }); } else await object.destroy({ useMasterKey: true });
- await audit(context, 'delete', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: Boolean(fields.isDeleted) } });
- }
- if (operation === 'resetPassword') {
- if (className !== '_User' || !objectId || typeof input.newPassword !== 'string' || input.newPassword.length < 8) fail(400, '密码至少 8 位');
- const query = new Parse.Query('_User'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true }); target.setPassword(input.newPassword); if (fields.passwordResetRequired) target.set('passwordResetRequired', false); await target.save(null, { useMasterKey: true }); await audit(context, 'reset-password', '_User', objectId); return response.json({ success: true, data: { objectId } });
- }
- fail(400, '不支持的后台操作');
- } catch (error) {
- const status = Number(error.status || (error.code === 101 ? 404 : 500));
- return response.status(status).json({ success: false, error: status >= 500 ? 'cloud_function_error' : 'request_rejected', message: error.message || '云函数执行失败' });
- }
- }
- `;
- function cmsReadCode(mode) {
- return String.raw`
- const MODE = ${JSON.stringify(mode)};
- const HIDDEN = /(?:password|secret|sessiontoken|masterkey|privatekey|legacyPasswordHash|adminPassword)/i;
- function inputOf(request) { const body = request.body || {}; return body.params && typeof body.params === 'object' ? body.params : body; }
- function fail(status, message) { const error = new Error(message); error.status = status; throw error; }
- function safe(value, depth = 0) { if (value == null || depth > 4) return value; if (Array.isArray(value)) return value.map((item) => safe(item, depth + 1)); if (value && typeof value.toJSON === 'function') return safe(value.toJSON(), depth + 1); if (typeof value === 'object') { const output = {}; for (const [key,item] of Object.entries(value)) if (!HIDDEN.test(key)) output[key] = safe(item, depth + 1); return output; } return value; }
- async function auth(request) { const current = request.user || (typeof user !== 'undefined' ? user : null); if (!current) fail(401, '需要登录'); await current.fetch({ useMasterKey: true }); const roles = Array.isArray(current.get('roles')) ? current.get('roles').map(String) : []; const superAdmin = current.get('adminRoleKey') === 'super-admin' || roles.includes('super-admin'); if (!(current.get('isAdmin') === true || current.get('role') === 'admin' || roles.includes('admin') || superAdmin)) fail(403, '需要管理员权限'); const company = request.company || current.get('company') || null; if (!company && !superAdmin) fail(403, '管理员未分配帐套'); return { current, company, superAdmin }; }
- async function schema(name) { try { return (await new Parse.Schema(name).get({ useMasterKey: true })).fields || {}; } catch (_) { return {}; } }
- function tenant(query, fields, context) { if (fields.company && context.company) query.equalTo('company', context.company); if (fields.isDeleted) query.notEqualTo('isDeleted', true); }
- function stripHtml(content) { return String(content == null ? '' : content).replace(/<!--[\s\S]*?-->/g, ' ').replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ').replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/ | /gi, ' ').replace(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>').replace(/"/gi, '"').replace(/'|'/gi, "'").replace(/\s+/g, ' ').trim(); }
- function escapeRegex(value) { return String(value).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); }
- async function handler(request, response) {
- try {
- const input = inputOf(request); const context = await auth(request); const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20));
- if (MODE === 'content-normalizer') return response.json({ success: true, data: { content: stripHtml(input.content), mode: String(input.mode || 'strip') } });
- if (MODE.startsWith('user-')) {
- const userId = String(input.userId || ''); if (!userId) fail(400, '缺少 userId'); const fields = await schema('_User'); const query = new Parse.Query('_User'); tenant(query, fields, context); const target = await query.get(userId, { useMasterKey: true }); const data = safe(target);
- if (MODE === 'user-extended') { delete data.legacyUserPlat; delete data.legacyWxUser; delete data.wechat; delete data.wxapp; return response.json({ success: true, data }); }
- if (MODE === 'user-platform') return response.json({ success: true, data: { objectId: target.id, platform: safe(target.get('legacyUserPlat') || {}), company: safe(target.get('company')) } });
- return response.json({ success: true, data: { objectId: target.id, wechat: safe(target.get('legacyWxUser') || target.get('wechat') || target.get('wxapp') || {}) } });
- }
- if (MODE === 'exam-classes' || MODE === 'guest-bar') {
- const className = MODE === 'exam-classes' ? 'ExamClass' : 'GuestBar'; const fields = await schema(className); const query = new Parse.Query(className); tenant(query, fields, context); query.descending(fields.updatedAt ? 'updatedAt' : 'createdAt'); const total = await query.count({ useMasterKey: true }); query.skip((page - 1) * pageSize); query.limit(pageSize); const rows = await query.find({ useMasterKey: true }); return response.json({ success: true, data: { page, pageSize, total, results: rows.map(safe) } });
- }
- if (MODE === 'search') {
- const keyword = stripHtml(input.keyword); if (!keyword) fail(400, '请输入搜索关键词'); const fields = await schema('CommonModel'); const candidates = ['title','inputer','sourceKey','synopsis','content'].filter((name) => fields[name] && fields[name].type === 'String'); if (!candidates.length) return response.json({ success: true, data: { page, pageSize, total: 0, results: [] } });
- const pattern = escapeRegex(keyword); const queries = candidates.slice(0, 3).map((field) => { const query = new Parse.Query('CommonModel'); tenant(query, fields, context); query.matches(field, pattern, 'i'); return query; }); const query = queries.length === 1 ? queries[0] : Parse.Query.or(...queries); query.descending('updatedAt'); query.skip((page - 1) * pageSize); query.limit(pageSize); const rows = await query.find({ useMasterKey: true }); return response.json({ success: true, data: { page, pageSize, total: rows.length < pageSize ? (page - 1) * pageSize + rows.length : null, results: rows.map((row) => { const json = safe(row); if (json.content) json.content = stripHtml(json.content).slice(0, 260); return json; }) } });
- }
- fail(400, '不支持的 CMS 查询');
- } catch (error) { const status = Number(error.status || (error.code === 101 ? 404 : 500)); return response.status(status).json({ success: false, error: status >= 500 ? 'cloud_function_error' : 'request_rejected', message: error.message || '云函数执行失败' }); }
- }
- `;
- }
- const definitions = [
- { name: 'xiaoshu.admin.gateway', desc: '小树陪练 Angular 管理后台统一数据网关(权限、帐套、CRUD、统计)', path: 'xiaoshu/admin/gateway', code: adminGatewayCode, params: [{ name: 'operation', type: 'String', required: true }] },
- { name: 'cms.content-normalizer', desc: '替代 ZL_StripeHtmlTag/ZL_StripeTrimstr,保守输出纯文本', path: 'xiaoshu/cms/content-normalizer', code: cmsReadCode('content-normalizer'), params: [{ name: 'content', type: 'String', required: false }] },
- { name: 'cms.users.extended', desc: '替代 ZL_EX_UserView,返回安全用户扩展资料', path: 'xiaoshu/cms/users/extended', code: cmsReadCode('user-extended'), params: [{ name: 'userId', type: 'String', required: true }] },
- { name: 'cms.users.platform', desc: '替代 ZL_User_PlatView', path: 'xiaoshu/cms/users/platform', code: cmsReadCode('user-platform'), params: [{ name: 'userId', type: 'String', required: true }] },
- { name: 'cms.users.wechat', desc: '替代 ZL_User_WXView,过滤令牌与密钥', path: 'xiaoshu/cms/users/wechat', code: cmsReadCode('user-wechat'), params: [{ name: 'userId', type: 'String', required: true }] },
- { name: 'cms.exams.classes', desc: '替代 ZL_Exam_ClassView', path: 'xiaoshu/cms/exams/classes', code: cmsReadCode('exam-classes'), params: [{ name: 'page', type: 'Number', required: false, default: 1 }] },
- { name: 'cms.guest.bar', desc: '替代 ZL_Guest_BarView', path: 'xiaoshu/cms/guest/bar', code: cmsReadCode('guest-bar'), params: [{ name: 'page', type: 'Number', required: false, default: 1 }] },
- { name: 'cms.search', desc: '替代 ZL_SearchView,搜索规范化 CommonModel', path: 'xiaoshu/cms/search', code: cmsReadCode('search'), params: [{ name: 'keyword', type: 'String', required: true }] },
- ];
- function validateDefinition(definition) {
- if (definition.path.startsWith('/')) throw new Error(`${definition.name}: 当前执行器的全局 path 不接受前导 /`);
- const factory = new Function(`${definition.code}\nreturn typeof handler;`);
- if (factory() !== 'function') throw new Error(`${definition.name}: 未定义 handler`);
- }
- async function parseRequest(path, init = {}) {
- const response = await fetch(`${PARSE_URL}${path}`, {
- ...init,
- headers: {
- 'X-Parse-Application-Id': APP_ID,
- 'X-Parse-Master-Key': MASTER_KEY,
- 'Content-Type': 'application/json',
- ...(init.headers || {}),
- },
- });
- const payload = await response.json();
- if (!response.ok || payload.error) throw new Error(payload.error || `Parse 请求失败:${response.status}`);
- return payload;
- }
- async function upsert(definition) {
- const where = encodeURIComponent(JSON.stringify({ name: definition.name }));
- const existing = await parseRequest(`/classes/Function?where=${where}&limit=1&keys=objectId`);
- const body = {
- name: definition.name,
- desc: definition.desc,
- type: 'standalone',
- path: definition.path,
- code: definition.code.trim(),
- params: definition.params,
- paramList: definition.params,
- respType: 'json',
- respJson: { success: true, data: {} },
- enabled: true,
- version: '1.0.0',
- };
- const objectId = existing.results?.[0]?.objectId;
- if (objectId) {
- await parseRequest(`/classes/Function/${objectId}`, { method: 'PUT', body: JSON.stringify(body) });
- return { action: 'updated', objectId };
- }
- const created = await parseRequest('/classes/Function', { method: 'POST', body: JSON.stringify(body) });
- return { action: 'created', objectId: created.objectId };
- }
- for (const definition of definitions) validateDefinition(definition);
- if (validateOnly) {
- console.log(`Validated ${definitions.length} cloud function definitions.`);
- } else {
- if (!MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY;不会把 masterKey 写入项目文件。');
- for (const definition of definitions) {
- const result = await upsert(definition);
- console.log(`${result.action.padEnd(7)} ${definition.path} (${result.objectId})`);
- }
- }
|