openyida 2026.9.1 → 2026.9.2-beta.0

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.
package/README.md CHANGED
@@ -200,7 +200,7 @@ openyida configure-process APP_XXX FORM_XXX .cache/openyida/process/process.json
200
200
  openyida process preview APP_XXX PROC_INST_XXX --output .cache/openyida/process/process.html
201
201
  openyida data query form APP_XXX FORM_XXX --page 1 --size 20
202
202
  openyida data query form APP_XXX FORM_XXX --dynamic-order '{"dateField_xxx":"-"}'
203
- openyida data create form APP_XXX FORM_XXX --data-file .cache/openyida/data-import/record.json
203
+ openyida data create form APP_XXX FORM_XXX --expect-form-name 客户 --expect-form-type receipt --data-file .cache/openyida/data-import/record.json
204
204
  openyida get-permission APP_XXX FORM_XXX
205
205
  ```
206
206
 
@@ -430,7 +430,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
430
430
 
431
431
  | Command | Description |
432
432
  |---------|-------------|
433
- | `openyida data <query\|get\|create\|update> <resource> ... \| delete form <appType> <formUuid> --inst-id <id> --confirm [--json]` | Unified data management (form/process/task/subform) |
433
+ | `openyida data <query\|get\|create\|update> <resource> ... \| delete form <appType> <formUuid> --inst-id <id> --expect-form-name <name> --expect-form-type receipt --confirm [--json]` | Unified data management (form/process/task/subform) |
434
434
  | `openyida task-center <type> [options]` | Global task center (todo/processed/cc etc.) |
435
435
  | `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
