openyida 2026.9.2-1 → 2026.9.2-beta.1
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/lib/app/display-page-readback.js +63 -5
- package/lib/app/get-schema.js +158 -5
- package/lib/app/publish.js +138 -6
- package/lib/core/locales/en.js +9 -0
- package/lib/core/locales/zh.js +9 -0
- package/lib/core/utils.js +409 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
85
|
-
|
|
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 &&
|
|
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
|
|
163
|
-
const
|
|
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,
|
package/lib/app/get-schema.js
CHANGED
|
@@ -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');
|
|
@@ -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) => 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
|
+
|
|
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(
|
|
598
|
-
schema:
|
|
740
|
+
fieldSummary: extractFieldSummary(resolvedResult),
|
|
741
|
+
schema: resolvedResult,
|
|
599
742
|
};
|
|
600
|
-
const displayPage = extractDisplayPageSummary(
|
|
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
|
};
|
package/lib/app/publish.js
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 流程:
|
|
12
12
|
* 1. 读取源文件,通过内置 babel-transform 编译 + UglifyJS 压缩
|
|
13
|
-
* 2. 用代码动态构建 Schema
|
|
13
|
+
* 2. 用代码动态构建 Schema;Canvas 页面拆出 source/runtime 后统一保存
|
|
14
14
|
* 3. 读取 token session,在写前确认登录态
|
|
15
|
-
* 4.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
}
|
package/lib/core/locales/en.js
CHANGED
|
@@ -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',
|
package/lib/core/locales/zh.js
CHANGED
|
@@ -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,413 @@ 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-oss-request-id')
|
|
1258
|
+
|| response.headers.get('x-request-id')
|
|
1259
|
+
|| '';
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function buildCodeBundleResponseMetadata(initialUrl, currentUrl, response, firstPartyTraceId) {
|
|
1263
|
+
const traceId = firstPartyTraceId
|
|
1264
|
+
|| response.headers.get('eagleeye-traceid')
|
|
1265
|
+
|| '';
|
|
1266
|
+
const metadata = {
|
|
1267
|
+
baseUrl: initialUrl.origin,
|
|
1268
|
+
finalHost: currentUrl.host,
|
|
1269
|
+
status: response.status,
|
|
1270
|
+
contentType: normalizeResponseContentType(response.headers.get('content-type')),
|
|
1271
|
+
eagleeyeTraceId: traceId,
|
|
1272
|
+
requestId: responseRequestId(response),
|
|
1273
|
+
};
|
|
1274
|
+
metadata.context = [
|
|
1275
|
+
`baseUrl=${metadata.baseUrl}`,
|
|
1276
|
+
`finalHost=${metadata.finalHost}`,
|
|
1277
|
+
`status=${metadata.status}`,
|
|
1278
|
+
`contentType=${metadata.contentType || 'missing'}`,
|
|
1279
|
+
metadata.eagleeyeTraceId ? `eagleeyeTraceId=${metadata.eagleeyeTraceId}` : '',
|
|
1280
|
+
metadata.requestId ? `requestId=${metadata.requestId}` : '',
|
|
1281
|
+
].filter(Boolean).join(', ');
|
|
1282
|
+
return metadata;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function createCodeBundleDownloadError(code, reason, metadata, responsePreview) {
|
|
1286
|
+
const suffix = responsePreview ? `; response=${responsePreview}` : '';
|
|
1287
|
+
const error = new Error(t('common.code_bundle_download_failed', reason, metadata.context) + suffix);
|
|
1288
|
+
error.code = code;
|
|
1289
|
+
error.details = {
|
|
1290
|
+
...metadata,
|
|
1291
|
+
responsePreview: responsePreview || undefined,
|
|
1292
|
+
};
|
|
1293
|
+
return error;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function compactResponsePreview(buffer) {
|
|
1297
|
+
return buffer.toString('utf8').replace(/\s+/g, ' ').trim().substring(0, 500);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
async function readErrorResponsePreview(response, timeout) {
|
|
1301
|
+
try {
|
|
1302
|
+
return compactResponsePreview(await readFetchResponseBuffer(response, 4096, timeout));
|
|
1303
|
+
} catch {
|
|
1304
|
+
return '';
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function looksLikeHtml(buffer) {
|
|
1309
|
+
const prefix = buffer.toString('utf8', 0, Math.min(buffer.length, 256)).trimStart().toLowerCase();
|
|
1310
|
+
return prefix.startsWith('<!doctype html')
|
|
1311
|
+
|| prefix.startsWith('<html')
|
|
1312
|
+
|| prefix.startsWith('<head')
|
|
1313
|
+
|| prefix.startsWith('<body');
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function expectedContentTypeMatches(actualContentType, expectedContentTypes) {
|
|
1317
|
+
if (!Array.isArray(expectedContentTypes) || expectedContentTypes.length === 0) {
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1320
|
+
return expectedContentTypes.some(value => normalizeResponseContentType(value) === actualContentType);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function jsonResponseReason(preview) {
|
|
1324
|
+
if (!preview) {
|
|
1325
|
+
return t('common.code_bundle_json_response', t('common.unknown_error'));
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
const payload = JSON.parse(preview);
|
|
1329
|
+
const message = payload && (payload.errorMsg || payload.message || payload.throwable || payload.errorCode);
|
|
1330
|
+
return t('common.code_bundle_json_response', message || t('common.unknown_error'));
|
|
1331
|
+
} catch {
|
|
1332
|
+
return t('common.code_bundle_json_response', t('common.unknown_error'));
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* 获取文本并显式跟随受控 302。鉴权头只发送给初始宜搭域名,不转发到 OSS。
|
|
1338
|
+
*/
|
|
1339
|
+
async function httpGetRedirectText(baseUrl, requestPath, queryParams, optionsOrLegacyCookies, maybeOptions) {
|
|
1340
|
+
if (typeof fetch !== 'function') {
|
|
1341
|
+
throw new Error('当前 Node.js 环境缺少 fetch,请使用 Node.js 18+');
|
|
1342
|
+
}
|
|
1343
|
+
const optionsOverride = resolveRequestOptions(optionsOrLegacyCookies, maybeOptions);
|
|
1344
|
+
const authHeaders = await resolveRequestAuthHeaders(optionsOverride);
|
|
1345
|
+
const initialUrl = buildRequestUrl(baseUrl, requestPath, queryParams);
|
|
1346
|
+
const maxRedirects = Number.isInteger(optionsOverride.maxRedirects)
|
|
1347
|
+
? optionsOverride.maxRedirects
|
|
1348
|
+
: 3;
|
|
1349
|
+
const maxBytes = Number(optionsOverride.maxBytes) || 5 * 1024 * 1024;
|
|
1350
|
+
let currentUrl = initialUrl;
|
|
1351
|
+
let firstPartyTraceId = '';
|
|
1352
|
+
|
|
1353
|
+
for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
|
|
1354
|
+
const firstPartyRequest = currentUrl.origin === initialUrl.origin;
|
|
1355
|
+
const response = await fetchWithTimeout(currentUrl, {
|
|
1356
|
+
method: 'GET',
|
|
1357
|
+
redirect: 'manual',
|
|
1358
|
+
headers: firstPartyRequest
|
|
1359
|
+
? {
|
|
1360
|
+
Accept: 'text/plain, application/javascript, */*',
|
|
1361
|
+
Origin: initialUrl.origin,
|
|
1362
|
+
Referer: initialUrl.origin + '/',
|
|
1363
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
1364
|
+
...authHeaders,
|
|
1365
|
+
}
|
|
1366
|
+
: {
|
|
1367
|
+
Accept: 'text/plain, application/javascript, */*',
|
|
1368
|
+
},
|
|
1369
|
+
}, optionsOverride.timeout);
|
|
1370
|
+
|
|
1371
|
+
if (firstPartyRequest && response.headers.get('eagleeye-traceid')) {
|
|
1372
|
+
firstPartyTraceId = response.headers.get('eagleeye-traceid');
|
|
1373
|
+
}
|
|
1374
|
+
const metadata = buildCodeBundleResponseMetadata(
|
|
1375
|
+
initialUrl,
|
|
1376
|
+
currentUrl,
|
|
1377
|
+
response,
|
|
1378
|
+
firstPartyTraceId
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
if (isHttpRedirectStatus(response.status)) {
|
|
1382
|
+
const location = response.headers.get('location');
|
|
1383
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
1384
|
+
throw new Error('CodeBundle 下载重定向无效或次数过多');
|
|
1385
|
+
}
|
|
1386
|
+
const nextUrl = new URL(location, currentUrl);
|
|
1387
|
+
if (!['http:', 'https:'].includes(nextUrl.protocol)) {
|
|
1388
|
+
throw new Error('CodeBundle 下载重定向协议不受支持');
|
|
1389
|
+
}
|
|
1390
|
+
if (currentUrl.protocol === 'https:' && nextUrl.protocol !== 'https:') {
|
|
1391
|
+
throw new Error('CodeBundle 下载禁止从 HTTPS 降级到 HTTP');
|
|
1392
|
+
}
|
|
1393
|
+
if (redirectCount === 0 && nextUrl.origin === initialUrl.origin) {
|
|
1394
|
+
return {
|
|
1395
|
+
__needLogin: true,
|
|
1396
|
+
__httpStatus: response.status,
|
|
1397
|
+
__location: location,
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
currentUrl = nextUrl;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (firstPartyRequest && response.status === 401) {
|
|
1405
|
+
return { __needLogin: true, __httpStatus: response.status };
|
|
1406
|
+
}
|
|
1407
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1408
|
+
const preview = await readErrorResponsePreview(response, optionsOverride.timeout);
|
|
1409
|
+
if ([401, 403].includes(response.status)) {
|
|
1410
|
+
throw createCodeBundleDownloadError(
|
|
1411
|
+
'CODE_BUNDLE_DOWNLOAD_FORBIDDEN',
|
|
1412
|
+
t('common.code_bundle_forbidden'),
|
|
1413
|
+
metadata,
|
|
1414
|
+
preview
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (response.status === 404) {
|
|
1418
|
+
throw createCodeBundleDownloadError(
|
|
1419
|
+
'CODE_BUNDLE_DOWNLOAD_NOT_FOUND',
|
|
1420
|
+
t('common.code_bundle_not_found'),
|
|
1421
|
+
metadata,
|
|
1422
|
+
preview
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
throw createCodeBundleDownloadError(
|
|
1426
|
+
'CODE_BUNDLE_DOWNLOAD_HTTP_ERROR',
|
|
1427
|
+
t('common.code_bundle_http_error', response.status),
|
|
1428
|
+
metadata,
|
|
1429
|
+
preview
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const contentType = metadata.contentType;
|
|
1434
|
+
const htmlResponse = contentType === 'text/html' || contentType === 'application/xhtml+xml';
|
|
1435
|
+
const jsonResponse = contentType === 'application/json' || contentType.endsWith('+json');
|
|
1436
|
+
if (htmlResponse || jsonResponse
|
|
1437
|
+
|| !expectedContentTypeMatches(contentType, optionsOverride.expectedContentTypes)) {
|
|
1438
|
+
const preview = await readErrorResponsePreview(response, optionsOverride.timeout);
|
|
1439
|
+
if (htmlResponse) {
|
|
1440
|
+
throw createCodeBundleDownloadError(
|
|
1441
|
+
'CODE_BUNDLE_DOWNLOAD_HTML_RESPONSE',
|
|
1442
|
+
t('common.code_bundle_html_response'),
|
|
1443
|
+
metadata,
|
|
1444
|
+
preview
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
if (jsonResponse) {
|
|
1448
|
+
throw createCodeBundleDownloadError(
|
|
1449
|
+
'CODE_BUNDLE_DOWNLOAD_JSON_RESPONSE',
|
|
1450
|
+
jsonResponseReason(preview),
|
|
1451
|
+
metadata,
|
|
1452
|
+
preview
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
throw createCodeBundleDownloadError(
|
|
1456
|
+
'CODE_BUNDLE_DOWNLOAD_UNEXPECTED_CONTENT_TYPE',
|
|
1457
|
+
t('common.code_bundle_unexpected_content_type', contentType || 'missing'),
|
|
1458
|
+
metadata,
|
|
1459
|
+
preview
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const buffer = await readFetchResponseBuffer(response, maxBytes, optionsOverride.timeout);
|
|
1464
|
+
if (looksLikeHtml(buffer)) {
|
|
1465
|
+
throw createCodeBundleDownloadError(
|
|
1466
|
+
'CODE_BUNDLE_DOWNLOAD_HTML_RESPONSE',
|
|
1467
|
+
t('common.code_bundle_html_response'),
|
|
1468
|
+
metadata,
|
|
1469
|
+
compactResponsePreview(buffer)
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
let value;
|
|
1473
|
+
try {
|
|
1474
|
+
value = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
1475
|
+
} catch {
|
|
1476
|
+
throw createCodeBundleDownloadError(
|
|
1477
|
+
'CODE_BUNDLE_DOWNLOAD_INVALID_UTF8',
|
|
1478
|
+
t('common.code_bundle_invalid_utf8'),
|
|
1479
|
+
metadata
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
if (typeof optionsOverride.onResponseMetadata === 'function') {
|
|
1483
|
+
optionsOverride.onResponseMetadata(metadata);
|
|
1484
|
+
}
|
|
1485
|
+
return value;
|
|
1486
|
+
}
|
|
1487
|
+
throw new Error('CodeBundle 下载重定向次数过多');
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1083
1490
|
/**
|
|
1084
1491
|
* 发送 HTTP POST 请求(application/x-www-form-urlencoded)
|
|
1085
1492
|
* @param {string} baseUrl
|
|
@@ -1504,8 +1911,10 @@ module.exports = {
|
|
|
1504
1911
|
isLoginExpired,
|
|
1505
1912
|
isCsrfTokenExpired,
|
|
1506
1913
|
httpPost,
|
|
1914
|
+
httpPostMultipart,
|
|
1507
1915
|
httpPostJson,
|
|
1508
1916
|
httpGet,
|
|
1917
|
+
httpGetRedirectText,
|
|
1509
1918
|
requestWithAutoLogin,
|
|
1510
1919
|
requestNonIdempotentWithAuthPreflight,
|
|
1511
1920
|
getNpmExecutable,
|