openyida 2026.9.13 → 2026.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app/canvas-compile.js +37 -4
- package/lib/app/display-page-readback.js +63 -5
- package/lib/app/get-schema.js +158 -5
- package/lib/app/publish.js +127 -5
- package/lib/core/command-manifest.js +2 -2
- package/lib/core/locales/en.js +9 -0
- package/lib/core/locales/zh.js +9 -0
- package/lib/core/utils.js +358 -0
- package/package.json +1 -1
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +3 -1
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +2 -2
- package/yida-skills/skills/yida-create-form-page/SKILL.md +1 -0
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +40 -6
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +1 -1
- package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +1 -1
- package/yida-skills/skills/yida-integration/SKILL.md +1 -0
- package/yida-skills/skills/yida-nav-shell/SKILL.md +4 -4
- package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
- package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +2 -2
- package/yida-skills/skills/yida-requirement-analysis/SKILL.md +1 -1
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +13 -16
|
@@ -24,12 +24,13 @@
|
|
|
24
24
|
* 3) 第三方依赖以 `window.<别名>` 形式引用(antd→window.antd、react→window.React …),
|
|
25
25
|
* 这些 UMD 依赖由画布运行时依据 importedModules 白名单按需注入。
|
|
26
26
|
*
|
|
27
|
-
* 因此本地编译 = Babel 把 JSX/TS 转成
|
|
28
|
-
* 把 export default 改写成画布入口 `YidaComp` → 正则抽出依赖包名。
|
|
27
|
+
* 因此本地编译 = Babel 把 JSX/TS 转成 JS → 把 import 改写成 window 别名引用、
|
|
28
|
+
* 把 export default 改写成画布入口 `YidaComp` → UglifyJS 压缩 → 正则抽出依赖包名。
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
const Babel = require('@babel/standalone');
|
|
32
32
|
const globals = require('globals');
|
|
33
|
+
const UglifyJS = require('uglify-js');
|
|
33
34
|
const {
|
|
34
35
|
assertNoEmojiInArtifactName,
|
|
35
36
|
assertNoEmojiInText,
|
|
@@ -877,6 +878,36 @@ function assertCanvasRuntimeParseable(runtimeCode, options = {}) {
|
|
|
877
878
|
}
|
|
878
879
|
}
|
|
879
880
|
|
|
881
|
+
/**
|
|
882
|
+
* 压缩画布运行态代码,并显式保留装配器读取的 YidaComp 入口名。
|
|
883
|
+
*/
|
|
884
|
+
function minifyCanvasRuntime(runtimeCode, options = {}) {
|
|
885
|
+
let result;
|
|
886
|
+
try {
|
|
887
|
+
result = UglifyJS.minify(runtimeCode, {
|
|
888
|
+
compress: true,
|
|
889
|
+
mangle: {
|
|
890
|
+
reserved: ['YidaComp'],
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
} catch (error) {
|
|
894
|
+
result = { error };
|
|
895
|
+
}
|
|
896
|
+
if (!result || result.error || typeof result.code !== 'string' || !result.code) {
|
|
897
|
+
const detail = result && result.error && result.error.message
|
|
898
|
+
? result.error.message
|
|
899
|
+
: '压缩结果为空';
|
|
900
|
+
throw new CliError(`Code Canvas runtimeCode 压缩失败: ${detail}`, {
|
|
901
|
+
code: 'OPENYIDA_CANVAS_MINIFY_FAILED',
|
|
902
|
+
details: {
|
|
903
|
+
stage: 'canvas_minify',
|
|
904
|
+
sourcePath: options.sourcePath || '',
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return result.code;
|
|
909
|
+
}
|
|
910
|
+
|
|
880
911
|
/**
|
|
881
912
|
* 本地编译 Code Canvas 源码。
|
|
882
913
|
* @param {string} source 原始 React/JSX/TSX 源码
|
|
@@ -984,11 +1015,12 @@ function compileCanvasLocal(source, options = {}) {
|
|
|
984
1015
|
configFile: false,
|
|
985
1016
|
});
|
|
986
1017
|
|
|
987
|
-
const
|
|
988
|
-
assertNoEmojiInText(
|
|
1018
|
+
const unminifiedRuntimeCode = stage2.code || '';
|
|
1019
|
+
assertNoEmojiInText(unminifiedRuntimeCode, {
|
|
989
1020
|
artifact: options.sourcePath ? options.sourcePath + ' runtime' : 'Code Canvas runtime',
|
|
990
1021
|
code: 'OPENYIDA_CANVAS_SOURCE_EMOJI_FORBIDDEN',
|
|
991
1022
|
});
|
|
1023
|
+
const runtimeCode = minifyCanvasRuntime(unminifiedRuntimeCode, options);
|
|
992
1024
|
assertCanvasRuntimeParseable(runtimeCode, options);
|
|
993
1025
|
assertDependencyManifestConsistent(runtimeCode, importedModules, options);
|
|
994
1026
|
return {
|
|
@@ -1059,6 +1091,7 @@ module.exports = {
|
|
|
1059
1091
|
resolveWindowAlias,
|
|
1060
1092
|
shouldAllowUnsupportedBareImports,
|
|
1061
1093
|
assertCanvasRuntimeParseable,
|
|
1094
|
+
minifyCanvasRuntime,
|
|
1062
1095
|
findBareDependencyGlobalIssues,
|
|
1063
1096
|
findSelfReferentialDependencyBindingIssues,
|
|
1064
1097
|
findDependencyManifestIssues,
|
|
@@ -23,10 +23,30 @@ function fingerprint(value) {
|
|
|
23
23
|
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function rawFingerprint(value) {
|
|
27
|
+
if (typeof value !== 'string' || !value) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveSize(value) {
|
|
34
|
+
const size = Number(value);
|
|
35
|
+
return Number.isFinite(size) && size > 0 ? size : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256(value) {
|
|
39
|
+
const digest = String(value || '').toLowerCase();
|
|
40
|
+
return /^[a-f0-9]{64}$/.test(digest) ? digest : '';
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
function createEmptyDisplayInfo() {
|
|
27
44
|
return {
|
|
28
45
|
hasYidaCodeCanvas: false,
|
|
46
|
+
hasCodeBundle: false,
|
|
29
47
|
hasNativeJsx: false,
|
|
48
|
+
codeBundleCount: 0,
|
|
49
|
+
bundleIds: [],
|
|
30
50
|
runtimeCodeBytes: 0,
|
|
31
51
|
sourceCodeBytes: 0,
|
|
32
52
|
compiledCodeBytes: 0,
|
|
@@ -34,6 +54,8 @@ function createEmptyDisplayInfo() {
|
|
|
34
54
|
componentCount: 0,
|
|
35
55
|
canvasRuntimeCode: '',
|
|
36
56
|
canvasSourceCode: '',
|
|
57
|
+
canvasRuntimeSha256: '',
|
|
58
|
+
canvasSourceSha256: '',
|
|
37
59
|
nativeCompiledCode: '',
|
|
38
60
|
nativeSourceCode: '',
|
|
39
61
|
};
|
|
@@ -79,12 +101,37 @@ function traverseDisplayNodes(node, info) {
|
|
|
79
101
|
|
|
80
102
|
if (node.componentName === 'YidaCodeCanvas') {
|
|
81
103
|
const props = node.props || {};
|
|
104
|
+
const codeBundle = props.codeBundle && typeof props.codeBundle === 'object'
|
|
105
|
+
? props.codeBundle
|
|
106
|
+
: null;
|
|
82
107
|
info.hasYidaCodeCanvas = true;
|
|
83
108
|
info.componentCount++;
|
|
84
|
-
|
|
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
|
+
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(
|
|
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');
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
808
|
+
expectedSchemaContent,
|
|
692
809
|
isCanvas ? 'canvas' : 'native'
|
|
693
810
|
);
|
|
694
811
|
} catch (healthCheckError) {
|
|
@@ -754,6 +871,9 @@ async function runMain(argv) {
|
|
|
754
871
|
version,
|
|
755
872
|
url: pageUrl,
|
|
756
873
|
publishMode,
|
|
874
|
+
storageMode,
|
|
875
|
+
bundleId,
|
|
876
|
+
fallbackReason,
|
|
757
877
|
publishReadbackVerified: !!(healthCheckResult && healthCheckResult.ok === true),
|
|
758
878
|
runtimeSmokeVerified: false,
|
|
759
879
|
runtimeSmokeStatus: 'not_checked',
|
|
@@ -794,5 +914,7 @@ if (require.main === module) {
|
|
|
794
914
|
module.exports.countCustomPageDataSources = countCustomPageDataSources;
|
|
795
915
|
module.exports.buildSchemaContent = buildSchemaContent;
|
|
796
916
|
module.exports.buildCanvasSchemaContent = buildCanvasSchemaContent;
|
|
917
|
+
module.exports.buildCanvasSavePayload = buildCanvasSavePayload;
|
|
797
918
|
module.exports.sendSaveRequestWithAuth = sendSaveRequestWithAuth;
|
|
919
|
+
module.exports.sendCanvasSaveRequestWithAuth = sendCanvasSaveRequestWithAuth;
|
|
798
920
|
}
|
|
@@ -1569,8 +1569,8 @@ function summarizeLocalizedCommands(commands) {
|
|
|
1569
1569
|
plan_command_ids: ['design-plan.init', 'design-plan.preview', 'design-plan.materialize', 'design-plan.patch'],
|
|
1570
1570
|
theme_command_ids: ['sample', 'create-app', 'update-app'],
|
|
1571
1571
|
navigation_command_ids: { platform: ['update-app', 'nav-group'], custom: ['update-app', 'update-form-config', 'get-form-config'] },
|
|
1572
|
-
navigation_policy: 'Before PRD planning,
|
|
1573
|
-
design_mode_policy: 'Analyze requirements first. First-time builds include new apps and existing apps without business pages. Reuse detailed supplied plans; if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. Confirm unresolved
|
|
1572
|
+
navigation_policy: 'Before PRD planning in Fast and Plan, the agent determines navigation ownership and layout from business context. Preserve explicit user requirements and existing navigation for local changes; resource-only scope must not trigger app setting changes. Use native navigation for form, approval and data-management workflows; use custom navigation for persistent menus and content state, branded menus or special interactions. Portal/dashboard/homepage labels and page-local tabs alone imply native navigation. Choose layout from module count, hierarchy, switching frequency, content width and device: top for few shallow modules and wide content, side for many frequent modules, L-shaped/mixed for two-level domains, custom dock for lightweight mobile entries. Preserve user-specified layouts. Store resolved type/source/reason/variant in the brief: ai_default for agent ownership decisions, user_selected for explicit user ownership; distinguish user requirements from agent reasoning. Include navigation in the overall Plan confirmation; Fast implements the recorded navigation decision. Platform navigation uses update-app --layout top|side|l_shape --show-app-nav, which writes layoutDirection=top|side|l_shape and hideAppNav=n. Preserve stored navType unchanged; do not derive or invent it and do not add a --nav-type option. Without --layout, normalize stored legacy layoutDirection and navType using the yc-utils application rules (hoz plus top_side means l_shape; missing layout uses top_fold/top_side for top/l_shape). Shell page-level top_fold/none overrides do not belong in app settings. Custom side/top/mixed/dock is a page layout and requires app and per-page navigation hiding with readback. Verify persisted layoutDirection and hideAppNav; updatedFields and themeVerification do not prove navigation persistence. Navigation tone is separate from layout.',
|
|
1573
|
+
design_mode_policy: 'Analyze requirements first. First-time builds include new apps and existing apps without business pages. Reuse detailed supplied plans; if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. Confirm unresolved visual style and page scope before planning; apply navigation_policy. Existing business apps only clarify the current change. A named resource-only request with explicitScope.allowInferredResources=false skips unrelated navigation and visual questions. Standard Plan uses design-plan init parallelTasks, then executes the returned materialize.command exactly once with business-file and visual-file; preview and --from-preview are only for explicitly incremental large plans. Confirm the displayed revision before building, and do not materialize or patch again after confirmation.',
|
|
1574
1574
|
product_design_policy: 'yida-requirement-analysis owns shared facts and first-time intake. yida-prd owns business planning; yida-design owns visual design. Reuse the same confirmed brief and supplied details. Prepare business and base visuals concurrently, then bind visuals to settled page tasks. yida-app merges and checks the artifacts before creating resources. Plan uses the compact authoring contract and selected theme context; the CLI reads full templates and renders all artifacts.',
|
|
1575
1575
|
ui_guidance_policy: 'Page implementation consumes yida-prd prd.md for positioning, information architecture, page prototype, native form entry policy, material strategy, business-specific checks, resource creation order, page implementation delivery order, navigation order, and acceptance criteria; it consumes yida-design design.md for app custom theme CSS delivery, themeColor/navTheme, visual states, visualScaffold, surface material, rounded rules, density rules, components, state styling, and page-level imageNeed. When a page is imageNeed=required, or beneficial with declared image slots, yida-image-assets prepares a traceable asset manifest before that page is implemented; imageNeed=none skips the branch. prd.md and design.md remain the only design sources of truth. Page implementation may extract page-spec.json as a derived implementation handoff from prd.md + design.md; conflicts are resolved by sending business conflicts to yida-prd and visual conflicts to yida-design before regenerating the spec. Core normal forms default to 1-3 business sample records before page implementation, followed by query readback. An explicit opt-out, configuration dictionary, sensitive data, or lack of safely constructible values requires a recorded skip reason. Screenshots, public sharing, data-source deep binding, and fine navigation grouping are optional after explicit user request or PRD acceptance criteria.',
|
|
1576
1576
|
default_nav_order_policy: 'For custom navigation, implement PRD navigation order in the custom shell and verify app/page navigation hiding; skip platform nav-group ordering. For platform navigation: after the primary page is successfully published, perform exactly one navigation order operation. If the PRD names a navigation order, publish without --auto-nav-order and then call openyida nav-group order <appType> <items...>. If the PRD only gives broad groups or is missing navigation order, use openyida publish ... --auto-nav-order and do not call nav-group order or auto-order afterward. Explicit and automatic ordering are mutually exclusive; never generate per-item move loops. The fallback priority is portal/home/workbench entry > business handling > data management > business analytics > system configuration.',
|