436
436
  | `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
@@ -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
+ httpGetRedirectText,
19
21
  requestWithAutoLogin,
20
22
  } = require('../core/utils');
21
23
  const { t } = require('../core/i18n');
@@ -24,7 +26,7 @@ const { fetchFormPageList } = require('./form-navigation');
24
26
  const { buildFieldResolution } = require('./schema-field-resolution');
25
27
  const { buildSemanticAnalysis } = require('./schema-semantic-analysis');
26
28
 
27
- // 需要在报表 fieldCode 中加 _value 后缀的字段类型
29
+ // 报表运行时可能暴露 raw fieldId _value;最终值必须由报表查询探针确认。
28
30
  const FIELD_TYPES_NEEDING_VALUE_SUFFIX = new Set([
29
31
  'SelectField',
30
32
  'MultiSelectField',
@@ -44,9 +46,9 @@ const FIELD_COMPONENT_NAMES = new Set([
44
46
  ]);
45
47
 
46
48
  /**
47
- * 从 Schema 中提取字段摘要,列出每个字段的真实 fieldId、组件别名和报表用 reportFieldCode。
49
+ * 从 Schema 中提取字段摘要。表单 Schema 只能提供报表字段候选,不能代替 cube 运行时元数据。
48
50
  * @param {object} schemaResult - getFormSchema API 返回结果
49
- * @returns {Array<{label, componentName, fieldId, alias, reportFieldCode, options}>}
51
+ * @returns {Array<{label, componentName, fieldId, alias, reportFieldCode, reportFieldCodeCandidates, options}>}
50
52
  */
51
53
  function extractFieldSummary(schemaResult) {
52
54
  const fields = [];
@@ -67,9 +69,9 @@ function extractFieldSummary(schemaResult) {
67
69
  ? (typeof labelRaw === 'object' ? (labelRaw.zh_CN || labelRaw.en_US || '') : String(labelRaw))
68
70
  : '';
69
71
  const fieldId = props.fieldId || '';
70
- const reportFieldCode = FIELD_TYPES_NEEDING_VALUE_SUFFIX.has(node.componentName)
71
- ? `${fieldId}_value`
72
- : fieldId;
72
+ const reportFieldCodeCandidates = FIELD_TYPES_NEEDING_VALUE_SUFFIX.has(node.componentName)
73
+ ? [fieldId, `${fieldId}_value`]
74
+ : [fieldId];
73
75
  if (fieldId) {
74
76
  const options = extractOptionSummary(props);
75
77
  const optionSource = getOptionSource(props) || [];
@@ -87,7 +89,8 @@ function extractFieldSummary(schemaResult) {
87
89
  componentName: node.componentName,
88
90
  fieldId,
89
91
  alias: aliasMaps.aliasByFieldId[fieldId] || '',
90
- reportFieldCode,
92
+ reportFieldCode: fieldId,
93
+ reportFieldCodeCandidates,
91
94
  options,
92
95
  optionCount: optionSource.length,
93
96
  optionsTruncated: optionSource.length > options.length,
@@ -226,6 +229,122 @@ function resolveSchemaContent(schemaResult) {
226
229
  return parseJsonObject(content);
227
230
  }
228
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) => httpGetRedirectText(
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
+
229
348
  function addImportedModules(target, value) {
230
349
  let modules = value;
231
350
  if (typeof modules === 'string') {
@@ -268,7 +387,10 @@ function extractDisplayPageSummary(schemaResult) {
268
387
 
269
388
  const displayPage = {
270
389
  hasYidaCodeCanvas: false,
390
+ hasCodeBundle: false,
271
391
  hasNativeJsx: false,
392
+ codeBundleCount: 0,
393
+ bundleIds: [],
272
394
  runtimeCodeBytes: 0,
273
395
  sourceCodeBytes: 0,
274
396
  compiledCodeBytes: 0,
@@ -290,10 +412,29 @@ function extractDisplayPageSummary(schemaResult) {
290
412
 
291
413
  if (node.componentName === 'YidaCodeCanvas' && isComponentInstance(node)) {
292
414
  const props = node.props || {};
415
+ const codeBundle = props.codeBundle && typeof props.codeBundle === 'object'
416
+ ? props.codeBundle
417
+ : null;
293
418
  displayPage.hasYidaCodeCanvas = true;
294
419
  displayPage.componentCount++;
295
420
  displayPage.runtimeCodeBytes += codeBytes(props.runtimeCode);
296
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
+ }
297
438
  addImportedModules(displayPage.importedModules, props.importedModules);
298
439
  } else if (node.componentName === 'Jsx' && isComponentInstance(node)) {
299
440
  displayPage.hasNativeJsx = true;
@@ -512,7 +653,7 @@ function printFieldSummary(result) {
512
653
  return;
513
654
  }
514
655
 
515
- process.stderr.write(`\n ${c.bold}${c.cyan}📋 字段摘要${c.reset} ${c.dim}(报表配置请使用 reportFieldCode)${c.reset}\n`);
656
+ process.stderr.write(`\n ${c.bold}${c.cyan}📋 字段摘要${c.reset} ${c.dim}(报表字段最终以运行时查询探针为准)${c.reset}\n`);
516
657
  process.stderr.write(` ${c.dim}${'─'.repeat(80)}${c.reset}\n`);
517
658
  process.stderr.write(
518
659
  ` ${c.bold}${'label'.padEnd(16)}${'alias'.padEnd(18)}${'componentName'.padEnd(20)}${'fieldId'.padEnd(28)}reportFieldCode${c.reset}\n`
@@ -524,7 +665,7 @@ function printFieldSummary(result) {
524
665
  );
525
666
  }
526
667
  process.stderr.write(` ${c.dim}${'─'.repeat(80)}${c.reset}\n`);
527
- process.stderr.write(` ${c.dim}注:SelectField/EmployeeField 在报表中需加 _value 后缀${c.reset}\n\n`);
668
+ process.stderr.write(` ${c.dim}注:Select/Employee 等字段同时返回 raw 与 _value 候选;create-report 会用真实图表查询验证并在原 reportId 内窄修复${c.reset}\n\n`);
528
669
  }
529
670
 
530
671
  function buildSchemaSummary(appType, formUuid, schemaResult, meta = {}) {
@@ -580,12 +721,15 @@ async function mapLimit(items, limit, iterator) {
580
721
  return results;
581
722
  }
582
723
 
583
- async function fetchSchemaRecord(appType, form, authRef, retries) {
724
+ async function fetchSchemaRecord(appType, form, authRef, retries, options = {}) {
584
725
  let lastError = null;
585
726
  for (let attempt = 0; attempt <= retries; attempt++) {
586
727
  try {
587
728
  const result = await fetchSchema(appType, form.formUuid, authRef);
588
729
  if (isSuccessfulSchemaResult(result)) {
730
+ const resolvedResult = options.resolveCodeBundles
731
+ ? await resolveCodeBundleSchema(result, appType, form.formUuid, authRef)
732
+ : result;
589
733
  const record = {
590
734
  formUuid: form.formUuid,
591
735
  formName: form.formName,
@@ -593,10 +737,10 @@ async function fetchSchemaRecord(appType, form, authRef, retries) {
593
737
  pathName: form.pathName,
594
738
  success: true,
595
739
  attempts: attempt + 1,
596
- fieldSummary: extractFieldSummary(result),
597
- schema: result,
740
+ fieldSummary: extractFieldSummary(resolvedResult),
741
+ schema: resolvedResult,
598
742
  };
599
- const displayPage = extractDisplayPageSummary(result);
743
+ const displayPage = extractDisplayPageSummary(resolvedResult);
600
744
  if (displayPage) {
601
745
  record.displayPage = displayPage;
602
746
  }
@@ -693,6 +837,10 @@ async function runSingle(parsed, authRef) {
693
837
  throwCommandError(errorMsg);
694
838
  }
695
839
 
840
+ if (!parsed.field && !parsed.compact && !parsed.summaryJson) {
841
+ result = await resolveCodeBundleSchema(result, parsed.appType, parsed.formUuid, authRef);
842
+ }
843
+
696
844
  // 保留既有 --field 返回结构;新 contract 仅由 --compact / --resolve-fields 触发。
697
845
  if (parsed.field) {
698
846
  const allFieldNodes = collectFieldNodes(result);
@@ -781,7 +929,9 @@ async function runBatch(parsed, authRef) {
781
929
  info(` 批量获取 ${forms.length} 个表单 Schema,并发 ${parsed.concurrency},重试 ${parsed.retries}`);
782
930
 
783
931
  const records = await mapLimit(forms, parsed.concurrency, async (form) => {
784
- 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
+ });
785
935
  if (record.success && parsed.analysisJson) {
786
936
  record.semanticAnalysis = buildSemanticAnalysis(
787
937
  parsed.appType,
@@ -847,5 +997,9 @@ module.exports = {
847
997
  fetchSchemaRecord,
848
998
  collectFieldNodes,
849
999
  findFieldNode,
1000
+ validateCodeBundleDescriptor,
1001
+ collectCodeBundleCanvases,
1002
+ downloadCodeBundleArtifact,
1003
+ resolveCodeBundleSchema,
850
1004
  run,
851
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');
@@ -58,6 +59,8 @@ const {
58
59
  const SCHEMA_VERSION = 'V5';
59
60
  const DOMAIN_CODE = 'tEXDRG';
60
61
  const PREFIX = '_view';
62
+ const MAX_CODE_BUNDLE_BYTES = 5 * 1024 * 1024;
63
+ const MAX_CANVAS_SCHEMA_SKELETON_BYTES = 1024 * 1024;
61
64
 
62
65
  // ── 参数解析 ─────────────────────────────────────────
63
66
 
@@ -284,6 +287,61 @@ function buildCanvasSchemaContent(sourceCode, runtimeCode, importedModules, form
284
287
  return buildCanvasPageSchemaContent(sourceCode, runtimeCode, importedModules, formUuid);
285
288
  }
286
289
 
290
+ function buildCanvasSavePayload(schemaContent, sourceCode, runtimeCode) {
291
+ if (typeof sourceCode !== 'string' || sourceCode.length === 0
292
+ || typeof runtimeCode !== 'string' || runtimeCode.length === 0) {
293
+ throw new Error('Canvas source/runtime 不能为空');
294
+ }
295
+ const totalCodeBytes = Buffer.byteLength(sourceCode, 'utf8')
296
+ + Buffer.byteLength(runtimeCode, 'utf8');
297
+ if (totalCodeBytes > MAX_CODE_BUNDLE_BYTES) {
298
+ throw new Error('Canvas source/runtime 总大小不能超过 5 MiB');
299
+ }
300
+
301
+ let schema;
302
+ try {
303
+ schema = JSON.parse(schemaContent);
304
+ } catch {
305
+ throw new Error('Canvas Schema 不是有效 JSON');
306
+ }
307
+
308
+ const canvases = [];
309
+ function traverse(node) {
310
+ if (Array.isArray(node)) {
311
+ node.forEach(traverse);
312
+ return;
313
+ }
314
+ if (!node || typeof node !== 'object') {
315
+ return;
316
+ }
317
+ if (node.componentName === 'YidaCodeCanvas' && node.id) {
318
+ canvases.push(node);
319
+ }
320
+ Object.values(node).forEach(traverse);
321
+ }
322
+ traverse(schema);
323
+ if (canvases.length !== 1) {
324
+ throw new Error('Canvas Schema 必须且只能包含一个 YidaCodeCanvas 节点');
325
+ }
326
+
327
+ const canvas = canvases[0];
328
+ canvas.props = canvas.props && typeof canvas.props === 'object' ? canvas.props : {};
329
+ delete canvas.props.code;
330
+ delete canvas.props.runtimeCode;
331
+ delete canvas.props.codeBundle;
332
+
333
+ const skeletonContent = JSON.stringify(schema);
334
+ if (Buffer.byteLength(skeletonContent, 'utf8') > MAX_CANVAS_SCHEMA_SKELETON_BYTES) {
335
+ throw new Error('Canvas Schema 骨架不能超过 1 MiB');
336
+ }
337
+ return {
338
+ canvasNodeId: canvas.id,
339
+ content: skeletonContent,
340
+ sourceCode,
341
+ runtimeCode,
342
+ };
343
+ }
344
+
287
345
 
288
346
  // ── 4. 发送 saveFormSchema 请求 ──────────────────────
289
347
 
@@ -313,6 +371,40 @@ function sendSaveRequestWithAuth(authRef, schemaContent, appType, formUuid, serv
313
371
  return httpPost(authRef.baseUrl, saveSchemaPath, postData, { silentStatus: true });
314
372
  }
315
373
 
374
+ function sendCanvasSaveRequestWithAuth(authRef, payload, appType, formUuid, serverRevision) {
375
+ if (
376
+ !authRef ||
377
+ typeof authRef.baseUrl !== 'string' ||
378
+ authRef.baseUrl.length === 0 ||
379
+ !isTokenAuthRef(authRef)
380
+ ) {
381
+ const authError = new Error('Publish Canvas write authentication is not ready.');
382
+ authError.code = 'PUBLISH_SCHEMA_WRITE_PRECHECK_FAILED';
383
+ return Promise.reject(authError);
384
+ }
385
+ const gmtModified = requireSchemaServerRevision({ gmtModified: serverRevision });
386
+ const savePath = `/alibaba/web/${appType}/query/codeBundle/save.json?_stamp=${Date.now()}`;
387
+ return httpPostMultipart(authRef.baseUrl, savePath, {
388
+ _csrf_token: authRef.csrfToken,
389
+ formUuid,
390
+ gmtModified,
391
+ canvasNodeId: payload.canvasNodeId,
392
+ content: payload.content,
393
+ importSchema: true,
394
+ }, {
395
+ source: {
396
+ content: payload.sourceCode,
397
+ fileName: 'source.jsx',
398
+ contentType: 'text/plain; charset=UTF-8',
399
+ },
400
+ runtime: {
401
+ content: payload.runtimeCode,
402
+ fileName: 'runtime.js',
403
+ contentType: 'application/javascript; charset=UTF-8',
404
+ },
405
+ }, { silentStatus: true });
406
+ }
407
+
316
408
  function walkFiles(dir, results, limit) {
317
409
  if (!fs.existsSync(dir) || results.length >= limit) {
318
410
  return;
@@ -622,6 +714,8 @@ async function runMain(argv) {
622
714
  baseUrl = authRef.baseUrl;
623
715
 
624
716
  let schemaContent;
717
+ let expectedSchemaContent;
718
+ let canvasSavePayload;
625
719
  let serverRevision;
626
720
  if (isCanvas) {
627
721
  try {
@@ -631,7 +725,18 @@ async function runMain(argv) {
631
725
  process.exit(1);
632
726
  }
633
727
  // canvasResult 已在登录前本地编译完成,这里只需据其装配 Schema content。
634
- schemaContent = buildCanvasSchemaContent(sourceCode, canvasResult.runtimeCode, canvasResult.importedModules, formUuid);
728
+ expectedSchemaContent = buildCanvasSchemaContent(
729
+ sourceCode,
730
+ canvasResult.runtimeCode,
731
+ canvasResult.importedModules,
732
+ formUuid
733
+ );
734
+ canvasSavePayload = buildCanvasSavePayload(
735
+ expectedSchemaContent,
736
+ sourceCode,
737
+ canvasResult.runtimeCode
738
+ );
739
+ schemaContent = canvasSavePayload.content;
635
740
  } else {
636
741
  let existingDataSource = null;
637
742
  try {
@@ -644,6 +749,7 @@ async function runMain(argv) {
644
749
  }
645
750
  baseUrl = authRef.baseUrl;
646
751
  schemaContent = buildSchemaContent(sourceCode, compiledCode, formUuid, { existingDataSource });
752
+ expectedSchemaContent = schemaContent;
647
753
  }
648
754
  success(t('publish.schema_built'));
649
755
 
@@ -654,7 +760,15 @@ async function runMain(argv) {
654
760
  label('Source:', sourcePath);
655
761
  label('Compiled:', compiledPath);
656
762
  step(3, t('publish.step_publish'));
657
- const response = await sendSaveRequestWithAuth(authRef, schemaContent, appType, formUuid, serverRevision);
763
+ const response = isCanvas
764
+ ? await sendCanvasSaveRequestWithAuth(
765
+ authRef,
766
+ canvasSavePayload,
767
+ appType,
768
+ formUuid,
769
+ serverRevision
770
+ )
771
+ : await sendSaveRequestWithAuth(authRef, schemaContent, appType, formUuid, serverRevision);
658
772
 
659
773
  if (!response || !response.success) {
660
774
  const errorMsg = response ? response.errorMsg || t('common.unknown_error') : t('common.request_failed');
@@ -668,6 +782,9 @@ async function runMain(argv) {
668
782
  const content = response.content || {};
669
783
  const savedFormUuid = content.formUuid || formUuid;
670
784
  const version = content.version || 0;
785
+ const storageMode = isCanvas ? content.storageMode || null : undefined;
786
+ const bundleId = isCanvas ? content.bundleId || null : undefined;
787
+ const fallbackReason = isCanvas ? content.fallbackReason || null : undefined;
671
788
  success(t('publish.schema_published'));
672
789
  label('Form UUID:', savedFormUuid);
673
790
  label('Version:', String(version));
@@ -686,7 +803,7 @@ async function runMain(argv) {
686
803
  appType,
687
804
  savedFormUuid,
688
805
  authRef,
689
- schemaContent,
806
+ expectedSchemaContent,
690
807
  isCanvas ? 'canvas' : 'native'
691
808
  );
692
809
  } catch (healthCheckError) {
@@ -733,7 +850,20 @@ async function runMain(argv) {
733
850
  ['URL', pageUrl],
734
851
  ]);
735
852
  console.log(JSON.stringify(withBrowserHandoff(
736
- { success: true, appType, formUuid: savedFormUuid, version, url: pageUrl, publishMode, healthCheck: healthCheckResult, navOrder: navOrderResult, navOrderWarning },
853
+ {
854
+ success: true,
855
+ appType,
856
+ formUuid: savedFormUuid,
857
+ version,
858
+ url: pageUrl,
859
+ publishMode,
860
+ storageMode,
861
+ bundleId,
862
+ fallbackReason,
863
+ healthCheck: healthCheckResult,
864
+ navOrder: navOrderResult,
865
+ navOrderWarning,
866
+ },
737
867
  pageUrl,
738
868
  { stage: 'publish_page_success', title: savedFormUuid },
739
869
  browserOpenMode
@@ -767,5 +897,7 @@ if (require.main === module) {
767
897
  module.exports.countCustomPageDataSources = countCustomPageDataSources;
768
898
  module.exports.buildSchemaContent = buildSchemaContent;
769
899
  module.exports.buildCanvasSchemaContent = buildCanvasSchemaContent;
900
+ module.exports.buildCanvasSavePayload = buildCanvasSavePayload;
770
901
  module.exports.sendSaveRequestWithAuth = sendSaveRequestWithAuth;
902
+ module.exports.sendCanvasSaveRequestWithAuth = sendCanvasSaveRequestWithAuth;
771
903
  }