openyida 2026.7.23-1 → 2026.7.23-2
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 +18 -0
- package/lib/app/create-form.js +58 -0
- package/lib/app/generate-page.js +12 -1
- package/lib/app/page-compiler.js +15 -0
- package/lib/app/page-linter.js +26 -0
- package/lib/app/publish.js +69 -19
- package/lib/app/services/canvas-page-compiler.js +15 -1
- package/lib/app/services/canvas-page-schema-builder.js +22 -1
- package/lib/app/services/form-compiler.js +42 -1
- package/lib/app/services/native-page-compiler.js +12 -0
- package/lib/core/locales/en.js +1 -0
- package/lib/core/locales/zh.js +1 -0
- package/lib/core/no-emoji-detector.js +261 -0
- package/lib/core/no-emoji-guard.js +59 -0
- package/package.json +1 -1
- package/yida-skills/SKILL.md +1 -1
- package/yida-skills/skills/yida-app/SKILL.md +1 -0
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +1 -0
- package/yida-skills/skills/yida-create-form-page/SKILL.md +1 -0
- package/yida-skills/skills/yida-custom-page/SKILL.md +1 -1
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
const Babel = require('@babel/standalone');
|
|
32
|
+
const {
|
|
33
|
+
assertNoEmojiInArtifactName,
|
|
34
|
+
assertNoEmojiInText,
|
|
35
|
+
} = require('../core/no-emoji-guard');
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
38
|
* 依赖 → window 别名白名单。
|
|
@@ -260,6 +264,16 @@ function esmToWindowPlugin({ types: t }, options = {}) {
|
|
|
260
264
|
* @returns {{ runtimeCode: string, importedModules: string }}
|
|
261
265
|
*/
|
|
262
266
|
function compileCanvasLocal(source, options = {}) {
|
|
267
|
+
if (options.sourcePath) {
|
|
268
|
+
assertNoEmojiInArtifactName(options.sourcePath, {
|
|
269
|
+
code: 'OPENYIDA_PAGE_FILENAME_EMOJI_FORBIDDEN',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
assertNoEmojiInText(source, {
|
|
273
|
+
artifact: options.sourcePath || 'Code Canvas source',
|
|
274
|
+
code: 'OPENYIDA_CANVAS_SOURCE_EMOJI_FORBIDDEN',
|
|
275
|
+
});
|
|
276
|
+
|
|
263
277
|
const importedModules = extractImportedModules(source);
|
|
264
278
|
|
|
265
279
|
// 第一步:剥类型 + 转 JSX(classic runtime,产出 React.createElement,
|
|
@@ -304,6 +318,10 @@ function compileCanvasLocal(source, options = {}) {
|
|
|
304
318
|
});
|
|
305
319
|
|
|
306
320
|
const runtimeCode = stage2.code || '';
|
|
321
|
+
assertNoEmojiInText(runtimeCode, {
|
|
322
|
+
artifact: options.sourcePath ? options.sourcePath + ' runtime' : 'Code Canvas runtime',
|
|
323
|
+
code: 'OPENYIDA_CANVAS_SOURCE_EMOJI_FORBIDDEN',
|
|
324
|
+
});
|
|
307
325
|
return {
|
|
308
326
|
runtimeCode,
|
|
309
327
|
importedModules: JSON.stringify(importedModules),
|
package/lib/app/create-form.js
CHANGED
|
@@ -64,6 +64,10 @@ const { createAuthRef } = require('../core/yida-client');
|
|
|
64
64
|
const { CliError } = require('../core/cli-error');
|
|
65
65
|
const { t } = require('../core/i18n');
|
|
66
66
|
const { safeParseJson } = require('../core/safe-json');
|
|
67
|
+
const {
|
|
68
|
+
assertNoEmojiInArtifactName,
|
|
69
|
+
assertNoEmojiInValue,
|
|
70
|
+
} = require('../core/no-emoji-guard');
|
|
67
71
|
const { buildYidaI18n, normalizeYidaLocale, resolveContentLocale } = require('../core/yida-i18n');
|
|
68
72
|
const { banner, step, label, success, fail, warn, info, error, result, usage, hint, listItem } = require('../core/chalk');
|
|
69
73
|
const { parseOpenOption: parseBrowserOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
|
|
@@ -381,6 +385,27 @@ function validateContainerChildrenArray(field, fieldPath, componentName) {
|
|
|
381
385
|
return field.children;
|
|
382
386
|
}
|
|
383
387
|
|
|
388
|
+
function hasNonEmptyAssociationFormUuid(field) {
|
|
389
|
+
const associationForm = field && field.associationForm;
|
|
390
|
+
if (!associationForm || typeof associationForm !== 'object' || Array.isArray(associationForm)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
return typeof associationForm.formUuid === 'string' && associationForm.formUuid.trim().length > 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function validateAssociationFormFieldDefinition(field, fieldPath) {
|
|
397
|
+
if (hasNonEmptyAssociationFormUuid(field)) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
throwInvalidFieldDefinition(
|
|
401
|
+
'AssociationFormField.associationForm.formUuid 必须是非空字符串: ' + fieldPath,
|
|
402
|
+
'CREATE_FORM_ASSOCIATION_FORM_UUID_MISSING',
|
|
403
|
+
field,
|
|
404
|
+
fieldPath,
|
|
405
|
+
{ componentName: 'AssociationFormField' }
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
384
409
|
function validateFieldDefinition(field, fieldPath, options) {
|
|
385
410
|
const validationOptions = options || {};
|
|
386
411
|
validatePlainFieldObject(field, fieldPath);
|
|
@@ -438,6 +463,10 @@ function validateFieldDefinition(field, fieldPath, options) {
|
|
|
438
463
|
);
|
|
439
464
|
}
|
|
440
465
|
|
|
466
|
+
if (componentName === 'AssociationFormField') {
|
|
467
|
+
validateAssociationFormFieldDefinition(field, fieldPath);
|
|
468
|
+
}
|
|
469
|
+
|
|
441
470
|
if (componentName === 'TableField') {
|
|
442
471
|
validateTableFieldChildren(field, fieldPath, validationOptions);
|
|
443
472
|
}
|
|
@@ -4334,6 +4363,27 @@ function emitCreateFormPostCreateFailure(context) {
|
|
|
4334
4363
|
}
|
|
4335
4364
|
}
|
|
4336
4365
|
|
|
4366
|
+
function assertNoEmojiInFormDefinition(formTitle, fields, validations, artifact) {
|
|
4367
|
+
assertNoEmojiInValue({
|
|
4368
|
+
formTitle,
|
|
4369
|
+
fields,
|
|
4370
|
+
validations,
|
|
4371
|
+
}, {
|
|
4372
|
+
artifact: artifact || 'create-form input',
|
|
4373
|
+
code: 'OPENYIDA_FORM_DEFINITION_EMOJI_FORBIDDEN',
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
function assertNoEmojiInDefinitionFileName(value) {
|
|
4378
|
+
const raw = String(value || '').trim();
|
|
4379
|
+
if (!raw || raw.startsWith('{') || raw.startsWith('[')) {
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
assertNoEmojiInArtifactName(raw, {
|
|
4383
|
+
code: 'OPENYIDA_FORM_DEFINITION_FILENAME_EMOJI_FORBIDDEN',
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4337
4387
|
// ── 保存 Schema 并更新表单配置(create/update 共用)──
|
|
4338
4388
|
//
|
|
4339
4389
|
// 封装了 saveFormSchema + updateFormConfig 两步,以及各自的 302 自动重登录重试。
|
|
@@ -4348,6 +4398,10 @@ async function saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, ver
|
|
|
4348
4398
|
info(t('create_form.formula_prefix_fixed', fixedRefs));
|
|
4349
4399
|
}
|
|
4350
4400
|
ensureDividerThemeAction(schema);
|
|
4401
|
+
assertNoEmojiInValue(schema, {
|
|
4402
|
+
artifact: 'Yida form schema ' + formUuid,
|
|
4403
|
+
code: 'OPENYIDA_FORM_SCHEMA_EMOJI_FORBIDDEN',
|
|
4404
|
+
});
|
|
4351
4405
|
const serverRevision = requireSchemaServerRevision(version, { appType, formUuid });
|
|
4352
4406
|
|
|
4353
4407
|
step(saveStep, t('create_form.step_save_schema', saveStep));
|
|
@@ -4411,7 +4465,9 @@ async function mainCreate(parsedArgs, authRef) {
|
|
|
4411
4465
|
label('Fields:', fieldsJsonOrFile);
|
|
4412
4466
|
|
|
4413
4467
|
step(2, t('create_form.step_read_fields', 2));
|
|
4468
|
+
assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
|
|
4414
4469
|
const { fields, columns, validations } = readFieldsDefinition(fieldsJsonOrFile);
|
|
4470
|
+
assertNoEmojiInFormDefinition(formTitle, fields, validations);
|
|
4415
4471
|
validateFormFieldDefinitions(fields);
|
|
4416
4472
|
const fieldCount = countDataFieldDefinitions(fields);
|
|
4417
4473
|
success(t('create_form.fields_loaded', fieldCount));
|
|
@@ -4559,7 +4615,9 @@ async function createFormForLegacyProcess(context, input) {
|
|
|
4559
4615
|
};
|
|
4560
4616
|
|
|
4561
4617
|
const { appType, formTitle, fieldsJsonOrFile, layout, theme, labelAlign } = parsedArgs;
|
|
4618
|
+
assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
|
|
4562
4619
|
const { fields, validations } = readFieldsDefinition(fieldsJsonOrFile);
|
|
4620
|
+
assertNoEmojiInFormDefinition(formTitle, fields, validations, 'legacy create-form input');
|
|
4563
4621
|
validateFormFieldDefinitions(fields);
|
|
4564
4622
|
const fieldCount = countDataFieldDefinitions(fields);
|
|
4565
4623
|
const createResult = await requestWithAutoLogin(function (auth) {
|
package/lib/app/generate-page.js
CHANGED
|
@@ -6,6 +6,10 @@ const { applyTemplateVariables } = require('../core/sample');
|
|
|
6
6
|
const { t } = require('../core/i18n');
|
|
7
7
|
const { safeParseJson } = require('../core/safe-json');
|
|
8
8
|
const { error, success, hint, result, warn } = require('../core/chalk');
|
|
9
|
+
const {
|
|
10
|
+
assertNoEmojiInArtifactName,
|
|
11
|
+
assertNoEmojiInText,
|
|
12
|
+
} = require('../core/no-emoji-guard');
|
|
9
13
|
const { runLintCheck } = require('./page-linter');
|
|
10
14
|
const { compileSource } = require('./page-compiler');
|
|
11
15
|
const { compileCanvasLocal } = require('./canvas-compile');
|
|
@@ -571,6 +575,9 @@ async function run(args) {
|
|
|
571
575
|
options.output || spec.output || (mode === 'canvas' ? templateConfig.defaultCanvasOutput : templateConfig.defaultNativeOutput)
|
|
572
576
|
);
|
|
573
577
|
const outputPath = outputResolution.outputPath;
|
|
578
|
+
assertNoEmojiInArtifactName(outputPath, {
|
|
579
|
+
code: 'OPENYIDA_PAGE_FILENAME_EMOJI_FORBIDDEN',
|
|
580
|
+
});
|
|
574
581
|
const manifestPath = getManifestPath(outputPath);
|
|
575
582
|
const templateSource = fs.readFileSync(templateFile, 'utf-8');
|
|
576
583
|
const ir = normalizePageSpec(spec, {
|
|
@@ -593,6 +600,10 @@ async function run(args) {
|
|
|
593
600
|
|
|
594
601
|
const variables = buildTemplateVariablesFromIr(ir);
|
|
595
602
|
const outputSource = applyTemplateVariables(templateSource, variables);
|
|
603
|
+
assertNoEmojiInText(outputSource, {
|
|
604
|
+
artifact: outputPath,
|
|
605
|
+
code: 'OPENYIDA_GENERATED_PAGE_EMOJI_FORBIDDEN',
|
|
606
|
+
});
|
|
596
607
|
|
|
597
608
|
ensureOutputDir(outputPath);
|
|
598
609
|
fs.writeFileSync(outputPath, outputSource, 'utf-8');
|
|
@@ -608,7 +619,7 @@ async function run(args) {
|
|
|
608
619
|
reportNavigationConfig(ir);
|
|
609
620
|
|
|
610
621
|
if (mode === 'canvas') {
|
|
611
|
-
const canvasResult = compileCanvasLocal(outputSource);
|
|
622
|
+
const canvasResult = compileCanvasLocal(outputSource, { sourcePath: outputPath });
|
|
612
623
|
const importedModules = JSON.parse(canvasResult.importedModules);
|
|
613
624
|
if (options.compile || spec.compile === true) {
|
|
614
625
|
const distPath = getCanvasDistPath(outputPath);
|
package/lib/app/page-compiler.js
CHANGED
|
@@ -7,6 +7,10 @@ const { default: babelTransform } = require('../core/babel-transform');
|
|
|
7
7
|
const { findProjectRoot } = require('../core/utils');
|
|
8
8
|
const { t } = require('../core/i18n');
|
|
9
9
|
const { info, success, error } = require('../core/chalk');
|
|
10
|
+
const {
|
|
11
|
+
assertNoEmojiInArtifactName,
|
|
12
|
+
assertNoEmojiInText,
|
|
13
|
+
} = require('../core/no-emoji-guard');
|
|
10
14
|
const { ensureYidaRuntimeContract } = require('./page-compat');
|
|
11
15
|
const { warnLargePageSource } = require('./page-size-guard');
|
|
12
16
|
|
|
@@ -16,6 +20,9 @@ const { warnLargePageSource } = require('./page-size-guard');
|
|
|
16
20
|
* @returns {{ sourceCode: string, compiledCode: string, compiledPath: string }}
|
|
17
21
|
*/
|
|
18
22
|
function compileSource(sourcePath) {
|
|
23
|
+
assertNoEmojiInArtifactName(sourcePath, {
|
|
24
|
+
code: 'OPENYIDA_PAGE_FILENAME_EMOJI_FORBIDDEN',
|
|
25
|
+
});
|
|
19
26
|
const sourceFileName = path.basename(sourcePath);
|
|
20
27
|
const parsedPath = path.parse(sourcePath);
|
|
21
28
|
const compiledFileName = `${parsedPath.name}.js`;
|
|
@@ -26,6 +33,10 @@ function compileSource(sourcePath) {
|
|
|
26
33
|
warnLargePageSource(rawSourceCode, sourcePath);
|
|
27
34
|
const runtimeResult = ensureYidaRuntimeContract(rawSourceCode);
|
|
28
35
|
const sourceCode = runtimeResult.code;
|
|
36
|
+
assertNoEmojiInText(sourceCode, {
|
|
37
|
+
artifact: sourcePath,
|
|
38
|
+
code: 'OPENYIDA_PAGE_SOURCE_EMOJI_FORBIDDEN',
|
|
39
|
+
});
|
|
29
40
|
|
|
30
41
|
info(t('publish.compiling', sourceFileName));
|
|
31
42
|
const babelResult = babelTransform(sourceCode, {}, false, { RE_VERSION: '7.4.0' });
|
|
@@ -46,6 +57,10 @@ function compileSource(sourcePath) {
|
|
|
46
57
|
if (uglifyResult.error) {
|
|
47
58
|
error(t('publish.minify_failed', uglifyResult.error.message || String(uglifyResult.error)));
|
|
48
59
|
}
|
|
60
|
+
assertNoEmojiInText(uglifyResult.code, {
|
|
61
|
+
artifact: compiledPath,
|
|
62
|
+
code: 'OPENYIDA_PAGE_SOURCE_EMOJI_FORBIDDEN',
|
|
63
|
+
});
|
|
49
64
|
|
|
50
65
|
const outputDir = path.dirname(compiledPath);
|
|
51
66
|
if (!fs.existsSync(outputDir)) {
|
package/lib/app/page-linter.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const { t } = require('../core/i18n');
|
|
4
4
|
const { warn, fail, hint, success } = require('../core/chalk');
|
|
5
|
+
const {
|
|
6
|
+
findEmojiInArtifactName,
|
|
7
|
+
findEmojiInText,
|
|
8
|
+
} = require('../core/no-emoji-guard');
|
|
5
9
|
const Babel = require('@babel/standalone');
|
|
6
10
|
|
|
7
11
|
const THEN_CALLBACK_LINE_LIMIT = 50;
|
|
@@ -663,6 +667,28 @@ function lintYidaSource(sourceCode, filePath) {
|
|
|
663
667
|
const lines = sourceCode.split('\n');
|
|
664
668
|
const disableMap = buildLineDisableMap(lines);
|
|
665
669
|
|
|
670
|
+
findEmojiInArtifactName(filePath, {
|
|
671
|
+
artifact: filePath || 'Yida page source',
|
|
672
|
+
maxIssues: 20,
|
|
673
|
+
}).forEach((issue) => {
|
|
674
|
+
errors.push({
|
|
675
|
+
line: 1,
|
|
676
|
+
rule: 'emoji-forbidden',
|
|
677
|
+
message: t('publish.lint_emoji_forbidden', issue.emoji),
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
findEmojiInText(sourceCode, {
|
|
682
|
+
artifact: filePath || 'Yida page source',
|
|
683
|
+
maxIssues: 20,
|
|
684
|
+
}).forEach((issue) => {
|
|
685
|
+
errors.push({
|
|
686
|
+
line: issue.line,
|
|
687
|
+
rule: 'emoji-forbidden',
|
|
688
|
+
message: t('publish.lint_emoji_forbidden', issue.emoji),
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
666
692
|
const hasRenderJsx = /export\s+function\s+renderJsx\s*\(/.test(sourceCode);
|
|
667
693
|
if (!hasRenderJsx) {
|
|
668
694
|
pushIssue(errors, 1, 'missing-render-jsx', t('publish.lint_missing_render_jsx'), disableMap);
|
package/lib/app/publish.js
CHANGED
|
@@ -39,6 +39,7 @@ const { buildPageFile, shouldBuildPageSource } = require('./page-compat');
|
|
|
39
39
|
const { warnLargePageSource } = require('./page-size-guard');
|
|
40
40
|
const { fetchFormPageList } = require('./form-navigation');
|
|
41
41
|
const { parseOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
|
|
42
|
+
const { assertNoEmojiInValue } = require('../core/no-emoji-guard');
|
|
42
43
|
const {
|
|
43
44
|
assertLegacyDirectWriteAllowed,
|
|
44
45
|
extractLegacyGuardArgs,
|
|
@@ -193,6 +194,14 @@ async function readExistingPageDataSource(appType, formUuid, authRef) {
|
|
|
193
194
|
|
|
194
195
|
function buildSchemaContent(sourceCode, compiledCode, formUuid, options = {}) {
|
|
195
196
|
const logInfo = options.silent === true ? () => {} : info;
|
|
197
|
+
assertNoEmojiInValue({
|
|
198
|
+
sourceCode,
|
|
199
|
+
compiledCode,
|
|
200
|
+
existingDataSource: options.existingDataSource,
|
|
201
|
+
}, {
|
|
202
|
+
artifact: 'native page schema ' + formUuid,
|
|
203
|
+
code: 'OPENYIDA_PAGE_SCHEMA_EMOJI_FORBIDDEN',
|
|
204
|
+
});
|
|
196
205
|
return buildNativePageSchemaContent(sourceCode, compiledCode, formUuid, {
|
|
197
206
|
...options,
|
|
198
207
|
onBuildingSchema: () => logInfo(t('publish.building_schema')),
|
|
@@ -210,6 +219,14 @@ function buildSchemaContent(sourceCode, compiledCode, formUuid, options = {}) {
|
|
|
210
219
|
|
|
211
220
|
function buildCanvasSchemaContent(sourceCode, runtimeCode, importedModules, formUuid) {
|
|
212
221
|
info(t('publish.building_schema'));
|
|
222
|
+
assertNoEmojiInValue({
|
|
223
|
+
sourceCode,
|
|
224
|
+
runtimeCode,
|
|
225
|
+
importedModules,
|
|
226
|
+
}, {
|
|
227
|
+
artifact: 'canvas page schema ' + formUuid,
|
|
228
|
+
code: 'OPENYIDA_PAGE_SCHEMA_EMOJI_FORBIDDEN',
|
|
229
|
+
});
|
|
213
230
|
return buildCanvasPageSchemaContent(sourceCode, runtimeCode, importedModules, formUuid);
|
|
214
231
|
}
|
|
215
232
|
|
|
@@ -573,12 +590,31 @@ async function ensurePublishTargetOrExit(appType, formUuid, authRef, options = {
|
|
|
573
590
|
|
|
574
591
|
function sendHealthCheckRequest(pageUrl, cookies) {
|
|
575
592
|
return new Promise((resolve) => {
|
|
576
|
-
|
|
593
|
+
let parsedUrl;
|
|
594
|
+
try {
|
|
595
|
+
parsedUrl = new URL(pageUrl);
|
|
596
|
+
} catch (urlError) {
|
|
597
|
+
resolve({ ok: false, statusCode: 0, error: urlError.message });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
577
600
|
const isHttps = parsedUrl.protocol === 'https:';
|
|
578
601
|
const requestModule = isHttps ? https : http;
|
|
579
|
-
const cookieHeader = cookies
|
|
602
|
+
const cookieHeader = Array.isArray(cookies) ? cookies
|
|
603
|
+
.filter((cookie) => cookie && cookie.name !== undefined && cookie.value !== undefined)
|
|
580
604
|
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
581
|
-
.join('; ');
|
|
605
|
+
.join('; ') : '';
|
|
606
|
+
|
|
607
|
+
if (!cookieHeader) {
|
|
608
|
+
resolve({
|
|
609
|
+
ok: false,
|
|
610
|
+
skipped: true,
|
|
611
|
+
reason: 'missing_cookies',
|
|
612
|
+
statusCode: 0,
|
|
613
|
+
error: t('common.request_failed'),
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
582
618
|
const requestOptions = {
|
|
583
619
|
hostname: parsedUrl.hostname,
|
|
584
620
|
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
@@ -591,22 +627,28 @@ function sendHealthCheckRequest(pageUrl, cookies) {
|
|
|
591
627
|
timeout: 30000,
|
|
592
628
|
};
|
|
593
629
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
630
|
+
let request;
|
|
631
|
+
try {
|
|
632
|
+
request = requestModule.request(requestOptions, (response) => {
|
|
633
|
+
let responseData = '';
|
|
634
|
+
response.on('data', (chunk) => {
|
|
635
|
+
if (responseData.length < 2000) {
|
|
636
|
+
responseData += chunk;
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
response.on('end', () => {
|
|
640
|
+
const statusCode = response.statusCode || 0;
|
|
641
|
+
resolve({
|
|
642
|
+
ok: statusCode >= 200 && statusCode < 400,
|
|
643
|
+
statusCode,
|
|
644
|
+
snippet: responseData.substring(0, 300).replace(/\s+/g, ' ').trim(),
|
|
645
|
+
});
|
|
607
646
|
});
|
|
608
647
|
});
|
|
609
|
-
})
|
|
648
|
+
} catch (requestError) {
|
|
649
|
+
resolve({ ok: false, statusCode: 0, error: requestError.message });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
610
652
|
|
|
611
653
|
request.on('timeout', () => {
|
|
612
654
|
request.destroy();
|
|
@@ -663,7 +705,7 @@ async function main(argv) {
|
|
|
663
705
|
step(1, t('publish.step_canvas_compile'));
|
|
664
706
|
info(t('publish.canvas_compiling'));
|
|
665
707
|
try {
|
|
666
|
-
canvasResult = await compileCanvas(initialSourceCode);
|
|
708
|
+
canvasResult = await compileCanvas(initialSourceCode, { sourcePath });
|
|
667
709
|
} catch (canvasCompileError) {
|
|
668
710
|
fail(t('publish.canvas_compile_failed', canvasCompileError.message));
|
|
669
711
|
process.exit(1);
|
|
@@ -776,7 +818,15 @@ async function main(argv) {
|
|
|
776
818
|
let healthCheckResult = null;
|
|
777
819
|
if (healthCheck) {
|
|
778
820
|
step(5, t('publish.step_health_check'));
|
|
779
|
-
|
|
821
|
+
try {
|
|
822
|
+
healthCheckResult = await sendHealthCheckRequest(pageUrl, cookies);
|
|
823
|
+
} catch (healthCheckError) {
|
|
824
|
+
healthCheckResult = {
|
|
825
|
+
ok: false,
|
|
826
|
+
statusCode: 0,
|
|
827
|
+
error: healthCheckError && healthCheckError.message ? healthCheckError.message : t('common.unknown_error'),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
780
830
|
if (healthCheckResult.ok) {
|
|
781
831
|
success(t('publish.health_check_ok', healthCheckResult.statusCode));
|
|
782
832
|
} else {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { compileCanvasLocal } = require('../canvas-compile');
|
|
5
|
+
const { assertNoEmojiInText } = require('../../core/no-emoji-guard');
|
|
5
6
|
const { schemaError } = require('../../schema/errors');
|
|
6
7
|
const { isLoadedPageSource } = require('../../schema/page-source-loader');
|
|
7
8
|
|
|
@@ -17,10 +18,23 @@ function compileCanvasPageSource(loadedSource) {
|
|
|
17
18
|
stage: 'canvas',
|
|
18
19
|
});
|
|
19
20
|
}
|
|
21
|
+
try {
|
|
22
|
+
assertNoEmojiInText(loadedSource.source, {
|
|
23
|
+
artifact: loadedSource.relativePath || 'Schema Canvas page source',
|
|
24
|
+
code: 'SCHEMA_PAGE_SOURCE_EMOJI_FORBIDDEN',
|
|
25
|
+
});
|
|
26
|
+
} catch (error) {
|
|
27
|
+
compilerError('SCHEMA_PAGE_SOURCE_EMOJI_FORBIDDEN', error.message, {
|
|
28
|
+
stage: 'emoji',
|
|
29
|
+
issues: error.details && error.details.issues,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
20
32
|
|
|
21
33
|
let canvasResult;
|
|
22
34
|
try {
|
|
23
|
-
canvasResult = compileCanvasLocal(loadedSource.source
|
|
35
|
+
canvasResult = compileCanvasLocal(loadedSource.source, {
|
|
36
|
+
sourcePath: loadedSource.relativePath,
|
|
37
|
+
});
|
|
24
38
|
} catch (error) {
|
|
25
39
|
compilerError('SCHEMA_PAGE_SOURCE_COMPILE_FAILED', 'Canvas page source could not be compiled.', {
|
|
26
40
|
stage: 'canvas',
|
|
@@ -5,6 +5,10 @@ const {
|
|
|
5
5
|
generateSuffix,
|
|
6
6
|
getGlobalDataSourceFitConfig,
|
|
7
7
|
} = require('./native-page-schema-builder');
|
|
8
|
+
const {
|
|
9
|
+
buildEmojiErrorMessage,
|
|
10
|
+
findEmojiInValue,
|
|
11
|
+
} = require('../../core/no-emoji-detector');
|
|
8
12
|
|
|
9
13
|
const CANVAS_ACTIONS_SOURCE = 'export function didMount() {}';
|
|
10
14
|
const CANVAS_ACTIONS_COMPILED = '"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.didMount=didMount;function didMount(){}';
|
|
@@ -32,7 +36,7 @@ function buildCanvasPageSchemaObject(sourceCode, runtimeCode, importedModules, f
|
|
|
32
36
|
);
|
|
33
37
|
const constructorCode = "function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}";
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
const schema = {
|
|
36
40
|
schemaType: 'superform',
|
|
37
41
|
schemaVersion: '5.0',
|
|
38
42
|
pages: [
|
|
@@ -129,6 +133,23 @@ function buildCanvasPageSchemaObject(sourceCode, runtimeCode, importedModules, f
|
|
|
129
133
|
},
|
|
130
134
|
config: { connectComponent: [] },
|
|
131
135
|
};
|
|
136
|
+
assertCanvasSchemaHasNoEmoji(schema, formUuid);
|
|
137
|
+
return schema;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function assertCanvasSchemaHasNoEmoji(schema, formUuid) {
|
|
141
|
+
const artifact = 'canvas page schema ' + formUuid;
|
|
142
|
+
const issues = findEmojiInValue(schema, {
|
|
143
|
+
artifact,
|
|
144
|
+
});
|
|
145
|
+
if (issues.length === 0) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const error = new Error(buildEmojiErrorMessage(issues, { artifact }));
|
|
150
|
+
error.code = 'OPENYIDA_PAGE_SCHEMA_EMOJI_FORBIDDEN';
|
|
151
|
+
error.details = { artifact, issues };
|
|
152
|
+
throw error;
|
|
132
153
|
}
|
|
133
154
|
|
|
134
155
|
function resolveSchemaBuilderDependency(value, createDefault, property) {
|
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
deepMerge,
|
|
8
8
|
normalizeFieldValidationRules,
|
|
9
9
|
} = require('./form-validation');
|
|
10
|
+
const { assertNoEmojiInValue } = require('../../core/no-emoji-guard');
|
|
10
11
|
|
|
11
12
|
const OPTION_FIELD_TYPES = Object.freeze(['RadioField', 'SelectField', 'CheckboxField', 'MultiSelectField']);
|
|
12
13
|
const TABLE_FIELD_TYPE = 'TableField';
|
|
@@ -574,6 +575,38 @@ function assertSupportedFieldDefinition(field, details) {
|
|
|
574
575
|
return componentName;
|
|
575
576
|
}
|
|
576
577
|
|
|
578
|
+
function hasNonEmptyAssociationFormUuid(field) {
|
|
579
|
+
const associationForm = field && field.associationForm;
|
|
580
|
+
if (!associationForm || typeof associationForm !== 'object' || Array.isArray(associationForm)) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
return typeof associationForm.formUuid === 'string' && associationForm.formUuid.trim().length > 0;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function hasManagedAssociationFormReference(field) {
|
|
587
|
+
if (!field || field.form === undefined || field.form === null) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
const formReference = String(field.form).trim();
|
|
591
|
+
return /^form:[A-Za-z][A-Za-z0-9_-]*$/.test(formReference);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function assertAssociationFormFieldDefinition(field, details) {
|
|
595
|
+
if (hasNonEmptyAssociationFormUuid(field) || hasManagedAssociationFormReference(field)) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
throw createFormCompilerError(
|
|
599
|
+
'AssociationFormField 必须配置 associationForm.formUuid 或 form: 引用',
|
|
600
|
+
'FORM_COMPILER_ASSOCIATION_FORM_UUID_MISSING',
|
|
601
|
+
Object.assign({
|
|
602
|
+
label: field && field.label,
|
|
603
|
+
title: field && field.title,
|
|
604
|
+
form: field && field.form,
|
|
605
|
+
componentName: ASSOCIATION_FORM_FIELD_TYPE,
|
|
606
|
+
}, details || {})
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
577
610
|
const COMPONENT_ALIAS_META = Symbol('openyida.componentAlias');
|
|
578
611
|
|
|
579
612
|
function normalizeComponentAlias(field) {
|
|
@@ -612,6 +645,9 @@ function buildFieldComponent(field, options) {
|
|
|
612
645
|
const semanticPath = semanticKey
|
|
613
646
|
? (compilerOptions.semanticPath ? compilerOptions.semanticPath + '.' + semanticKey : semanticKey)
|
|
614
647
|
: '';
|
|
648
|
+
if (componentName === ASSOCIATION_FORM_FIELD_TYPE) {
|
|
649
|
+
assertAssociationFormFieldDefinition(field, { semanticPath });
|
|
650
|
+
}
|
|
615
651
|
const fieldId = resolveFieldId(componentName, semanticPath, compilerOptions);
|
|
616
652
|
const nodeId = nextNodeId();
|
|
617
653
|
recordFieldBinding(compilerOptions, semanticPath, fieldId, componentName);
|
|
@@ -1743,7 +1779,7 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
|
|
|
1743
1779
|
};
|
|
1744
1780
|
|
|
1745
1781
|
// 顶层 Schema(与模板结构完全一致)- actions 和 config 与 pages 平级
|
|
1746
|
-
|
|
1782
|
+
const schema = {
|
|
1747
1783
|
schemaType: 'superform',
|
|
1748
1784
|
schemaVersion: '5.0',
|
|
1749
1785
|
pages: [pageSchema],
|
|
@@ -1767,6 +1803,11 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
|
|
|
1767
1803
|
connectComponent: [],
|
|
1768
1804
|
},
|
|
1769
1805
|
};
|
|
1806
|
+
assertNoEmojiInValue(schema, {
|
|
1807
|
+
artifact: 'compiled form schema ' + formUuid,
|
|
1808
|
+
code: 'OPENYIDA_FORM_SCHEMA_EMOJI_FORBIDDEN',
|
|
1809
|
+
});
|
|
1810
|
+
return schema;
|
|
1770
1811
|
}
|
|
1771
1812
|
|
|
1772
1813
|
function buildEmptyFormSchema() {
|
|
@@ -4,6 +4,7 @@ const crypto = require('crypto');
|
|
|
4
4
|
const UglifyJS = require('uglify-js');
|
|
5
5
|
const { default: babelTransform } = require('../../core/babel-transform');
|
|
6
6
|
const { buildPageSource } = require('../page-compat');
|
|
7
|
+
const { assertNoEmojiInText } = require('../../core/no-emoji-guard');
|
|
7
8
|
const { schemaError } = require('../../schema/errors');
|
|
8
9
|
const { isLoadedPageSource } = require('../../schema/page-source-loader');
|
|
9
10
|
|
|
@@ -32,6 +33,17 @@ function compileNativePageSource(loadedSource) {
|
|
|
32
33
|
stage: 'normalize',
|
|
33
34
|
});
|
|
34
35
|
}
|
|
36
|
+
try {
|
|
37
|
+
assertNoEmojiInText(buildResult.code, {
|
|
38
|
+
artifact: loadedSource.relativePath || 'Schema native page source',
|
|
39
|
+
code: 'SCHEMA_PAGE_SOURCE_EMOJI_FORBIDDEN',
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
compilerError('SCHEMA_PAGE_SOURCE_EMOJI_FORBIDDEN', error.message, {
|
|
43
|
+
stage: 'emoji',
|
|
44
|
+
issues: error.details && error.details.issues,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
35
47
|
|
|
36
48
|
let babelResult;
|
|
37
49
|
try {
|
package/lib/core/locales/en.js
CHANGED
|
@@ -1116,6 +1116,7 @@ Examples:
|
|
|
1116
1116
|
lint_import_require: 'Found import/require. Yida custom pages cannot import modules; load third-party scripts with this.utils.loadScript',
|
|
1117
1117
|
lint_react_hooks: 'Found React Hooks. Yida runtime uses React 16 class-component style and does not support useState/useEffect hooks',
|
|
1118
1118
|
lint_export_default: 'Found export default. Yida needs named exports such as export function renderJsx/didMount',
|
|
1119
|
+
lint_emoji_forbidden: 'Found emoji "{0}". OpenYida generated artifacts must not use emoji in UI copy, source comments, file paths, or code constants; use plain text, SVG, or icon components instead.',
|
|
1119
1120
|
lint_jsx_extension: 'This .js file contains JSX; prefer .jsx for source files. The compiled output is still .js',
|
|
1120
1121
|
lint_event_direct_method: 'Event binding passes this.xxx directly and will lose this; use (e) => { this.xxx(e); }',
|
|
1121
1122
|
lint_event_bind_this: 'Event binding uses .bind(this); use (e) => { this.xxx(e); } instead',
|
package/lib/core/locales/zh.js
CHANGED
|
@@ -1110,6 +1110,7 @@ openyida - 宜搭命令行工具
|
|
|
1110
1110
|
lint_import_require: '检测到 import/require。宜搭自定义页面禁止模块导入,第三方脚本应通过 this.utils.loadScript 加载',
|
|
1111
1111
|
lint_react_hooks: '检测到 React Hooks。宜搭运行时基于 React 16 类组件模式,不支持 useState/useEffect 等 Hooks',
|
|
1112
1112
|
lint_export_default: '检测到 export default。宜搭需要 export function renderJsx/didMount 等具名导出',
|
|
1113
|
+
lint_emoji_forbidden: '检测到 emoji「{0}」。OpenYida 生成产物禁止在 UI 文案、源码注释、文件路径或代码常量中使用 emoji;请改用普通文字、SVG 或图标组件。',
|
|
1113
1114
|
lint_jsx_extension: '当前文件是 .js 但包含 JSX,建议改为 .jsx,编译产物仍会输出 .js',
|
|
1114
1115
|
lint_event_direct_method: '事件绑定直接传入 this.xxx,会导致 this 丢失;请使用 (e) => { this.xxx(e); }',
|
|
1115
1116
|
lint_event_bind_this: '事件绑定使用了 .bind(this),请改为 (e) => { this.xxx(e); }',
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Covers common emoji blocks and Dingbats/Misc Symbols such as ✅, ⚠️, 📊, 🚀.
|
|
4
|
+
// Keep this detector scoped to generated artifacts; CLI status output may still
|
|
5
|
+
// use terminal glyphs independently.
|
|
6
|
+
const EMOJI_PATTERN = /(?:[0-9#*]\uFE0F?\u20E3|[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}](?:\uFE0F|\uFE0E)?(?:\u200D[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}](?:\uFE0F|\uFE0E)?)*)/gu;
|
|
7
|
+
const HEX_DIGITS = /^[0-9a-fA-F]+$/;
|
|
8
|
+
|
|
9
|
+
function normalizeArtifactName(value) {
|
|
10
|
+
return String(value || 'artifact');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getLineInfo(text, index) {
|
|
14
|
+
const before = text.slice(0, index);
|
|
15
|
+
const line = before.split(/\r\n|\r|\n/).length;
|
|
16
|
+
const lineStart = Math.max(before.lastIndexOf('\n'), before.lastIndexOf('\r')) + 1;
|
|
17
|
+
const nextNewline = text.indexOf('\n', index);
|
|
18
|
+
const lineEnd = nextNewline >= 0 ? nextNewline : text.length;
|
|
19
|
+
const column = index - lineStart + 1;
|
|
20
|
+
return {
|
|
21
|
+
line,
|
|
22
|
+
column,
|
|
23
|
+
excerpt: compactExcerpt(text.slice(lineStart, lineEnd), column),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function compactExcerpt(lineText, column) {
|
|
28
|
+
const normalized = String(lineText || '').replace(/\s+/g, ' ').trim();
|
|
29
|
+
if (normalized.length <= 120) {
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
const center = Math.max(0, column - 1);
|
|
33
|
+
const start = Math.max(0, center - 45);
|
|
34
|
+
const end = Math.min(normalized.length, start + 110);
|
|
35
|
+
return (start > 0 ? '...' : '') + normalized.slice(start, end) + (end < normalized.length ? '...' : '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createIssue(source, index, emoji, options, extra) {
|
|
39
|
+
const lineInfo = getLineInfo(source, index);
|
|
40
|
+
return Object.assign({
|
|
41
|
+
artifact: normalizeArtifactName(options.artifact),
|
|
42
|
+
path: options.path || options.valuePath || '',
|
|
43
|
+
emoji,
|
|
44
|
+
index,
|
|
45
|
+
line: lineInfo.line,
|
|
46
|
+
column: lineInfo.column,
|
|
47
|
+
excerpt: lineInfo.excerpt,
|
|
48
|
+
}, extra || {});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isEmojiCharacter(value) {
|
|
52
|
+
EMOJI_PATTERN.lastIndex = 0;
|
|
53
|
+
const matched = EMOJI_PATTERN.test(value);
|
|
54
|
+
EMOJI_PATTERN.lastIndex = 0;
|
|
55
|
+
return matched;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findRawEmojiInText(source, options) {
|
|
59
|
+
const issues = [];
|
|
60
|
+
EMOJI_PATTERN.lastIndex = 0;
|
|
61
|
+
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = EMOJI_PATTERN.exec(source)) !== null) {
|
|
64
|
+
issues.push(createIssue(source, match.index, match[0], options));
|
|
65
|
+
}
|
|
66
|
+
return issues;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readUnicodeEscapeAt(source, index) {
|
|
70
|
+
if (source[index] !== '\\' || source[index + 1] !== 'u') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (source[index + 2] === '{') {
|
|
75
|
+
const closeIndex = source.indexOf('}', index + 3);
|
|
76
|
+
if (closeIndex < 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const hex = source.slice(index + 3, closeIndex);
|
|
80
|
+
if (!hex || hex.length > 6 || !HEX_DIGITS.test(hex)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
84
|
+
if (!Number.isFinite(codePoint) || codePoint > 0x10FFFF) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
raw: source.slice(index, closeIndex + 1),
|
|
89
|
+
codePoint,
|
|
90
|
+
length: closeIndex + 1 - index,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hex = source.slice(index + 2, index + 6);
|
|
95
|
+
if (hex.length !== 4 || !HEX_DIGITS.test(hex)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
raw: source.slice(index, index + 6),
|
|
100
|
+
codePoint: Number.parseInt(hex, 16),
|
|
101
|
+
length: 6,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isHighSurrogate(codePoint) {
|
|
106
|
+
return codePoint >= 0xD800 && codePoint <= 0xDBFF;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isLowSurrogate(codePoint) {
|
|
110
|
+
return codePoint >= 0xDC00 && codePoint <= 0xDFFF;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function combineSurrogatePair(high, low) {
|
|
114
|
+
return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function codePointToString(codePoint) {
|
|
118
|
+
try {
|
|
119
|
+
return String.fromCodePoint(codePoint);
|
|
120
|
+
} catch {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findEscapedEmojiInText(source, options) {
|
|
126
|
+
const issues = [];
|
|
127
|
+
for (let index = 0; index < source.length; index++) {
|
|
128
|
+
const first = readUnicodeEscapeAt(source, index);
|
|
129
|
+
if (!first) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let raw = first.raw;
|
|
134
|
+
let codePoint = first.codePoint;
|
|
135
|
+
let length = first.length;
|
|
136
|
+
|
|
137
|
+
if (isHighSurrogate(first.codePoint)) {
|
|
138
|
+
const second = readUnicodeEscapeAt(source, index + first.length);
|
|
139
|
+
if (second && isLowSurrogate(second.codePoint)) {
|
|
140
|
+
raw += second.raw;
|
|
141
|
+
codePoint = combineSurrogatePair(first.codePoint, second.codePoint);
|
|
142
|
+
length += second.length;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const emoji = codePointToString(codePoint);
|
|
147
|
+
if (emoji && isEmojiCharacter(emoji)) {
|
|
148
|
+
issues.push(createIssue(source, index, emoji, options, {
|
|
149
|
+
escaped: true,
|
|
150
|
+
escape: raw,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
index += Math.max(length - 1, 0);
|
|
155
|
+
}
|
|
156
|
+
return issues;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function findEmojiInText(text, options = {}) {
|
|
160
|
+
const source = String(text || '');
|
|
161
|
+
const maxIssues = options.maxIssues || 20;
|
|
162
|
+
const scanOptions = {
|
|
163
|
+
artifact: normalizeArtifactName(options.artifact),
|
|
164
|
+
path: options.path || options.valuePath || '',
|
|
165
|
+
valuePath: options.valuePath,
|
|
166
|
+
};
|
|
167
|
+
return findRawEmojiInText(source, scanOptions)
|
|
168
|
+
.concat(findEscapedEmojiInText(source, scanOptions))
|
|
169
|
+
.sort((left, right) => left.index - right.index)
|
|
170
|
+
.slice(0, maxIssues);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function findEmojiInArtifactName(artifactName, options = {}) {
|
|
174
|
+
const artifact = normalizeArtifactName(options.artifact || artifactName);
|
|
175
|
+
return findEmojiInText(String(artifactName || ''), {
|
|
176
|
+
artifact,
|
|
177
|
+
path: options.path || 'filePath',
|
|
178
|
+
maxIssues: options.maxIssues,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function appendPath(parentPath, key) {
|
|
183
|
+
if (typeof key === 'number') {
|
|
184
|
+
return `${parentPath || '$'}[${key}]`;
|
|
185
|
+
}
|
|
186
|
+
if (/^[A-Za-z_$][\w$]*$/.test(String(key))) {
|
|
187
|
+
return parentPath ? `${parentPath}.${key}` : String(key);
|
|
188
|
+
}
|
|
189
|
+
return `${parentPath || '$'}[${JSON.stringify(String(key))}]`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function findEmojiInValue(value, options = {}) {
|
|
193
|
+
const issues = [];
|
|
194
|
+
const seen = new WeakSet();
|
|
195
|
+
const maxIssues = options.maxIssues || 20;
|
|
196
|
+
|
|
197
|
+
function visit(current, currentPath) {
|
|
198
|
+
if (issues.length >= maxIssues) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (typeof current === 'string') {
|
|
202
|
+
findEmojiInText(current, {
|
|
203
|
+
artifact: options.artifact,
|
|
204
|
+
path: currentPath,
|
|
205
|
+
maxIssues: maxIssues - issues.length,
|
|
206
|
+
}).forEach((issue) => issues.push(issue));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!current || typeof current !== 'object') {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (seen.has(current)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
seen.add(current);
|
|
216
|
+
|
|
217
|
+
if (Array.isArray(current)) {
|
|
218
|
+
current.forEach((item, index) => visit(item, appendPath(currentPath, index)));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Object.keys(current).forEach((key) => {
|
|
223
|
+
visit(current[key], appendPath(currentPath, key));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
visit(value, options.path || '');
|
|
228
|
+
return issues;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function formatEmojiIssue(issue) {
|
|
232
|
+
const location = issue.line
|
|
233
|
+
? `${issue.artifact}:${issue.line}:${issue.column}`
|
|
234
|
+
: issue.artifact;
|
|
235
|
+
const pathText = issue.path ? ` (${issue.path})` : '';
|
|
236
|
+
const escapeText = issue.escape ? ` via escape "${issue.escape}"` : '';
|
|
237
|
+
return `${location}${pathText} contains emoji "${issue.emoji}"${escapeText}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildEmojiErrorMessage(issues, options = {}) {
|
|
241
|
+
const artifact = normalizeArtifactName(options.artifact || (issues[0] && issues[0].artifact));
|
|
242
|
+
const guidance = options.guidance || 'Remove emoji from generated UI copy, Schema strings, comments, file paths, and code constants. Use plain text, SVG, or icon components instead.';
|
|
243
|
+
const lines = [
|
|
244
|
+
`${artifact} contains emoji, which is forbidden for OpenYida generated artifacts.`,
|
|
245
|
+
...issues.slice(0, 5).map(formatEmojiIssue),
|
|
246
|
+
guidance,
|
|
247
|
+
];
|
|
248
|
+
if (issues.length > 5) {
|
|
249
|
+
lines.splice(lines.length - 1, 0, `...and ${issues.length - 5} more emoji occurrence(s).`);
|
|
250
|
+
}
|
|
251
|
+
return lines.join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
EMOJI_PATTERN,
|
|
256
|
+
buildEmojiErrorMessage,
|
|
257
|
+
findEmojiInArtifactName,
|
|
258
|
+
findEmojiInText,
|
|
259
|
+
findEmojiInValue,
|
|
260
|
+
formatEmojiIssue,
|
|
261
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('./cli-error');
|
|
4
|
+
const {
|
|
5
|
+
EMOJI_PATTERN,
|
|
6
|
+
buildEmojiErrorMessage,
|
|
7
|
+
findEmojiInArtifactName,
|
|
8
|
+
findEmojiInText,
|
|
9
|
+
findEmojiInValue,
|
|
10
|
+
formatEmojiIssue,
|
|
11
|
+
} = require('./no-emoji-detector');
|
|
12
|
+
|
|
13
|
+
function normalizeArtifactName(value) {
|
|
14
|
+
return String(value || 'artifact');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createEmojiError(issues, options = {}) {
|
|
18
|
+
return new CliError(buildEmojiErrorMessage(issues, options), {
|
|
19
|
+
code: options.code || 'OPENYIDA_ARTIFACT_EMOJI_FORBIDDEN',
|
|
20
|
+
details: {
|
|
21
|
+
artifact: normalizeArtifactName(options.artifact || (issues[0] && issues[0].artifact)),
|
|
22
|
+
issues,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertIssuesEmpty(issues, options) {
|
|
28
|
+
if (issues.length > 0) {
|
|
29
|
+
throw createEmojiError(issues, options);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertNoEmojiInText(text, options = {}) {
|
|
34
|
+
assertIssuesEmpty(findEmojiInText(text, options), options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertNoEmojiInValue(value, options = {}) {
|
|
38
|
+
assertIssuesEmpty(findEmojiInValue(value, options), options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertNoEmojiInArtifactName(artifactName, options = {}) {
|
|
42
|
+
assertIssuesEmpty(findEmojiInArtifactName(artifactName, options), {
|
|
43
|
+
...options,
|
|
44
|
+
artifact: options.artifact || artifactName,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
EMOJI_PATTERN,
|
|
50
|
+
assertNoEmojiInArtifactName,
|
|
51
|
+
assertNoEmojiInText,
|
|
52
|
+
assertNoEmojiInValue,
|
|
53
|
+
buildEmojiErrorMessage,
|
|
54
|
+
createEmojiError,
|
|
55
|
+
findEmojiInArtifactName,
|
|
56
|
+
findEmojiInText,
|
|
57
|
+
findEmojiInValue,
|
|
58
|
+
formatEmojiIssue,
|
|
59
|
+
};
|
package/package.json
CHANGED
package/yida-skills/SKILL.md
CHANGED
|
@@ -227,7 +227,7 @@ schema-managed create/update 必须等待用户对当前 `planId` 显式批准
|
|
|
227
227
|
|
|
228
228
|
1. **技能加载唯一入口**:执行任何子技能前,支持 `use_skill` 的宿主必须调用 `use_skill("<技能名>", "<本阶段目的>")` 加载对应技能;不要用 `Read` / `read_file` / `cat` 读取 SKILL.md 路径,不凭记忆猜参数格式。
|
|
229
229
|
2. **corpId 一致性检查**:创建或发布页面前对比 prd/resource context 与当前 auth snapshot(本地 OAuth token session 或 snapshot 明确返回的宿主注入 env token)中的 corpId,不一致必须询问用户(重新登录到目标组织,或确认在当前组织继续操作已解析资源/缺失资源)。
|
|
230
|
-
3. **发布前本地校验**:普通自定义页面 `.oyd.jsx` / `.jsx` 发布前跑 `openyida check-page` + `openyida compile`;Code Canvas `.canvas.jsx` 不跑这两个普通自定义页面检查,改由 `openyida publish` 的 Canvas 编译阶段或 `compileCanvasLocal` 快检校验;JSON 配置写盘后先解析校验,再调用平台命令。
|
|
230
|
+
3. **发布前本地校验**:普通自定义页面 `.oyd.jsx` / `.jsx` 发布前跑 `openyida check-page` + `openyida compile`;Code Canvas `.canvas.jsx` 不跑这两个普通自定义页面检查,改由 `openyida publish` 的 Canvas 编译阶段或 `compileCanvasLocal` 快检校验;JSON 配置写盘后先解析校验,再调用平台命令。OpenYida 生成产物硬禁 emoji:页面源码、Canvas 源码、表单 Schema、发布 Schema 和产物文件路径出现 emoji 时必须改源码/字段 JSON/路径,不得用 `--skip-lint` 或重复发布绕过。
|
|
231
231
|
4. **页面源码修改必须发布闭环**:只要本轮 Write/Edit/Create 了页面源码 `project/pages/src/*.{canvas.jsx,canvas.tsx,oyd.jsx,jsx,tsx}`(含完整搭建、补齐、已有页面 update path、单点优化),final 前必须看到成功的 `openyida publish <source> <appType> <displayPageFormUuid>` 命令结果;本地文件编辑、diff、本地校验或编译只证明源码可发布,不等于远端页面已更新。若没有 publish 成功证据,final 只能说“源码已修改,尚未发布”,禁止说“页面已更新 / 已重新发布 / 已上线”。
|
|
232
232
|
5. **命令输入文件禁止 shell 写入**:当 OpenYida 命令需要 JSON/YAML/CSV/config/script 文件参数时,先使用当前 agent 运行时提供的结构化文件写入工具(如 create_file / Write / file edit tool)创建文件,再把路径传给命令;禁止用 shell heredoc、`cat`/`echo`/`printf`/`tee` 加输出重定向,或把命令 stdout 重定向成业务文件。
|
|
233
233
|
6. **读文件少用 Bash 噪声**:读取或定位 workspace 文件优先用宿主的 Read / Glob / Grep;OpenYida CLI 已返回成功 JSON、URL 或 `formUuid/appType` 时,不要再用 Bash `cat`/`ls` 做无意义复核。
|
|
@@ -160,6 +160,7 @@ UI 不是独立替代主流程的步骤,而是按模式插入到页面生成
|
|
|
160
160
|
|
|
161
161
|
- **模板路径**:先写业务化 `page-spec.json`,再执行 `openyida generate-page ... --spec <page-spec.json> --compile`。生成后只读取 `.openyida-page.json` / CLI 摘要判断 `domainFidelity` 和 dataBinding 状态;若需要补业务语义或样式,基于生成文件做小范围 Edit/patch。禁止在 `generate-page` 后立即 Read 500+ 行源码再全量 Write 覆盖同一路径。
|
|
162
162
|
- **手写路径**:如果已经明确最终页面结构、数据桥和视觉细节,跳过 `generate-page`,直接 Write 最终 `.canvas.jsx`,再做本地快检和 publish。不要先生成模板再把模板完全覆盖。
|
|
163
|
+
- **emoji 硬门禁**:表单字段 JSON、页面 spec、`.canvas.jsx` / `.oyd.jsx` 源码、发布 Schema 和产物文件路径都不能包含 emoji。OpenYida 报 emoji 错误时修改字段文案、spec、源码或路径;不要用 `--skip-lint`、重复 create/publish 或全量 rewrite 试图绕过。
|
|
163
164
|
|
|
164
165
|
选择模板路径时必须:
|
|
165
166
|
|
|
@@ -106,6 +106,7 @@ openyida sample yida-canvas-custom-page portal-native-components --output projec
|
|
|
106
106
|
9. **自定义主题必须页面内注入**:`--theme` 只接受平台预置 key;如果页面设计使用非预置主题(例如活力橙、深玫红、自定义暗黑金),Canvas 页面必须在自身源码中注入 `style#yida-global-theme` 或等价 scoped CSS vars,并在根节点设置 `data-theme-scope="page"`。官方 sample 每个页面都要做,避免宿主应用 `black` 主题把页面染成黑灰。
|
|
107
107
|
10. **真实交付不使用前端 seed 冒充业务数据**:`openyida sample` 原样发布可以保留 sample/seed 数据,但必须在页面上标注为 sample/seed。完整应用或真实交付页只要需要列表、看板、详情记录,并且本轮已经创建/解析业务表单,就必须在 `page-spec.json` 写入 `dataBinding.mode=form`、真实 `appType/formUuid` 和字段映射,让页面从表单读取。若需要演示数据,先通过表单数据写入链路创建 demo/mock records,再由 Canvas 读取这些真实表单记录;未写入 demo records 且没有真实数据时展示空态、表单入口、刷新/登记按钮。
|
|
108
108
|
11. **页面生成二选一**:选择模板路径时,`openyida generate-page ... --spec ... --compile` 之后只读取 CLI 摘要或 `.openyida-page.json` 判断 `domainFidelity` / dataBinding,并对生成源码做小范围 Edit/patch;禁止立刻 Read 大段源码后全量 Write 覆盖同一路径。选择手写路径时,直接 Write 最终 `.canvas.jsx` 并快检/发布,不要先跑 `generate-page` 再完全覆盖。
|
|
109
|
+
12. **Canvas 产物硬禁 emoji**:`.canvas.jsx` 源码、模板 spec 会渲染到页面的文案、JS 注释、数据常量和产物文件路径都不能包含 emoji。`generate-page --compile`、`compileCanvasLocal` 或 `publish` 报 emoji 错误时,先改 spec/源码/路径;不要尝试跳过 lint 或重复发布。
|
|
109
110
|
|
|
110
111
|
## 数据真实性边界
|
|
111
112
|
|
|
@@ -92,6 +92,7 @@ Divider > Field
|
|
|
92
92
|
- 复杂业务表单应自然使用多种字段类型,例如文本、数值、日期、选择、成员/部门、附件、子表、关联表单;字段类型多样性服务于业务语义。
|
|
93
93
|
- `TableField` 必须提供 `children` 子字段,`AssociationFormField` 必须提供关联表单信息。
|
|
94
94
|
- 审批人、审批状态、审批节点等流程运行字段由流程能力承载;表单只收集业务数据。
|
|
95
|
+
- 表单标题、字段 label/title、选项、提示语、校验文案、动作源码、字段 JSON 常量和字段 JSON 文件路径都禁止 emoji;`create-form` / schema compiler 报 emoji 错误时必须改字段 JSON 或路径,不能重复 create 或用同义命令绕过。
|
|
95
96
|
|
|
96
97
|
## Divider 主题规则
|
|
97
98
|
|
|
@@ -56,7 +56,7 @@ description: 宜搭普通自定义页面 JSX / Jsx 组件开发规范(React 16
|
|
|
56
56
|
10. **Tabs 显隐控制**:下拉值变更后自动回退到第一个可见 Tab,内容区用 `display: none` 保留 DOM
|
|
57
57
|
11. **加载态必须可恢复**:列表/看板页默认保留空态或演示数据;接口失败、超时或返回异常时必须把 `loading` 置回 `false`,不要只渲染“正在加载...”挡住整页
|
|
58
58
|
12. **禁止可见原生下拉**:筛选、预约、审批等用户可见下拉交互不要使用 `<select>`;普通自定义页也不要把表单设计器里的 `SelectField` 当 React 筛选组件直接渲染。默认使用 Tailwind className 组合 `button + menu + option` 的自定义下拉组件,并带 `.oyd-select-arrow` 下箭头、`.oyd-select-check` 选中标记和页面级 focus reset;light 模式下选中项整块背景必须用 `--oyd-control-selected-bg` 这类低透明度浅色 token,不要直接用 `--color-brand1-1`
|
|
59
|
-
13. **严禁 emoji**:页面渲染出来的任何位置(标题、按钮、标签、状态、空态文案、图表标题等)**一律禁止出现 emoji**(😀🚀✅⚠️📦📊 等一切彩色符号字符)。需要图标一律用功能性内联 SVG(见 `skills/yida-page-uiux` 图标策略);需要状态标记用文字 + 语义色标签。emoji 是最明显的 AI 味来源之一,且跨端显示不一致。JS
|
|
59
|
+
13. **严禁 emoji**:页面渲染出来的任何位置(标题、按钮、标签、状态、空态文案、图表标题等)**一律禁止出现 emoji**(😀🚀✅⚠️📦📊 等一切彩色符号字符)。需要图标一律用功能性内联 SVG(见 `skills/yida-page-uiux` 图标策略);需要状态标记用文字 + 语义色标签。emoji 是最明显的 AI 味来源之一,且跨端显示不一致。JS 注释、文件路径、数据常量和示例数组里也不要留装饰性符号;`check-page` / `compile` / `publish` 报 emoji 错误时必须删除或改为 SVG/图标组件,不能用 `--skip-lint` 绕过。
|
|
60
60
|
14. **light 页面禁灰黑主题**:普通业务列表、协同表、录入表、工作台和门户默认不要用近黑按钮、近黑描边、灰黑重阴影作为主题;主操作、选中态、筛选焦点和批量操作使用品牌色或 sample 自带主题色,边框用浅色品牌混合。只有用户明确要求暗色/高对比时才使用深色主视觉。
|
|
61
61
|
15. **发布前必须跑检查链路**:先执行 `openyida check-page <file>` 和 `openyida compile <file>`;若出现 warning/error,按规则修复后再发布
|
|
62
62
|
16. **源码修改发布闭环**:只要本轮 Write/Edit/Create 了 `project/pages/src/*.{oyd.jsx,jsx,tsx}` 普通自定义页面源码,`check-page` / `compile` 只证明源码可发布,不等于远端页面已更新;final 前必须看到成功的 `openyida publish <source> <appType> <displayPageFormUuid>`。没有 publish 成功证据时,只能说“源码已修改,尚未发布”,不能说“页面已更新 / 已重新发布”。
|