openyida 2026.9.13 → 2026.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,12 +24,13 @@
24
24
  * 3) 第三方依赖以 `window.<别名>` 形式引用(antd→window.antd、react→window.React …),
25
25
  * 这些 UMD 依赖由画布运行时依据 importedModules 白名单按需注入。
26
26
  *
27
- * 因此本地编译 = Babel 把 JSX/TS 转成 ES5 → 把 import 改写成 window 别名引用、
28
- * 把 export default 改写成画布入口 `YidaComp` → 正则抽出依赖包名。
27
+ * 因此本地编译 = Babel 把 JSX/TS 转成 JS → 把 import 改写成 window 别名引用、
28
+ * 把 export default 改写成画布入口 `YidaComp` → UglifyJS 压缩 → 正则抽出依赖包名。
29
29
  */
30
30
 
31
31
  const Babel = require('@babel/standalone');
32
32
  const globals = require('globals');
33
+ const UglifyJS = require('uglify-js');
33
34
  const {
34
35
  assertNoEmojiInArtifactName,
35
36
  assertNoEmojiInText,
@@ -877,6 +878,36 @@ function assertCanvasRuntimeParseable(runtimeCode, options = {}) {
877
878
  }
878
879
  }
879
880
 
881
+ /**
882
+ * 压缩画布运行态代码,并显式保留装配器读取的 YidaComp 入口名。
883
+ */
884
+ function minifyCanvasRuntime(runtimeCode, options = {}) {
885
+ let result;
886
+ try {
887
+ result = UglifyJS.minify(runtimeCode, {
888
+ compress: true,
889
+ mangle: {
890
+ reserved: ['YidaComp'],
891
+ },
892
+ });
893
+ } catch (error) {
894
+ result = { error };
895
+ }
896
+ if (!result || result.error || typeof result.code !== 'string' || !result.code) {
897
+ const detail = result && result.error && result.error.message
898
+ ? result.error.message
899
+ : '压缩结果为空';
900
+ throw new CliError(`Code Canvas runtimeCode 压缩失败: ${detail}`, {
901
+ code: 'OPENYIDA_CANVAS_MINIFY_FAILED',
902
+ details: {
903
+ stage: 'canvas_minify',
904
+ sourcePath: options.sourcePath || '',
905
+ },
906
+ });
907
+ }
908
+ return result.code;
909
+ }
910
+
880
911
  /**
881
912
  * 本地编译 Code Canvas 源码。
882
913
  * @param {string} source 原始 React/JSX/TSX 源码
@@ -984,11 +1015,12 @@ function compileCanvasLocal(source, options = {}) {
984
1015
  configFile: false,
985
1016
  });
986
1017
 
987
- const runtimeCode = stage2.code || '';
988
- assertNoEmojiInText(runtimeCode, {
1018
+ const unminifiedRuntimeCode = stage2.code || '';
1019
+ assertNoEmojiInText(unminifiedRuntimeCode, {
989
1020
  artifact: options.sourcePath ? options.sourcePath + ' runtime' : 'Code Canvas runtime',
990
1021
  code: 'OPENYIDA_CANVAS_SOURCE_EMOJI_FORBIDDEN',
991
1022
  });
1023
+ const runtimeCode = minifyCanvasRuntime(unminifiedRuntimeCode, options);
992
1024
  assertCanvasRuntimeParseable(runtimeCode, options);
993
1025
  assertDependencyManifestConsistent(runtimeCode, importedModules, options);
994
1026
  return {
@@ -1059,6 +1091,7 @@ module.exports = {
1059
1091
  resolveWindowAlias,
1060
1092
  shouldAllowUnsupportedBareImports,
1061
1093
  assertCanvasRuntimeParseable,
1094
+ minifyCanvasRuntime,
1062
1095
  findBareDependencyGlobalIssues,
1063
1096
  findSelfReferentialDependencyBindingIssues,
1064
1097
  findDependencyManifestIssues,
@@ -23,10 +23,30 @@ function fingerprint(value) {
23
23
  return crypto.createHash('sha256').update(normalized).digest('hex');
24
24
  }
25
25
 
26
+ function rawFingerprint(value) {
27
+ if (typeof value !== 'string' || !value) {
28
+ return '';
29
+ }
30
+ return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
31
+ }
32
+
33
+ function positiveSize(value) {
34
+ const size = Number(value);
35
+ return Number.isFinite(size) && size > 0 ? size : 0;
36
+ }
37
+
38
+ function sha256(value) {
39
+ const digest = String(value || '').toLowerCase();
40
+ return /^[a-f0-9]{64}$/.test(digest) ? digest : '';
41
+ }
42
+
26
43
  function createEmptyDisplayInfo() {
27
44
  return {
28
45
  hasYidaCodeCanvas: false,
46
+ hasCodeBundle: false,
29
47
  hasNativeJsx: false,
48
+ codeBundleCount: 0,
49
+ bundleIds: [],
30
50
  runtimeCodeBytes: 0,
31
51
  sourceCodeBytes: 0,
32
52
  compiledCodeBytes: 0,
@@ -34,6 +54,8 @@ function createEmptyDisplayInfo() {
34
54
  componentCount: 0,
35
55
  canvasRuntimeCode: '',
36
56
  canvasSourceCode: '',
57
+ canvasRuntimeSha256: '',
58
+ canvasSourceSha256: '',
37
59
  nativeCompiledCode: '',
38
60
  nativeSourceCode: '',
39
61
  };
@@ -79,12 +101,37 @@ function traverseDisplayNodes(node, info) {
79
101
 
80
102
  if (node.componentName === 'YidaCodeCanvas') {
81
103
  const props = node.props || {};
104
+ const codeBundle = props.codeBundle && typeof props.codeBundle === 'object'
105
+ ? props.codeBundle
106
+ : null;
82
107
  info.hasYidaCodeCanvas = true;
83
108
  info.componentCount++;
84
- info.canvasRuntimeCode = props.runtimeCode || '';
85
- info.canvasSourceCode = props.code || '';
109
+ if (typeof props.runtimeCode === 'string' && props.runtimeCode) {
110
+ info.canvasRuntimeCode = props.runtimeCode;
111
+ }
112
+ if (typeof props.code === 'string' && props.code) {
113
+ info.canvasSourceCode = props.code;
114
+ }
86
115
  info.runtimeCodeBytes += codeBytes(props.runtimeCode);
87
116
  info.sourceCodeBytes += codeBytes(props.code);
117
+ if (codeBundle) {
118
+ const bundleId = String(codeBundle.bundleId || '');
119
+ const runtime = codeBundle.runtime && typeof codeBundle.runtime === 'object'
120
+ ? codeBundle.runtime
121
+ : {};
122
+ const source = codeBundle.source && typeof codeBundle.source === 'object'
123
+ ? codeBundle.source
124
+ : {};
125
+ info.hasCodeBundle = true;
126
+ info.codeBundleCount++;
127
+ if (bundleId && !info.bundleIds.includes(bundleId)) {
128
+ info.bundleIds.push(bundleId);
129
+ }
130
+ info.runtimeCodeBytes += positiveSize(runtime.size);
131
+ info.sourceCodeBytes += positiveSize(source.size);
132
+ info.canvasRuntimeSha256 = sha256(runtime.sha256) || info.canvasRuntimeSha256;
133
+ info.canvasSourceSha256 = sha256(source.sha256) || info.canvasSourceSha256;
134
+ }
88
135
  addImportedModules(info.importedModules, props.importedModules);
89
136
  } else if (node.componentName === 'Jsx') {
90
137
  info.hasNativeJsx = true;
@@ -133,7 +180,9 @@ function hasExpectedDisplayComponent(info, publishMode) {
133
180
  return false;
134
181
  }
135
182
  if (publishMode === 'canvas') {
136
- return info.hasYidaCodeCanvas && info.runtimeCodeBytes > 0;
183
+ return info.hasYidaCodeCanvas && (
184
+ info.runtimeCodeBytes > 0 || !!info.canvasRuntimeSha256
185
+ );
137
186
  }
138
187
  return info.hasNativeJsx && info.compiledCodeBytes > 0;
139
188
  }
@@ -144,7 +193,10 @@ function summarizeDisplayPublishInfo(info) {
144
193
  }
145
194
  return {
146
195
  hasYidaCodeCanvas: info.hasYidaCodeCanvas,
196
+ hasCodeBundle: info.hasCodeBundle,
147
197
  hasNativeJsx: info.hasNativeJsx,
198
+ codeBundleCount: info.codeBundleCount,
199
+ bundleIds: info.bundleIds.slice(),
148
200
  runtimeCodeBytes: info.runtimeCodeBytes,
149
201
  sourceCodeBytes: info.sourceCodeBytes,
150
202
  compiledCodeBytes: info.compiledCodeBytes,
@@ -159,8 +211,13 @@ function verifyPublishedContentMatch(readbackSchema, expectedSchemaContent, publ
159
211
  const displayComponentPresent = hasExpectedDisplayComponent(readbackInfo, publishMode);
160
212
  const readbackArtifact = getPublishArtifact(readbackInfo, publishMode);
161
213
  const expectedArtifact = getPublishArtifact(expectedInfo, publishMode);
162
- const readbackFingerprint = fingerprint(readbackArtifact);
163
- const expectedFingerprint = fingerprint(expectedArtifact);
214
+ const codeBundleReadback = publishMode === 'canvas' && readbackInfo && readbackInfo.hasCodeBundle;
215
+ const readbackFingerprint = codeBundleReadback
216
+ ? readbackInfo.canvasRuntimeSha256
217
+ : fingerprint(readbackArtifact);
218
+ const expectedFingerprint = codeBundleReadback
219
+ ? rawFingerprint(expectedArtifact)
220
+ : fingerprint(expectedArtifact);
164
221
 
165
222
  return {
166
223
  readbackInfo,
@@ -180,6 +237,7 @@ function verifyPublishedContentMatch(readbackSchema, expectedSchemaContent, publ
180
237
  module.exports = {
181
238
  extractDisplayPublishInfo,
182
239
  fingerprint,
240
+ rawFingerprint,
183
241
  hasExpectedDisplayComponent,
184
242
  parseSchemaContent,
185
243
  summarizeDisplayPublishInfo,
@@ -9,6 +9,7 @@
9
9
 
10
10
  'use strict';
11
11
 
12
+ const crypto = require('crypto');
12
13
  const fs = require('fs');
13
14
  const path = require('path');
14
15
  const {
@@ -16,6 +17,7 @@ const {
16
17
  triggerLogin,
17
18
  resolveBaseUrl,
18
19
  httpGet,
20
+ httpGetCodeBundleText,
19
21
  requestWithAutoLogin,
20
22
  } = require('../core/utils');
21
23
  const { t } = require('../core/i18n');
@@ -227,6 +229,122 @@ function resolveSchemaContent(schemaResult) {
227
229
  return parseJsonObject(content);
228
230
  }
229
231
 
232
+ function validateCodeBundleDescriptor(codeBundle) {
233
+ const bundleId = String(codeBundle && codeBundle.bundleId || '');
234
+ const source = codeBundle && codeBundle.source;
235
+ const runtime = codeBundle && codeBundle.runtime;
236
+ const validSha256 = value => /^[a-f0-9]{64}$/.test(String(value || ''));
237
+ const validSize = value => Number.isFinite(Number(value)) && Number(value) > 0
238
+ && Number(value) <= 5 * 1024 * 1024;
239
+ if (!validSha256(bundleId)
240
+ || !source || !runtime
241
+ || !validSha256(source.sha256) || !validSize(source.size)
242
+ || !validSha256(runtime.sha256) || !validSize(runtime.size)
243
+ || Number(source.size) + Number(runtime.size) > 5 * 1024 * 1024) {
244
+ throw new Error('YidaCodeCanvas codeBundle 描述不合法');
245
+ }
246
+ return { bundleId, source, runtime };
247
+ }
248
+
249
+ function collectCodeBundleCanvases(content) {
250
+ const canvases = [];
251
+ function traverse(node) {
252
+ if (Array.isArray(node)) {
253
+ node.forEach(traverse);
254
+ return;
255
+ }
256
+ if (!node || typeof node !== 'object') {
257
+ return;
258
+ }
259
+ if (node.componentName === 'YidaCodeCanvas' && node.id && node.props
260
+ && node.props.codeBundle && typeof node.props.codeBundle === 'object') {
261
+ canvases.push({ props: node.props, descriptor: validateCodeBundleDescriptor(node.props.codeBundle) });
262
+ }
263
+ Object.values(node).forEach(traverse);
264
+ }
265
+ traverse(content);
266
+ return canvases;
267
+ }
268
+
269
+ async function downloadCodeBundleArtifact(appType, formUuid, bundleId, artifact, descriptor, authRef) {
270
+ let responseMetadata = null;
271
+ const expectedContentTypes = artifact === 'source'
272
+ ? ['text/plain']
273
+ : ['application/javascript', 'text/javascript', 'application/x-javascript'];
274
+ const value = await requestWithAutoLogin((auth) => httpGetCodeBundleText(
275
+ auth.baseUrl,
276
+ `/alibaba/web/${appType}/query/codeBundle/download.json`,
277
+ { formUuid, bundleId, artifact },
278
+ {
279
+ silentStatus: true,
280
+ maxBytes: 5 * 1024 * 1024,
281
+ expectedContentTypes,
282
+ onResponseMetadata: metadata => {
283
+ responseMetadata = metadata;
284
+ },
285
+ }
286
+ ), authRef);
287
+ if (typeof value !== 'string') {
288
+ const message = value && (value.errorMsg || value.message);
289
+ throw new Error(message || `下载 CodeBundle ${artifact} 失败`);
290
+ }
291
+ const size = Buffer.byteLength(value, 'utf8');
292
+ const digest = crypto.createHash('sha256').update(value, 'utf8').digest('hex');
293
+ if (size !== Number(descriptor.size) || digest !== descriptor.sha256) {
294
+ const context = responseMetadata && responseMetadata.context
295
+ ? responseMetadata.context
296
+ : `baseUrl=${authRef && authRef.baseUrl || 'unknown'}`;
297
+ const error = new Error(t('common.code_bundle_integrity_failed', artifact, context));
298
+ error.code = 'CODE_BUNDLE_INTEGRITY_MISMATCH';
299
+ error.details = {
300
+ artifact,
301
+ expectedSize: Number(descriptor.size),
302
+ actualSize: size,
303
+ expectedSha256: descriptor.sha256,
304
+ actualSha256: digest,
305
+ ...(responseMetadata || {}),
306
+ };
307
+ throw error;
308
+ }
309
+ return value;
310
+ }
311
+
312
+ async function resolveCodeBundleSchema(schemaResult, appType, formUuid, authRef) {
313
+ const content = resolveSchemaContent(schemaResult);
314
+ if (!content) {
315
+ return schemaResult;
316
+ }
317
+ const canvases = collectCodeBundleCanvases(content);
318
+ if (canvases.length === 0) {
319
+ return schemaResult;
320
+ }
321
+
322
+ const downloaded = new Map();
323
+ for (const canvas of canvases) {
324
+ const { bundleId, source, runtime } = canvas.descriptor;
325
+ let code = downloaded.get(bundleId);
326
+ if (!code) {
327
+ const [sourceCode, runtimeCode] = await Promise.all([
328
+ downloadCodeBundleArtifact(appType, formUuid, bundleId, 'source', source, authRef),
329
+ downloadCodeBundleArtifact(appType, formUuid, bundleId, 'runtime', runtime, authRef),
330
+ ]);
331
+ code = { sourceCode, runtimeCode };
332
+ downloaded.set(bundleId, code);
333
+ }
334
+ canvas.props.code = code.sourceCode;
335
+ canvas.props.runtimeCode = code.runtimeCode;
336
+ delete canvas.props.codeBundle;
337
+ }
338
+
339
+ if (schemaResult && Object.prototype.hasOwnProperty.call(schemaResult, 'content')) {
340
+ schemaResult.content = typeof schemaResult.content === 'string'
341
+ ? JSON.stringify(content)
342
+ : content;
343
+ return schemaResult;
344
+ }
345
+ return content;
346
+ }
347
+
230
348
  function addImportedModules(target, value) {
231
349
  let modules = value;
232
350
  if (typeof modules === 'string') {
@@ -269,7 +387,10 @@ function extractDisplayPageSummary(schemaResult) {
269
387
 
270
388
  const displayPage = {
271
389
  hasYidaCodeCanvas: false,
390
+ hasCodeBundle: false,
272
391
  hasNativeJsx: false,
392
+ codeBundleCount: 0,
393
+ bundleIds: [],
273
394
  runtimeCodeBytes: 0,
274
395
  sourceCodeBytes: 0,
275
396
  compiledCodeBytes: 0,
@@ -291,10 +412,29 @@ function extractDisplayPageSummary(schemaResult) {
291
412
 
292
413
  if (node.componentName === 'YidaCodeCanvas' && isComponentInstance(node)) {
293
414
  const props = node.props || {};
415
+ const codeBundle = props.codeBundle && typeof props.codeBundle === 'object'
416
+ ? props.codeBundle
417
+ : null;
294
418
  displayPage.hasYidaCodeCanvas = true;
295
419
  displayPage.componentCount++;
296
420
  displayPage.runtimeCodeBytes += codeBytes(props.runtimeCode);
297
421
  displayPage.sourceCodeBytes += codeBytes(props.code);
422
+ if (codeBundle) {
423
+ const source = codeBundle.source && typeof codeBundle.source === 'object'
424
+ ? codeBundle.source
425
+ : {};
426
+ const runtime = codeBundle.runtime && typeof codeBundle.runtime === 'object'
427
+ ? codeBundle.runtime
428
+ : {};
429
+ const bundleId = String(codeBundle.bundleId || '');
430
+ displayPage.hasCodeBundle = true;
431
+ displayPage.codeBundleCount++;
432
+ displayPage.sourceCodeBytes += Number(source.size) || 0;
433
+ displayPage.runtimeCodeBytes += Number(runtime.size) || 0;
434
+ if (bundleId && !displayPage.bundleIds.includes(bundleId)) {
435
+ displayPage.bundleIds.push(bundleId);
436
+ }
437
+ }
298
438
  addImportedModules(displayPage.importedModules, props.importedModules);
299
439
  } else if (node.componentName === 'Jsx' && isComponentInstance(node)) {
300
440
  displayPage.hasNativeJsx = true;
@@ -581,12 +721,15 @@ async function mapLimit(items, limit, iterator) {
581
721
  return results;
582
722
  }
583
723
 
584
- async function fetchSchemaRecord(appType, form, authRef, retries) {
724
+ async function fetchSchemaRecord(appType, form, authRef, retries, options = {}) {
585
725
  let lastError = null;
586
726
  for (let attempt = 0; attempt <= retries; attempt++) {
587
727
  try {
588
728
  const result = await fetchSchema(appType, form.formUuid, authRef);
589
729
  if (isSuccessfulSchemaResult(result)) {
730
+ const resolvedResult = options.resolveCodeBundles
731
+ ? await resolveCodeBundleSchema(result, appType, form.formUuid, authRef)
732
+ : result;
590
733
  const record = {
591
734
  formUuid: form.formUuid,
592
735
  formName: form.formName,
@@ -594,10 +737,10 @@ async function fetchSchemaRecord(appType, form, authRef, retries) {
594
737
  pathName: form.pathName,
595
738
  success: true,
596
739
  attempts: attempt + 1,
597
- fieldSummary: extractFieldSummary(result),
598
- schema: result,
740
+ fieldSummary: extractFieldSummary(resolvedResult),
741
+ schema: resolvedResult,
599
742
  };
600
- const displayPage = extractDisplayPageSummary(result);
743
+ const displayPage = extractDisplayPageSummary(resolvedResult);
601
744
  if (displayPage) {
602
745
  record.displayPage = displayPage;
603
746
  }
@@ -694,6 +837,10 @@ async function runSingle(parsed, authRef) {
694
837
  throwCommandError(errorMsg);
695
838
  }
696
839
 
840
+ if (!parsed.field && !parsed.compact && !parsed.summaryJson) {
841
+ result = await resolveCodeBundleSchema(result, parsed.appType, parsed.formUuid, authRef);
842
+ }
843
+
697
844
  // 保留既有 --field 返回结构;新 contract 仅由 --compact / --resolve-fields 触发。
698
845
  if (parsed.field) {
699
846
  const allFieldNodes = collectFieldNodes(result);
@@ -782,7 +929,9 @@ async function runBatch(parsed, authRef) {
782
929
  info(` 批量获取 ${forms.length} 个表单 Schema,并发 ${parsed.concurrency},重试 ${parsed.retries}`);
783
930
 
784
931
  const records = await mapLimit(forms, parsed.concurrency, async (form) => {
785
- const record = await fetchSchemaRecord(parsed.appType, form, authRef, parsed.retries);
932
+ const record = await fetchSchemaRecord(parsed.appType, form, authRef, parsed.retries, {
933
+ resolveCodeBundles: !parsed.summaryJson,
934
+ });
786
935
  if (record.success && parsed.analysisJson) {
787
936
  record.semanticAnalysis = buildSemanticAnalysis(
788
937
  parsed.appType,
@@ -848,5 +997,9 @@ module.exports = {
848
997
  fetchSchemaRecord,
849
998
  collectFieldNodes,
850
999
  findFieldNode,
1000
+ validateCodeBundleDescriptor,
1001
+ collectCodeBundleCanvases,
1002
+ downloadCodeBundleArtifact,
1003
+ resolveCodeBundleSchema,
851
1004
  run,
852
1005
  };
@@ -10,9 +10,9 @@
10
10
  *
11
11
  * 流程:
12
12
  * 1. 读取源文件,通过内置 babel-transform 编译 + UglifyJS 压缩
13
- * 2. 用代码动态构建 Schema,将 source/compiled 填入 actions.module
13
+ * 2. 用代码动态构建 Schema;Canvas 页面拆出 source/runtime 后统一保存
14
14
  * 3. 读取 token session,在写前确认登录态
15
- * 4. 通过 HTTP POST 调用 saveFormSchema,token 失效时尝试 token refresh
15
+ * 4. Canvas 调统一保存接口;普通 JSX 页面仍调用原 saveFormSchema
16
16
  */
