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.
@@ -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),
@@ -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) {
@@ -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);
@@ -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)) {
@@ -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);
@@ -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
- const parsedUrl = new URL(pageUrl);
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
- const request = requestModule.request(requestOptions, (response) => {
595
- let responseData = '';
596
- response.on('data', (chunk) => {
597
- if (responseData.length < 2000) {
598
- responseData += chunk;
599
- }
600
- });
601
- response.on('end', () => {
602
- const statusCode = response.statusCode || 0;
603
- resolve({
604
- ok: statusCode >= 200 && statusCode < 400,
605
- statusCode,
606
- snippet: responseData.substring(0, 300).replace(/\s+/g, ' ').trim(),
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
- healthCheckResult = await sendHealthCheckRequest(pageUrl, cookies);
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
- return {
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
- return {
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 {
@@ -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',
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.7.23-1",
3
+ "version": "2026.7.23-2",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -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 成功证据时,只能说“源码已修改,尚未发布”,不能说“页面已更新 / 已重新发布”。