openyida 2026.9.3-1 → 2026.9.4-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.
@@ -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,
@@ -734,6 +735,36 @@ function assertCanvasRuntimeParseable(runtimeCode, options = {}) {
734
735
  }
735
736
  }
736
737
 
738
+ /**
739
+ * 压缩画布运行态代码,并显式保留装配器读取的 YidaComp 入口名。
740
+ */
741
+ function minifyCanvasRuntime(runtimeCode, options = {}) {
742
+ let result;
743
+ try {
744
+ result = UglifyJS.minify(runtimeCode, {
745
+ compress: true,
746
+ mangle: {
747
+ reserved: ['YidaComp'],
748
+ },
749
+ });
750
+ } catch (error) {
751
+ result = { error };
752
+ }
753
+ if (!result || result.error || typeof result.code !== 'string' || !result.code) {
754
+ const detail = result && result.error && result.error.message
755
+ ? result.error.message
756
+ : '压缩结果为空';
757
+ throw new CliError(`Code Canvas runtimeCode 压缩失败: ${detail}`, {
758
+ code: 'OPENYIDA_CANVAS_MINIFY_FAILED',
759
+ details: {
760
+ stage: 'canvas_minify',
761
+ sourcePath: options.sourcePath || '',
762
+ },
763
+ });
764
+ }
765
+ return result.code;
766
+ }
767
+
737
768
  /**
738
769
  * 本地编译 Code Canvas 源码。
739
770
  * @param {string} source 原始 React/JSX/TSX 源码
@@ -839,11 +870,12 @@ function compileCanvasLocal(source, options = {}) {
839
870
  configFile: false,
840
871
  });
841
872
 
842
- const runtimeCode = stage2.code || '';
843
- assertNoEmojiInText(runtimeCode, {
873
+ const unminifiedRuntimeCode = stage2.code || '';
874
+ assertNoEmojiInText(unminifiedRuntimeCode, {
844
875
  artifact: options.sourcePath ? options.sourcePath + ' runtime' : 'Code Canvas runtime',
845
876
  code: 'OPENYIDA_CANVAS_SOURCE_EMOJI_FORBIDDEN',
846
877
  });
878
+ const runtimeCode = minifyCanvasRuntime(unminifiedRuntimeCode, options);
847
879
  assertCanvasRuntimeParseable(runtimeCode, options);
848
880
  return {
849
881
  runtimeCode,
@@ -910,6 +942,7 @@ module.exports = {
910
942
  resolveWindowAlias,
911
943
  shouldAllowUnsupportedBareImports,
912
944
  assertCanvasRuntimeParseable,
945
+ minifyCanvasRuntime,
913
946
  findBareDependencyGlobalIssues,
914
947
  MODULE_ALIAS_MAP,
915
948
  };
@@ -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) {
@@ -747,7 +864,20 @@ async function runMain(argv) {
747
864
  ['URL', pageUrl],
748
865
  ]);
749
866
  console.log(JSON.stringify(withBrowserHandoff(
750
- { success: true, appType, formUuid: savedFormUuid, version, url: pageUrl, publishMode, healthCheck: healthCheckResult, navOrder: navOrderResult, navOrderWarning },
867
+ {
868
+ success: true,
869
+ appType,
870
+ formUuid: savedFormUuid,
871
+ version,
872
+ url: pageUrl,
873
+ publishMode,
874
+ storageMode,
875
+ bundleId,
876
+ fallbackReason,
877
+ healthCheck: healthCheckResult,
878
+ navOrder: navOrderResult,
879
+ navOrderWarning,
880
+ },
751
881
  pageUrl,
752
882
  { stage: 'publish_page_success', title: savedFormUuid },
753
883
  browserOpenMode
@@ -781,5 +911,7 @@ if (require.main === module) {
781
911
  module.exports.countCustomPageDataSources = countCustomPageDataSources;
782
912
  module.exports.buildSchemaContent = buildSchemaContent;
783
913
  module.exports.buildCanvasSchemaContent = buildCanvasSchemaContent;
914
+ module.exports.buildCanvasSavePayload = buildCanvasSavePayload;
784
915
  module.exports.sendSaveRequestWithAuth = sendSaveRequestWithAuth;
916
+ module.exports.sendCanvasSaveRequestWithAuth = sendCanvasSaveRequestWithAuth;
785
917
  }
@@ -869,6 +869,15 @@ Examples:
869
869
  response_body: ' Response body: {0}',
870
870
  response_detail: ' Response detail: {0}',
871
871
  response_not_json: 'response is not JSON',
872
+ code_bundle_download_failed: 'CodeBundle download failed: {0} ({1})',
873
+ code_bundle_forbidden: 'permission verification or OSS access was denied',
874
+ code_bundle_not_found: 'the file does not exist or has been removed',
875
+ code_bundle_http_error: 'HTTP {0}',
876
+ code_bundle_html_response: 'received an HTML error page; the environment or route may be incorrect',
877
+ code_bundle_json_response: 'the service returned a JSON error response: {0}',
878
+ code_bundle_unexpected_content_type: 'unexpected response Content-Type: {0}',
879
+ code_bundle_invalid_utf8: 'the response is not valid UTF-8 text',
880
+ code_bundle_integrity_failed: 'CodeBundle {0} integrity verification failed ({1})',
872
881
  login_expired: ' Login session expired: {0}',
873
882
  csrf_expired: ' CSRF token expired: {0}',
874
883
  csrf_refreshed: ' csrf_token refreshed',
@@ -840,6 +840,15 @@ openyida - 宜搭命令行工具
840
840
  response_body: ' 响应内容: {0}',
841
841
  response_detail: ' 响应详情: {0}',
842
842
  response_not_json: '响应非 JSON',
843
+ code_bundle_download_failed: 'CodeBundle 下载失败:{0}({1})',
844
+ code_bundle_forbidden: '权限校验或 OSS 访问被拒绝',
845
+ code_bundle_not_found: '文件不存在或已被清理',
846
+ code_bundle_http_error: 'HTTP {0}',
847
+ code_bundle_html_response: '返回了 HTML 错误页,可能是环境或路由错误',
848
+ code_bundle_json_response: '服务端返回 JSON 错误响应:{0}',
849
+ code_bundle_unexpected_content_type: '响应 Content-Type 不符合预期:{0}',
850
+ code_bundle_invalid_utf8: '响应内容不是有效 UTF-8 文本',
851
+ code_bundle_integrity_failed: 'CodeBundle {0} 完整性校验失败({1})',
843
852
  login_expired: ' 检测到登录过期: {0}',
844
853
  csrf_expired: ' 检测到 csrf_token 过期: {0}',
845
854
  csrf_refreshed: ' csrf_token 已刷新',
package/lib/core/utils.js CHANGED
@@ -1080,6 +1080,362 @@ function createNonJsonResponseResult(statusCode, data) {
1080
1080
  };
1081
1081
  }
1082
1082
 
1083
+ function buildRequestUrl(baseUrl, requestPath, queryParams) {
1084
+ const normalizedBaseUrl = String(baseUrl || '').replace(/\/+$/, '') + '/';
1085
+ const requestUrl = new URL(requestPath, normalizedBaseUrl);
1086
+ if (queryParams) {
1087
+ Object.entries(queryParams).forEach(([key, value]) => {
1088
+ if (value !== undefined && value !== null) {
1089
+ requestUrl.searchParams.set(key, String(value));
1090
+ }
1091
+ });
1092
+ }
1093
+ return requestUrl;
1094
+ }
1095
+
1096
+ async function fetchWithTimeout(requestUrl, requestOptions, timeout) {
1097
+ const controller = new AbortController();
1098
+ const timer = setTimeout(() => controller.abort(), timeout || 30000);
1099
+ try {
1100
+ return await fetch(requestUrl, {
1101
+ ...requestOptions,
1102
+ signal: controller.signal,
1103
+ });
1104
+ } catch (error) {
1105
+ if (error && error.name === 'AbortError') {
1106
+ throw new Error(t('common.request_timeout'));
1107
+ }
1108
+ throw error;
1109
+ } finally {
1110
+ clearTimeout(timer);
1111
+ }
1112
+ }
1113
+
1114
+ async function parseJsonFetchResponse(response, options = {}) {
1115
+ const data = await response.text();
1116
+ if (!options.silentStatus) {
1117
+ warn(t('common.http_status', response.status));
1118
+ }
1119
+ if (isHttpRedirectStatus(response.status) || isHttpAuthStatus(response.status)) {
1120
+ return {
1121
+ __needLogin: true,
1122
+ __httpStatus: response.status,
1123
+ __location: response.headers.get('location') || '',
1124
+ };
1125
+ }
1126
+ try {
1127
+ const parsed = JSON.parse(data);
1128
+ if (isLoginExpired(parsed)) {
1129
+ return { __needLogin: true };
1130
+ }
1131
+ if (isCsrfTokenExpired(parsed)) {
1132
+ return { __csrfExpired: true };
1133
+ }
1134
+ return parsed;
1135
+ } catch {
1136
+ if (!options.silentStatus) {
1137
+ warn(t('common.http_response', data.substring(0, 500)));
1138
+ }
1139
+ return createNonJsonResponseResult(response.status, data);
1140
+ }
1141
+ }
1142
+
1143
+ function appendMultipartFile(form, fieldName, file) {
1144
+ if (!file || file.content === undefined || file.content === null) {
1145
+ throw new TypeError(`multipart file ${fieldName} is required`);
1146
+ }
1147
+ const content = Buffer.isBuffer(file.content)
1148
+ ? file.content
1149
+ : Buffer.from(String(file.content), 'utf8');
1150
+ form.append(
1151
+ fieldName,
1152
+ new Blob([content], { type: file.contentType || 'application/octet-stream' }),
1153
+ file.fileName || fieldName
1154
+ );
1155
+ }
1156
+
1157
+ /**
1158
+ * 发送 multipart/form-data POST。Content-Type boundary 由 Node.js FormData 生成。
1159
+ */
1160
+ async function httpPostMultipart(baseUrl, requestPath, fields, files, optionsOrLegacyCookies, maybeOptions) {
1161
+ if (typeof fetch !== 'function' || typeof FormData !== 'function' || typeof Blob !== 'function') {
1162
+ throw new Error('当前 Node.js 环境缺少 fetch/FormData/Blob,请使用 Node.js 18+');
1163
+ }
1164
+ const optionsOverride = resolveRequestOptions(optionsOrLegacyCookies, maybeOptions);
1165
+ const authHeaders = await resolveRequestAuthHeaders(optionsOverride);
1166
+ const form = new FormData();
1167
+ Object.entries(fields || {}).forEach(([key, value]) => {
1168
+ if (value !== undefined && value !== null) {
1169
+ form.append(key, String(value));
1170
+ }
1171
+ });
1172
+ Object.entries(files || {}).forEach(([key, file]) => appendMultipartFile(form, key, file));
1173
+
1174
+ const requestUrl = buildRequestUrl(baseUrl, requestPath);
1175
+ const response = await fetchWithTimeout(requestUrl, {
1176
+ method: 'POST',
1177
+ redirect: 'manual',
1178
+ headers: {
1179
+ Accept: 'application/json, text/plain, */*',
1180
+ Origin: requestUrl.origin,
1181
+ Referer: optionsOverride.referer || requestUrl.origin + '/',
1182
+ 'x-requested-with': 'XMLHttpRequest',
1183
+ ...authHeaders,
1184
+ },
1185
+ body: form,
1186
+ }, optionsOverride.timeout);
1187
+ return parseJsonFetchResponse(response, optionsOverride);
1188
+ }
1189
+
1190
+ async function readFetchResponseBuffer(response, maxBytes, timeout) {
1191
+ const limit = Number(maxBytes);
1192
+ const timeoutMs = Number(timeout) || 30000;
1193
+ const contentLength = Number(response.headers.get('content-length') || 0);
1194
+ if (Number.isFinite(limit) && limit > 0 && contentLength > limit) {
1195
+ const error = new Error(`响应内容超过允许大小 ${limit} bytes`);
1196
+ error.code = 'HTTP_RESPONSE_TOO_LARGE';
1197
+ throw error;
1198
+ }
1199
+
1200
+ if (!response.body || typeof response.body.getReader !== 'function') {
1201
+ let timer;
1202
+ const arrayBuffer = await Promise.race([
1203
+ response.arrayBuffer(),
1204
+ new Promise((resolve, reject) => {
1205
+ void resolve;
1206
+ timer = setTimeout(() => reject(new Error(t('common.request_timeout'))), timeoutMs);
1207
+ }),
1208
+ ]).finally(() => clearTimeout(timer));
1209
+ const buffer = Buffer.from(arrayBuffer);
1210
+ if (Number.isFinite(limit) && limit > 0 && buffer.length > limit) {
1211
+ const error = new Error(`响应内容超过允许大小 ${limit} bytes`);
1212
+ error.code = 'HTTP_RESPONSE_TOO_LARGE';
1213
+ throw error;
1214
+ }
1215
+ return buffer;
1216
+ }
1217
+
1218
+ const chunks = [];
1219
+ let size = 0;
1220
+ const reader = response.body.getReader();
1221
+ let timedOut = false;
1222
+ const timer = setTimeout(() => {
1223
+ timedOut = true;
1224
+ void reader.cancel();
1225
+ }, timeoutMs);
1226
+ try {
1227
+ for (;;) {
1228
+ const { done, value } = await reader.read();
1229
+ if (done) {
1230
+ break;
1231
+ }
1232
+ const chunk = Buffer.from(value);
1233
+ size += chunk.length;
1234
+ if (Number.isFinite(limit) && limit > 0 && size > limit) {
1235
+ await reader.cancel();
1236
+ const error = new Error(`响应内容超过允许大小 ${limit} bytes`);
1237
+ error.code = 'HTTP_RESPONSE_TOO_LARGE';
1238
+ throw error;
1239
+ }
1240
+ chunks.push(chunk);
1241
+ }
1242
+ if (timedOut) {
1243
+ throw new Error(t('common.request_timeout'));
1244
+ }
1245
+ } finally {
1246
+ clearTimeout(timer);
1247
+ reader.releaseLock();
1248
+ }
1249
+ return Buffer.concat(chunks, size);
1250
+ }
1251
+
1252
+ function normalizeResponseContentType(value) {
1253
+ return String(value || '').split(';')[0].trim().toLowerCase();
1254
+ }
1255
+
1256
+ function responseRequestId(response) {
1257
+ return response.headers.get('x-request-id') || '';
1258
+ }
1259
+
1260
+ function buildCodeBundleResponseMetadata(requestUrl, response) {
1261
+ const metadata = {
1262
+ baseUrl: requestUrl.origin,
1263
+ finalHost: requestUrl.host,
1264
+ status: response.status,
1265
+ contentType: normalizeResponseContentType(response.headers.get('content-type')),
1266
+ eagleeyeTraceId: response.headers.get('eagleeye-traceid') || '',
1267
+ requestId: responseRequestId(response),
1268
+ };
1269
+ metadata.context = [
1270
+ `baseUrl=${metadata.baseUrl}`,
1271
+ `finalHost=${metadata.finalHost}`,
1272
+ `status=${metadata.status}`,
1273
+ `contentType=${metadata.contentType || 'missing'}`,
1274
+ metadata.eagleeyeTraceId ? `eagleeyeTraceId=${metadata.eagleeyeTraceId}` : '',
1275
+ metadata.requestId ? `requestId=${metadata.requestId}` : '',
1276
+ ].filter(Boolean).join(', ');
1277
+ return metadata;
1278
+ }
1279
+
1280
+ function createCodeBundleDownloadError(code, reason, metadata, responsePreview) {
1281
+ const suffix = responsePreview ? `; response=${responsePreview}` : '';
1282
+ const error = new Error(t('common.code_bundle_download_failed', reason, metadata.context) + suffix);
1283
+ error.code = code;
1284
+ error.details = {
1285
+ ...metadata,
1286
+ responsePreview: responsePreview || undefined,
1287
+ };
1288
+ return error;
1289
+ }
1290
+
1291
+ function compactResponsePreview(buffer) {
1292
+ return buffer.toString('utf8').replace(/\s+/g, ' ').trim().substring(0, 500);
1293
+ }
1294
+
1295
+ async function readErrorResponsePreview(response, timeout) {
1296
+ try {
1297
+ return compactResponsePreview(await readFetchResponseBuffer(response, 4096, timeout));
1298
+ } catch {
1299
+ return '';
1300
+ }
1301
+ }
1302
+
1303
+ function looksLikeHtml(buffer) {
1304
+ const prefix = buffer.toString('utf8', 0, Math.min(buffer.length, 256)).trimStart().toLowerCase();
1305
+ return prefix.startsWith('<!doctype html')
1306
+ || prefix.startsWith('<html')
1307
+ || prefix.startsWith('<head')
1308
+ || prefix.startsWith('<body');
1309
+ }
1310
+
1311
+ function expectedContentTypeMatches(actualContentType, expectedContentTypes) {
1312
+ if (!Array.isArray(expectedContentTypes) || expectedContentTypes.length === 0) {
1313
+ return true;
1314
+ }
1315
+ return expectedContentTypes.some(value => normalizeResponseContentType(value) === actualContentType);
1316
+ }
1317
+
1318
+ function jsonResponseReason(preview) {
1319
+ if (!preview) {
1320
+ return t('common.code_bundle_json_response', t('common.unknown_error'));
1321
+ }
1322
+ try {
1323
+ const payload = JSON.parse(preview);
1324
+ const message = payload && (payload.errorMsg || payload.message || payload.throwable || payload.errorCode);
1325
+ return t('common.code_bundle_json_response', message || t('common.unknown_error'));
1326
+ } catch {
1327
+ return t('common.code_bundle_json_response', t('common.unknown_error'));
1328
+ }
1329
+ }
1330
+
1331
+ /**
1332
+ * 从宜搭首方接口直接获取 CodeBundle 文本。
1333
+ */
1334
+ async function httpGetCodeBundleText(baseUrl, requestPath, queryParams, optionsOrLegacyCookies, maybeOptions) {
1335
+ if (typeof fetch !== 'function') {
1336
+ throw new Error('当前 Node.js 环境缺少 fetch,请使用 Node.js 18+');
1337
+ }
1338
+ const optionsOverride = resolveRequestOptions(optionsOrLegacyCookies, maybeOptions);
1339
+ const authHeaders = await resolveRequestAuthHeaders(optionsOverride);
1340
+ const requestUrl = buildRequestUrl(baseUrl, requestPath, queryParams);
1341
+ const maxBytes = Number(optionsOverride.maxBytes) || 5 * 1024 * 1024;
1342
+ const response = await fetchWithTimeout(requestUrl, {
1343
+ method: 'GET',
1344
+ redirect: 'manual',
1345
+ headers: {
1346
+ Accept: 'text/plain, application/javascript, */*',
1347
+ Origin: requestUrl.origin,
1348
+ Referer: requestUrl.origin + '/',
1349
+ 'x-requested-with': 'XMLHttpRequest',
1350
+ ...authHeaders,
1351
+ },
1352
+ }, optionsOverride.timeout);
1353
+ const metadata = buildCodeBundleResponseMetadata(requestUrl, response);
1354
+
1355
+ if (response.status === 401) {
1356
+ return { __needLogin: true, __httpStatus: response.status };
1357
+ }
1358
+ if (response.status < 200 || response.status >= 300) {
1359
+ const preview = await readErrorResponsePreview(response, optionsOverride.timeout);
1360
+ if (response.status === 403) {
1361
+ throw createCodeBundleDownloadError(
1362
+ 'CODE_BUNDLE_DOWNLOAD_FORBIDDEN',
1363
+ t('common.code_bundle_forbidden'),
1364
+ metadata,
1365
+ preview
1366
+ );
1367
+ }
1368
+ if (response.status === 404) {
1369
+ throw createCodeBundleDownloadError(
1370
+ 'CODE_BUNDLE_DOWNLOAD_NOT_FOUND',
1371
+ t('common.code_bundle_not_found'),
1372
+ metadata,
1373
+ preview
1374
+ );
1375
+ }
1376
+ throw createCodeBundleDownloadError(
1377
+ 'CODE_BUNDLE_DOWNLOAD_HTTP_ERROR',
1378
+ t('common.code_bundle_http_error', response.status),
1379
+ metadata,
1380
+ preview
1381
+ );
1382
+ }
1383
+
1384
+ const contentType = metadata.contentType;
1385
+ const htmlResponse = contentType === 'text/html' || contentType === 'application/xhtml+xml';
1386
+ const jsonResponse = contentType === 'application/json' || contentType.endsWith('+json');
1387
+ if (htmlResponse || jsonResponse
1388
+ || !expectedContentTypeMatches(contentType, optionsOverride.expectedContentTypes)) {
1389
+ const preview = await readErrorResponsePreview(response, optionsOverride.timeout);
1390
+ if (htmlResponse) {
1391
+ throw createCodeBundleDownloadError(
1392
+ 'CODE_BUNDLE_DOWNLOAD_HTML_RESPONSE',
1393
+ t('common.code_bundle_html_response'),
1394
+ metadata,
1395
+ preview
1396
+ );
1397
+ }
1398
+ if (jsonResponse) {
1399
+ throw createCodeBundleDownloadError(
1400
+ 'CODE_BUNDLE_DOWNLOAD_JSON_RESPONSE',
1401
+ jsonResponseReason(preview),
1402
+ metadata,
1403
+ preview
1404
+ );
1405
+ }
1406
+ throw createCodeBundleDownloadError(
1407
+ 'CODE_BUNDLE_DOWNLOAD_UNEXPECTED_CONTENT_TYPE',
1408
+ t('common.code_bundle_unexpected_content_type', contentType || 'missing'),
1409
+ metadata,
1410
+ preview
1411
+ );
1412
+ }
1413
+
1414
+ const buffer = await readFetchResponseBuffer(response, maxBytes, optionsOverride.timeout);
1415
+ if (looksLikeHtml(buffer)) {
1416
+ throw createCodeBundleDownloadError(
1417
+ 'CODE_BUNDLE_DOWNLOAD_HTML_RESPONSE',
1418
+ t('common.code_bundle_html_response'),
1419
+ metadata,
1420
+ compactResponsePreview(buffer)
1421
+ );
1422
+ }
1423
+ let value;
1424
+ try {
1425
+ value = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
1426
+ } catch {
1427
+ throw createCodeBundleDownloadError(
1428
+ 'CODE_BUNDLE_DOWNLOAD_INVALID_UTF8',
1429
+ t('common.code_bundle_invalid_utf8'),
1430
+ metadata
1431
+ );
1432
+ }
1433
+ if (typeof optionsOverride.onResponseMetadata === 'function') {
1434
+ optionsOverride.onResponseMetadata(metadata);
1435
+ }
1436
+ return value;
1437
+ }
1438
+
1083
1439
  /**
1084
1440
  * 发送 HTTP POST 请求(application/x-www-form-urlencoded)
1085
1441
  * @param {string} baseUrl
@@ -1504,8 +1860,10 @@ module.exports = {
1504
1860
  isLoginExpired,
1505
1861
  isCsrfTokenExpired,
1506
1862
  httpPost,
1863
+ httpPostMultipart,
1507
1864
  httpPostJson,
1508
1865
  httpGet,
1866
+ httpGetCodeBundleText,
1509
1867
  requestWithAutoLogin,
1510
1868
  requestNonIdempotentWithAuthPreflight,
1511
1869
  getNpmExecutable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.9.3-1",
3
+ "version": "2026.9.4-beta.0",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",