17
17
 
18
18
  const fs = require('fs');
@@ -21,6 +21,7 @@ const querystring = require('querystring');
21
21
  const {
22
22
  findProjectRoot,
23
23
  httpPost,
24
+ httpPostMultipart,
24
25
  httpGet,
25
26
  requestWithAutoLogin,
26
27
  } = require('../core/utils');
@@ -59,6 +60,8 @@ const {
59
60
  const SCHEMA_VERSION = 'V5';
60
61
  const DOMAIN_CODE = 'tEXDRG';
61
62
  const PREFIX = '_view';
63
+ const MAX_CODE_BUNDLE_BYTES = 5 * 1024 * 1024;
64
+ const MAX_CANVAS_SCHEMA_SKELETON_BYTES = 1024 * 1024;
62
65
 
63
66
  // ── 参数解析 ─────────────────────────────────────────
64
67
 
@@ -285,6 +288,61 @@ function buildCanvasSchemaContent(sourceCode, runtimeCode, importedModules, form
285
288
  return buildCanvasPageSchemaContent(sourceCode, runtimeCode, importedModules, formUuid);
286
289
  }
287
290
 
291
+ function buildCanvasSavePayload(schemaContent, sourceCode, runtimeCode) {
292
+ if (typeof sourceCode !== 'string' || sourceCode.length === 0
293
+ || typeof runtimeCode !== 'string' || runtimeCode.length === 0) {
294
+ throw new Error('Canvas source/runtime 不能为空');
295
+ }
296
+ const totalCodeBytes = Buffer.byteLength(sourceCode, 'utf8')
297
+ + Buffer.byteLength(runtimeCode, 'utf8');
298
+ if (totalCodeBytes > MAX_CODE_BUNDLE_BYTES) {
299
+ throw new Error('Canvas source/runtime 总大小不能超过 5 MiB');
300
+ }
301
+
302
+ let schema;
303
+ try {
304
+ schema = JSON.parse(schemaContent);
305
+ } catch {
306
+ throw new Error('Canvas Schema 不是有效 JSON');
307
+ }
308
+
309
+ const canvases = [];
310
+ function traverse(node) {
311
+ if (Array.isArray(node)) {
312
+ node.forEach(traverse);
313
+ return;
314
+ }
315
+ if (!node || typeof node !== 'object') {
316
+ return;
317
+ }
318
+ if (node.componentName === 'YidaCodeCanvas' && node.id) {
319
+ canvases.push(node);
320
+ }
321
+ Object.values(node).forEach(traverse);
322
+ }
323
+ traverse(schema);
324
+ if (canvases.length !== 1) {
325
+ throw new Error('Canvas Schema 必须且只能包含一个 YidaCodeCanvas 节点');
326
+ }
327
+
328
+ const canvas = canvases[0];
329
+ canvas.props = canvas.props && typeof canvas.props === 'object' ? canvas.props : {};
330
+ delete canvas.props.code;
331
+ delete canvas.props.runtimeCode;
332
+ delete canvas.props.codeBundle;
333
+
334
+ const skeletonContent = JSON.stringify(schema);
335
+ if (Buffer.byteLength(skeletonContent, 'utf8') > MAX_CANVAS_SCHEMA_SKELETON_BYTES) {
336
+ throw new Error('Canvas Schema 骨架不能超过 1 MiB');
337
+ }
338
+ return {
339
+ canvasNodeId: canvas.id,
340
+ content: skeletonContent,
341
+ sourceCode,
342
+ runtimeCode,
343
+ };
344
+ }
345
+
288
346
 
289
347
  // ── 4. 发送 saveFormSchema 请求 ──────────────────────
290
348
 
@@ -314,6 +372,40 @@ function sendSaveRequestWithAuth(authRef, schemaContent, appType, formUuid, serv
314
372
  return httpPost(authRef.baseUrl, saveSchemaPath, postData, { silentStatus: true });
315
373
  }
316
374
 
375
+ function sendCanvasSaveRequestWithAuth(authRef, payload, appType, formUuid, serverRevision) {
376
+ if (
377
+ !authRef ||
378
+ typeof authRef.baseUrl !== 'string' ||
379
+ authRef.baseUrl.length === 0 ||
380
+ !isTokenAuthRef(authRef)
381
+ ) {
382
+ const authError = new Error('Publish Canvas write authentication is not ready.');
383
+ authError.code = 'PUBLISH_SCHEMA_WRITE_PRECHECK_FAILED';
384
+ return Promise.reject(authError);
385
+ }
386
+ const gmtModified = requireSchemaServerRevision({ gmtModified: serverRevision });
387
+ const savePath = `/alibaba/web/${appType}/query/codeBundle/save.json?_stamp=${Date.now()}`;
388
+ return httpPostMultipart(authRef.baseUrl, savePath, {
389
+ _csrf_token: authRef.csrfToken,
390
+ formUuid,
391
+ gmtModified,
392
+ canvasNodeId: payload.canvasNodeId,
393
+ content: payload.content,
394
+ importSchema: true,
395
+ }, {
396
+ source: {
397
+ content: payload.sourceCode,
398
+ fileName: 'source.jsx',
399
+ contentType: 'text/plain; charset=UTF-8',
400
+ },
401
+ runtime: {
402
+ content: payload.runtimeCode,
403
+ fileName: 'runtime.js',
404
+ contentType: 'application/javascript; charset=UTF-8',
405
+ },
406
+ }, { silentStatus: true });
407
+ }
408
+
317
409
  function walkFiles(dir, results, limit) {
318
410
  if (!fs.existsSync(dir) || results.length >= limit) {
319
411
  return;
@@ -624,6 +716,8 @@ async function runMain(argv) {
624
716
  baseUrl = authRef.baseUrl;
625
717
 
626
718
  let schemaContent;
719
+ let expectedSchemaContent;
720
+ let canvasSavePayload;
627
721
  let serverRevision;
628
722
  if (isCanvas) {
629
723
  try {
@@ -633,7 +727,18 @@ async function runMain(argv) {
633
727
  process.exit(1);
634
728
  }
635
729
  // canvasResult 已在登录前本地编译完成,这里只需据其装配 Schema content。
636
- schemaContent = buildCanvasSchemaContent(sourceCode, canvasResult.runtimeCode, canvasResult.importedModules, formUuid);
730
+ expectedSchemaContent = buildCanvasSchemaContent(
731
+ sourceCode,
732
+ canvasResult.runtimeCode,
733
+ canvasResult.importedModules,
734
+ formUuid
735
+ );
736
+ canvasSavePayload = buildCanvasSavePayload(
737
+ expectedSchemaContent,
738
+ sourceCode,
739
+ canvasResult.runtimeCode
740
+ );
741
+ schemaContent = canvasSavePayload.content;
637
742
  } else {
638
743
  let existingDataSource = null;
639
744
  try {
@@ -646,6 +751,7 @@ async function runMain(argv) {
646
751
  }
647
752
  baseUrl = authRef.baseUrl;
648
753
  schemaContent = buildSchemaContent(sourceCode, compiledCode, formUuid, { existingDataSource });
754
+ expectedSchemaContent = schemaContent;
649
755
  }
650
756
  success(t('publish.schema_built'));
651
757
 
@@ -656,7 +762,15 @@ async function runMain(argv) {
656
762
  label('Source:', sourcePath);
657
763
  label('Compiled:', compiledPath);
658
764
  step(3, t('publish.step_publish'));
659
- const response = await sendSaveRequestWithAuth(authRef, schemaContent, appType, formUuid, serverRevision);
765
+ const response = isCanvas
766
+ ? await sendCanvasSaveRequestWithAuth(
767
+ authRef,
768
+ canvasSavePayload,
769
+ appType,
770
+ formUuid,
771
+ serverRevision
772
+ )
773
+ : await sendSaveRequestWithAuth(authRef, schemaContent, appType, formUuid, serverRevision);
660
774
 
661
775
  if (!response || !response.success) {
662
776
  const errorMsg = response ? response.errorMsg || t('common.unknown_error') : t('common.request_failed');
@@ -670,6 +784,9 @@ async function runMain(argv) {
670
784
  const content = response.content || {};
671
785
  const savedFormUuid = content.formUuid || formUuid;
672
786
  const version = content.version || 0;
787
+ const storageMode = isCanvas ? content.storageMode || null : undefined;
788
+ const bundleId = isCanvas ? content.bundleId || null : undefined;
789
+ const fallbackReason = isCanvas ? content.fallbackReason || null : undefined;
673
790
  success(t('publish.schema_published'));
674
791
  label('Form UUID:', savedFormUuid);
675
792
  label('Version:', String(version));
@@ -688,7 +805,7 @@ async function runMain(argv) {
688
805
  appType,
689
806
  savedFormUuid,
690
807
  authRef,
691
- schemaContent,
808
+ expectedSchemaContent,
692
809
  isCanvas ? 'canvas' : 'native'
693
810
  );
694
811
  } catch (healthCheckError) {
@@ -754,6 +871,9 @@ async function runMain(argv) {
754
871
  version,
755
872
  url: pageUrl,
756
873
  publishMode,
874
+ storageMode,
875
+ bundleId,
876
+ fallbackReason,
757
877
  publishReadbackVerified: !!(healthCheckResult && healthCheckResult.ok === true),
758
878
  runtimeSmokeVerified: false,
759
879
  runtimeSmokeStatus: 'not_checked',
@@ -794,5 +914,7 @@ if (require.main === module) {
794
914
  module.exports.countCustomPageDataSources = countCustomPageDataSources;
795
915
  module.exports.buildSchemaContent = buildSchemaContent;
796
916
  module.exports.buildCanvasSchemaContent = buildCanvasSchemaContent;
917
+ module.exports.buildCanvasSavePayload = buildCanvasSavePayload;
797
918
  module.exports.sendSaveRequestWithAuth = sendSaveRequestWithAuth;
919
+ module.exports.sendCanvasSaveRequestWithAuth = sendCanvasSaveRequestWithAuth;
798
920
  }
@@ -1569,8 +1569,8 @@ function summarizeLocalizedCommands(commands) {
1569
1569
  plan_command_ids: ['design-plan.init', 'design-plan.preview', 'design-plan.materialize', 'design-plan.patch'],
1570
1570
  theme_command_ids: ['sample', 'create-app', 'update-app'],
1571
1571
  navigation_command_ids: { platform: ['update-app', 'nav-group'], custom: ['update-app', 'update-form-config', 'get-form-config'] },
1572
- navigation_policy: 'Before PRD planning, reuse explicit navigation choices. During first-time intake, ask only when ownership is unknown and offer exactly two neutral options with no preselection: "宜搭原生导航" (使用宜搭自带的导航菜单。) and "自定义导航" (在自定义页面里定制菜单的样式和操作方式,替代宜搭自带的导航菜单。). Native means platform navigation. The agent chooses layout from module count, hierarchy, switching frequency, content width and target device; do not ask layout or presentation-style questions. Prefer top for few shallow modules and wide content, side for many frequently accessed modules, L-shaped/mixed for business domains with child modules, and custom dock for lightweight mobile entries. Preserve user-specified layouts. Store resolved type/source/reason/variant in the brief, distinguishing user ownership from agent layout reasoning. Platform navigation uses update-app --layout top|side|l_shape --show-app-nav, which writes layoutDirection=top|side|l_shape and hideAppNav=n. Preserve stored navType unchanged; do not derive or invent it and do not add a --nav-type option. Without --layout, normalize stored legacy layoutDirection and navType using the yc-utils application rules (hoz plus top_side means l_shape; missing layout uses top_fold/top_side for top/l_shape). Shell page-level top_fold/none overrides do not belong in app settings. Custom side/top/mixed/dock is a page layout and requires app and per-page navigation hiding with readback. Verify persisted layoutDirection and hideAppNav; updatedFields and themeVerification do not prove navigation persistence. Navigation tone is separate from layout.',
1573
- design_mode_policy: 'Analyze requirements first. First-time builds include new apps and existing apps without business pages. Reuse detailed supplied plans; if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. Confirm unresolved navigation ownership, visual style and page scope before planning or resource creation; the agent determines navigation layout from the business context without a layout question. Existing business apps only clarify the current change. A named resource-only request with explicitScope.allowInferredResources=false skips unrelated navigation and visual questions. Standard Plan uses design-plan init parallelTasks, then executes the returned materialize.command exactly once with business-file and visual-file; preview and --from-preview are only for explicitly incremental large plans. Confirm the displayed revision before building, and do not materialize or patch again after confirmation.',
1572
+ navigation_policy: 'Before PRD planning in Fast and Plan, the agent determines navigation ownership and layout from business context. Preserve explicit user requirements and existing navigation for local changes; resource-only scope must not trigger app setting changes. Use native navigation for form, approval and data-management workflows; use custom navigation for persistent menus and content state, branded menus or special interactions. Portal/dashboard/homepage labels and page-local tabs alone imply native navigation. Choose layout from module count, hierarchy, switching frequency, content width and device: top for few shallow modules and wide content, side for many frequent modules, L-shaped/mixed for two-level domains, custom dock for lightweight mobile entries. Preserve user-specified layouts. Store resolved type/source/reason/variant in the brief: ai_default for agent ownership decisions, user_selected for explicit user ownership; distinguish user requirements from agent reasoning. Include navigation in the overall Plan confirmation; Fast implements the recorded navigation decision. Platform navigation uses update-app --layout top|side|l_shape --show-app-nav, which writes layoutDirection=top|side|l_shape and hideAppNav=n. Preserve stored navType unchanged; do not derive or invent it and do not add a --nav-type option. Without --layout, normalize stored legacy layoutDirection and navType using the yc-utils application rules (hoz plus top_side means l_shape; missing layout uses top_fold/top_side for top/l_shape). Shell page-level top_fold/none overrides do not belong in app settings. Custom side/top/mixed/dock is a page layout and requires app and per-page navigation hiding with readback. Verify persisted layoutDirection and hideAppNav; updatedFields and themeVerification do not prove navigation persistence. Navigation tone is separate from layout.',
1573
+ design_mode_policy: 'Analyze requirements first. First-time builds include new apps and existing apps without business pages. Reuse detailed supplied plans; if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. Confirm unresolved visual style and page scope before planning; apply navigation_policy. Existing business apps only clarify the current change. A named resource-only request with explicitScope.allowInferredResources=false skips unrelated navigation and visual questions. Standard Plan uses design-plan init parallelTasks, then executes the returned materialize.command exactly once with business-file and visual-file; preview and --from-preview are only for explicitly incremental large plans. Confirm the displayed revision before building, and do not materialize or patch again after confirmation.',
1574
1574
  product_design_policy: 'yida-requirement-analysis owns shared facts and first-time intake. yida-prd owns business planning; yida-design owns visual design. Reuse the same confirmed brief and supplied details. Prepare business and base visuals concurrently, then bind visuals to settled page tasks. yida-app merges and checks the artifacts before creating resources. Plan uses the compact authoring contract and selected theme context; the CLI reads full templates and renders all artifacts.',
1575
1575
  ui_guidance_policy: 'Page implementation consumes yida-prd prd.md for positioning, information architecture, page prototype, native form entry policy, material strategy, business-specific checks, resource creation order, page implementation delivery order, navigation order, and acceptance criteria; it consumes yida-design design.md for app custom theme CSS delivery, themeColor/navTheme, visual states, visualScaffold, surface material, rounded rules, density rules, components, state styling, and page-level imageNeed. When a page is imageNeed=required, or beneficial with declared image slots, yida-image-assets prepares a traceable asset manifest before that page is implemented; imageNeed=none skips the branch. prd.md and design.md remain the only design sources of truth. Page implementation may extract page-spec.json as a derived implementation handoff from prd.md + design.md; conflicts are resolved by sending business conflicts to yida-prd and visual conflicts to yida-design before regenerating the spec. Core normal forms default to 1-3 business sample records before page implementation, followed by query readback. An explicit opt-out, configuration dictionary, sensitive data, or lack of safely constructible values requires a recorded skip reason. Screenshots, public sharing, data-source deep binding, and fine navigation grouping are optional after explicit user request or PRD acceptance criteria.',
1576
1576
  default_nav_order_policy: 'For custom navigation, implement PRD navigation order in the custom shell and verify app/page navigation hiding; skip platform nav-group ordering. For platform navigation: after the primary page is successfully published, perform exactly one navigation order operation. If the PRD names a navigation order, publish without --auto-nav-order and then call openyida nav-group order <appType> <items...>. If the PRD only gives broad groups or is missing navigation order, use openyida publish ... --auto-nav-order and do not call nav-group order or auto-order afterward. Explicit and automatic ordering are mutually exclusive; never generate per-item move loops. The fallback priority is portal/home/workbench entry > business handling > data management > business analytics > system configuration.',