openyida 2026.7.22 → 2026.7.23-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,26 @@ function readJsonInput(value, options) {
17
17
  return fs.readFileSync(resolvedPath, 'utf-8');
18
18
  }
19
19
 
20
+ function normalizeCreateFields(fields) {
21
+ if (
22
+ !Array.isArray(fields)
23
+ || fields.length === 0
24
+ || !fields.every((item) => (
25
+ item
26
+ && typeof item === 'object'
27
+ && !Array.isArray(item)
28
+ && String(item.action || '').toLowerCase() === 'add'
29
+ && item.field
30
+ && typeof item.field === 'object'
31
+ && !Array.isArray(item.field)
32
+ ))
33
+ ) {
34
+ return fields;
35
+ }
36
+
37
+ return fields.map((item) => item.field);
38
+ }
39
+
20
40
  function createDefinitionReaders(dependencies) {
21
41
  const {
22
42
  fs,
@@ -43,9 +63,9 @@ function createDefinitionReaders(dependencies) {
43
63
  let columns = 1;
44
64
 
45
65
  if (Array.isArray(parsed)) {
46
- fields = parsed;
66
+ fields = normalizeCreateFields(parsed);
47
67
  } else if (typeof parsed === 'object' && parsed !== null) {
48
- fields = parsed.fields || [];
68
+ fields = normalizeCreateFields(parsed.fields || []);
49
69
  columns = parsed.columns !== undefined ? parsed.columns : 1;
50
70
  if (Array.isArray(parsed.validations)) {
51
71
  validations = parsed.validations;
@@ -95,7 +95,11 @@ function parseOpenOption(inputArgs) {
95
95
  const OPTION_FIELD_TYPES = ['RadioField', 'SelectField', 'CheckboxField', 'MultiSelectField'];
96
96
 
97
97
  function throwCreateFormError(message, code, details) {
98
- throw new CliError(message, {
98
+ throw createCreateFormError(message, code, details);
99
+ }
100
+
101
+ function createCreateFormError(message, code, details) {
102
+ return new CliError(message, {
99
103
  code: code || 'CREATE_FORM_FAILED',
100
104
  details,
101
105
  });
@@ -210,6 +214,28 @@ const FIELD_TYPE_ALIAS = {
210
214
  serialnumberfield: 'SerialNumberField',
211
215
  };
212
216
 
217
+ const SUPPORTED_BUSINESS_FIELD_TYPES = [
218
+ 'TextField',
219
+ 'TextareaField',
220
+ 'RadioField',
221
+ 'SelectField',
222
+ 'CheckboxField',
223
+ 'MultiSelectField',
224
+ 'NumberField',
225
+ 'RateField',
226
+ 'DateField',
227
+ 'CascadeDateField',
228
+ 'EmployeeField',
229
+ 'DepartmentSelectField',
230
+ 'CountrySelectField',
231
+ 'AddressField',
232
+ 'AttachmentField',
233
+ 'ImageField',
234
+ 'TableField',
235
+ 'AssociationFormField',
236
+ 'SerialNumberField',
237
+ ];
238
+
213
239
  const FORM_PRESENTATION_TYPE_ALIAS = {
214
240
  Divider: 'Divider',
215
241
  divider: 'Divider',
@@ -262,11 +288,28 @@ function normalizeComponentAlias(field) {
262
288
  return String(rawAlias).trim();
263
289
  }
264
290
 
291
+ function readFormDefinitionType(field) {
292
+ if (!field || typeof field !== 'object' || Array.isArray(field)) {
293
+ return '';
294
+ }
295
+ if (field.type !== undefined && field.type !== null && field.type !== '') {
296
+ return String(field.type).trim();
297
+ }
298
+ if (field.componentName !== undefined && field.componentName !== null && field.componentName !== '') {
299
+ return String(field.componentName).trim();
300
+ }
301
+ if (field.componentType !== undefined && field.componentType !== null && field.componentType !== '') {
302
+ return String(field.componentType).trim();
303
+ }
304
+ return '';
305
+ }
306
+
265
307
  function normalizeFormDefinitionType(field) {
266
- if (!field || !field.type) {
308
+ const rawType = readFormDefinitionType(field);
309
+ if (!rawType) {
267
310
  return '';
268
311
  }
269
- return FORM_PRESENTATION_TYPE_ALIAS[field.type] || FIELD_TYPE_ALIAS[field.type] || field.type;
312
+ return FORM_PRESENTATION_TYPE_ALIAS[rawType] || FIELD_TYPE_ALIAS[rawType] || rawType;
270
313
  }
271
314
 
272
315
  function isFormPresentationComponent(componentName) {
@@ -277,6 +320,194 @@ function isFormPresentationDefinition(field) {
277
320
  return isFormPresentationComponent(normalizeFormDefinitionType(field));
278
321
  }
279
322
 
323
+ function isSupportedBusinessFieldType(componentName) {
324
+ return SUPPORTED_BUSINESS_FIELD_TYPES.indexOf(componentName) !== -1;
325
+ }
326
+
327
+ function getDefinitionDisplayName(field) {
328
+ if (!field || typeof field !== 'object') {
329
+ return '';
330
+ }
331
+ return field.label || field.title || readFormDefinitionType(field) || '';
332
+ }
333
+
334
+ function buildInvalidFieldDefinitionDetails(field, fieldPath, extra) {
335
+ const details = Object.assign({
336
+ path: fieldPath || 'fields',
337
+ label: field && typeof field === 'object' ? field.label : undefined,
338
+ title: field && typeof field === 'object' ? field.title : undefined,
339
+ type: readFormDefinitionType(field),
340
+ }, extra || {});
341
+ Object.keys(details).forEach(function (key) {
342
+ if (details[key] === undefined || details[key] === '') {
343
+ delete details[key];
344
+ }
345
+ });
346
+ return details;
347
+ }
348
+
349
+ function throwInvalidFieldDefinition(message, code, field, fieldPath, extra) {
350
+ throwCreateFormError(
351
+ message,
352
+ code || 'CREATE_FORM_INVALID_FIELD_DEFINITION',
353
+ buildInvalidFieldDefinitionDetails(field, fieldPath, extra)
354
+ );
355
+ }
356
+
357
+ function validatePlainFieldObject(field, fieldPath) {
358
+ if (!field || typeof field !== 'object' || Array.isArray(field)) {
359
+ throwInvalidFieldDefinition(
360
+ '字段定义必须是对象: ' + fieldPath,
361
+ 'CREATE_FORM_INVALID_FIELD_DEFINITION',
362
+ field,
363
+ fieldPath
364
+ );
365
+ }
366
+ }
367
+
368
+ function validateContainerChildrenArray(field, fieldPath, componentName) {
369
+ if (field.children === undefined || field.children === null) {
370
+ return [];
371
+ }
372
+ if (!Array.isArray(field.children)) {
373
+ throwInvalidFieldDefinition(
374
+ componentName + '.children 必须是数组: ' + fieldPath,
375
+ 'CREATE_FORM_INVALID_CONTAINER_CHILDREN',
376
+ field,
377
+ fieldPath,
378
+ { componentName }
379
+ );
380
+ }
381
+ return field.children;
382
+ }
383
+
384
+ function validateFieldDefinition(field, fieldPath, options) {
385
+ const validationOptions = options || {};
386
+ validatePlainFieldObject(field, fieldPath);
387
+
388
+ const componentName = normalizeFormDefinitionType(field);
389
+ if (!componentName) {
390
+ throwInvalidFieldDefinition(
391
+ '字段定义缺少 type/componentName/componentType: ' + fieldPath,
392
+ 'CREATE_FORM_FIELD_TYPE_MISSING',
393
+ field,
394
+ fieldPath
395
+ );
396
+ }
397
+
398
+ if (isFormPresentationComponent(componentName)) {
399
+ if (validationOptions.disallowPresentation) {
400
+ throwInvalidFieldDefinition(
401
+ 'TableField.children 不支持展示布局组件: ' + componentName,
402
+ 'CREATE_FORM_TABLE_CHILD_PRESENTATION_UNSUPPORTED',
403
+ field,
404
+ fieldPath,
405
+ { componentName }
406
+ );
407
+ }
408
+ if (componentName === 'Column' && !validationOptions.allowColumn) {
409
+ throwInvalidFieldDefinition(
410
+ 'Column 只能作为 ColumnContainer.children 的 Column 对象使用: ' + fieldPath,
411
+ 'CREATE_FORM_COLUMN_OUTSIDE_COLUMNS_LAYOUT',
412
+ field,
413
+ fieldPath,
414
+ { componentName }
415
+ );
416
+ }
417
+ if (componentName === 'ColumnsLayout') {
418
+ validateColumnsLayoutDefinition(field, fieldPath);
419
+ } else if (componentName === 'PageSection') {
420
+ validateContainerChildrenArray(field, fieldPath, componentName).forEach(function (child, childIndex) {
421
+ validateFieldDefinition(child, fieldPath + '.children[' + childIndex + ']');
422
+ });
423
+ } else if (componentName === 'Column') {
424
+ validateContainerChildrenArray(field, fieldPath, componentName).forEach(function (child, childIndex) {
425
+ validateFieldDefinition(child, fieldPath + '.children[' + childIndex + ']');
426
+ });
427
+ }
428
+ return componentName;
429
+ }
430
+
431
+ if (!isSupportedBusinessFieldType(componentName)) {
432
+ throwInvalidFieldDefinition(
433
+ '不支持的字段类型: ' + componentName,
434
+ 'CREATE_FORM_UNSUPPORTED_FIELD_TYPE',
435
+ field,
436
+ fieldPath,
437
+ { componentName }
438
+ );
439
+ }
440
+
441
+ if (componentName === 'TableField') {
442
+ validateTableFieldChildren(field, fieldPath, validationOptions);
443
+ }
444
+
445
+ return componentName;
446
+ }
447
+
448
+ function validateColumnsLayoutDefinition(field, fieldPath) {
449
+ const columns = validateContainerChildrenArray(field, fieldPath, 'ColumnsLayout');
450
+ columns.forEach(function (columnDefinition, columnIndex) {
451
+ const columnPath = fieldPath + '.children[' + columnIndex + ']';
452
+ if (Array.isArray(columnDefinition)) {
453
+ columnDefinition.forEach(function (child, childIndex) {
454
+ validateFieldDefinition(child, columnPath + '[' + childIndex + ']');
455
+ });
456
+ return;
457
+ }
458
+ if (
459
+ columnDefinition &&
460
+ typeof columnDefinition === 'object' &&
461
+ !Array.isArray(columnDefinition) &&
462
+ normalizeFormDefinitionType(columnDefinition) === 'Column'
463
+ ) {
464
+ validateFieldDefinition(columnDefinition, columnPath, { allowColumn: true });
465
+ return;
466
+ }
467
+ throwInvalidFieldDefinition(
468
+ 'ColumnContainer.children 只能包含二维字段数组或 Column 对象: ' + columnPath,
469
+ 'CREATE_FORM_INVALID_COLUMNS_LAYOUT_CHILDREN',
470
+ columnDefinition,
471
+ columnPath,
472
+ { componentName: normalizeFormDefinitionType(columnDefinition) || readFormDefinitionType(columnDefinition) }
473
+ );
474
+ });
475
+ }
476
+
477
+ function validateTableFieldChildren(field, fieldPath, options) {
478
+ if (field.children === undefined || field.children === null) {
479
+ return;
480
+ }
481
+ if (!Array.isArray(field.children)) {
482
+ throwInvalidFieldDefinition(
483
+ 'TableField.children 必须是字段数组: ' + fieldPath,
484
+ 'CREATE_FORM_INVALID_TABLE_CHILDREN',
485
+ field,
486
+ fieldPath,
487
+ { componentName: 'TableField' }
488
+ );
489
+ }
490
+ field.children.forEach(function (child, childIndex) {
491
+ validateFieldDefinition(child, fieldPath + '.children[' + childIndex + ']', Object.assign({}, options, {
492
+ disallowPresentation: true,
493
+ }));
494
+ });
495
+ }
496
+
497
+ function validateFormFieldDefinitions(fields, rootPath) {
498
+ if (!Array.isArray(fields)) {
499
+ throwInvalidFieldDefinition(
500
+ '字段定义必须是数组',
501
+ 'CREATE_FORM_INVALID_FIELD_DEFINITION',
502
+ null,
503
+ rootPath || 'fields'
504
+ );
505
+ }
506
+ fields.forEach(function (field, index) {
507
+ validateFieldDefinition(field, (rootPath || 'fields') + '[' + index + ']');
508
+ });
509
+ }
510
+
280
511
  function normalizeI18nValue(value, fallback) {
281
512
  if (value && typeof value === 'object') {
282
513
  return value;
@@ -465,7 +696,16 @@ function buildFormNodeComponents(fields) {
465
696
  // ── 生成字段组件 ─────────────────────────────────────
466
697
 
467
698
  function buildFieldComponent(field) {
468
- const componentName = FIELD_TYPE_ALIAS[field.type] || field.type;
699
+ const componentName = normalizeFormDefinitionType(field);
700
+ if (!isSupportedBusinessFieldType(componentName)) {
701
+ throwInvalidFieldDefinition(
702
+ componentName ? '不支持的字段类型: ' + componentName : '字段定义缺少 type/componentName/componentType',
703
+ componentName ? 'CREATE_FORM_UNSUPPORTED_FIELD_TYPE' : 'CREATE_FORM_FIELD_TYPE_MISSING',
704
+ field,
705
+ 'field',
706
+ { componentName }
707
+ );
708
+ }
469
709
  const fieldId = generateFieldId(componentName);
470
710
  const nodeId = nextNodeId();
471
711
 
@@ -3836,20 +4076,14 @@ function ensureComponentsMapForComponent(schema, component) {
3836
4076
  }
3837
4077
  }
3838
4078
 
3839
- function getDefinitionDisplayName(field) {
3840
- if (!field || typeof field !== 'object') {
3841
- return '';
3842
- }
3843
- return field.label || field.title || field.type || '';
3844
- }
3845
-
3846
4079
  function countDataFieldDefinitions(fields) {
3847
4080
  let count = 0;
3848
4081
  (fields || []).forEach(function (field) {
3849
4082
  if (!field || typeof field !== 'object' || Array.isArray(field)) {
3850
4083
  return;
3851
4084
  }
3852
- if (!isFormPresentationDefinition(field)) {
4085
+ const componentName = normalizeFormDefinitionType(field);
4086
+ if (isSupportedBusinessFieldType(componentName)) {
3853
4087
  count++;
3854
4088
  }
3855
4089
  if (Array.isArray(field.children)) {
@@ -3881,11 +4115,12 @@ function applyChangesToSchema(schema, changes) {
3881
4115
  const actionDesc = t('create_form.action_label', changeIndex + 1, change.action);
3882
4116
 
3883
4117
  if (change.action === 'add') {
3884
- if (!change.field || !change.field.type || (!change.field.label && !change.field.title && !isFormPresentationDefinition(change.field))) {
4118
+ if (!change.field || !normalizeFormDefinitionType(change.field) || (!change.field.label && !change.field.title && !isFormPresentationDefinition(change.field))) {
3885
4119
  warn(actionDesc + t('create_form.add_missing_field'));
3886
4120
  return;
3887
4121
  }
3888
4122
 
4123
+ validateFormFieldDefinitions([change.field], 'changes[' + changeIndex + '].field');
3889
4124
  const newComponent = buildFormNodeComponent(change.field);
3890
4125
  ensureComponentsMapForComponent(schema, newComponent);
3891
4126
  const displayName = getDefinitionDisplayName(change.field);
@@ -4057,12 +4292,54 @@ function requireSchemaServerRevision(serverRevision, details) {
4057
4292
  return serverRevision;
4058
4293
  }
4059
4294
 
4295
+ function sanitizeFailureResult(result) {
4296
+ if (!result || typeof result !== 'object') {
4297
+ return result || null;
4298
+ }
4299
+ const sanitized = {};
4300
+ ['success', 'errorMsg', 'errorCode', 'code', 'message', '__httpStatus', '__needLogin', '__csrfExpired'].forEach(function (key) {
4301
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
4302
+ sanitized[key] = result[key];
4303
+ }
4304
+ });
4305
+ return sanitized;
4306
+ }
4307
+
4308
+ function buildCreateFormPostCreateFailurePayload(context) {
4309
+ const errorObject = context && context.error;
4310
+ const payload = {
4311
+ success: false,
4312
+ appType: context.appType,
4313
+ formTitle: context.formTitle,
4314
+ formUuid: context.formUuid,
4315
+ stage: context.stage || 'postCreate',
4316
+ error: errorObject && errorObject.message ? errorObject.message : String(errorObject || 'request failed'),
4317
+ errorCode: errorObject && errorObject.code ? errorObject.code : (context.errorCode || 'CREATE_FORM_POST_CREATE_FAILED'),
4318
+ retryAdvice: t('create_form.create_post_failure_retry_advice', context.appType, context.formTitle),
4319
+ };
4320
+ if (context.fieldCount !== undefined) {
4321
+ payload.fieldCount = context.fieldCount;
4322
+ }
4323
+ return payload;
4324
+ }
4325
+
4326
+ function emitCreateFormPostCreateFailure(context) {
4327
+ const errorObject = context && context.error;
4328
+ if (errorObject && errorObject.__openyidaPostCreateFailureEmitted) {
4329
+ return;
4330
+ }
4331
+ console.log(JSON.stringify(buildCreateFormPostCreateFailurePayload(context)));
4332
+ if (errorObject && typeof errorObject === 'object') {
4333
+ errorObject.__openyidaPostCreateFailureEmitted = true;
4334
+ }
4335
+ }
4336
+
4060
4337
  // ── 保存 Schema 并更新表单配置(create/update 共用)──
4061
4338
  //
4062
4339
  // 封装了 saveFormSchema + updateFormConfig 两步,以及各自的 302 自动重登录重试。
4063
4340
  // 返回 { saveResult, configResult }。
4064
4341
 
4065
- async function saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, version, stepOffset) {
4342
+ async function saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, version, stepOffset, failureContext) {
4066
4343
  const saveStep = stepOffset || 4;
4067
4344
  const configStep = saveStep + 1;
4068
4345
 
@@ -4092,12 +4369,20 @@ async function saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, ver
4092
4369
  if (saveResult && !saveResult.__needLogin) {
4093
4370
  hint(t('common.response_detail', JSON.stringify(saveResult, null, 2)));
4094
4371
  }
4095
- console.log(JSON.stringify({ success: false, formUuid: formUuid, error: saveErrorMsg }));
4096
- throwCreateFormError(saveErrorMsg, 'CREATE_FORM_SAVE_SCHEMA_FAILED', {
4372
+ const saveError = createCreateFormError(saveErrorMsg, 'CREATE_FORM_SAVE_SCHEMA_FAILED', {
4097
4373
  appType,
4098
4374
  formUuid,
4099
- result: saveResult,
4375
+ result: sanitizeFailureResult(saveResult),
4100
4376
  });
4377
+ if (failureContext) {
4378
+ emitCreateFormPostCreateFailure(Object.assign({}, failureContext, {
4379
+ stage: 'saveFormSchema',
4380
+ error: saveError,
4381
+ }));
4382
+ } else {
4383
+ console.log(JSON.stringify({ success: false, formUuid: formUuid, error: saveErrorMsg }));
4384
+ }
4385
+ throw saveError;
4101
4386
  }
4102
4387
 
4103
4388
  success(t('create_form.schema_saved'));
@@ -4127,6 +4412,7 @@ async function mainCreate(parsedArgs, authRef) {
4127
4412
 
4128
4413
  step(2, t('create_form.step_read_fields', 2));
4129
4414
  const { fields, columns, validations } = readFieldsDefinition(fieldsJsonOrFile);
4415
+ validateFormFieldDefinitions(fields);
4130
4416
  const fieldCount = countDataFieldDefinitions(fields);
4131
4417
  success(t('create_form.fields_loaded', fieldCount));
4132
4418
  label('Columns:', String(columns));
@@ -4158,30 +4444,62 @@ async function mainCreate(parsedArgs, authRef) {
4158
4444
 
4159
4445
  const formUuid = createResult.content.formUuid || createResult.content;
4160
4446
  success(t('create_form.blank_created', formUuid));
4161
- const shellSchemaResult = await requestWithAutoLogin(function (auth) {
4162
- return sendGetRequest(
4163
- auth.baseUrl,
4164
- buildApiPath(appType, 'getFormSchema', { prefix: '_view', namespace: 'alibaba' }),
4165
- { formUuid: formUuid, schemaVersion: 'V5' }
4166
- );
4167
- }, authRef);
4168
- const serverRevision = extractSchemaServerRevision(shellSchemaResult);
4447
+ let configResult;
4448
+ let postCreateStage = 'getFormSchema';
4449
+ try {
4450
+ const shellSchemaResult = await requestWithAutoLogin(function (auth) {
4451
+ return sendGetRequest(
4452
+ auth.baseUrl,
4453
+ buildApiPath(appType, 'getFormSchema', { prefix: '_view', namespace: 'alibaba' }),
4454
+ { formUuid: formUuid, schemaVersion: 'V5' }
4455
+ );
4456
+ }, authRef);
4457
+ if (!shellSchemaResult || shellSchemaResult.success === false || shellSchemaResult.__needLogin || shellSchemaResult.__csrfExpired) {
4458
+ const schemaErrorMsg = shellSchemaResult
4459
+ ? shellSchemaResult.errorMsg || shellSchemaResult.message || t('common.unknown_error')
4460
+ : t('common.request_failed');
4461
+ throwCreateFormError(schemaErrorMsg, 'CREATE_FORM_GET_SCHEMA_FAILED', {
4462
+ appType,
4463
+ formUuid,
4464
+ result: sanitizeFailureResult(shellSchemaResult),
4465
+ });
4466
+ }
4467
+ const serverRevision = extractSchemaServerRevision(shellSchemaResult);
4169
4468
 
4170
- // Step 4 & 5: 生成 Schema 并保存,然后更新表单配置
4171
- const corpId = resolveCorpId(authRef.authData);
4172
- if (!corpId) {
4173
- warn(t('create_form.no_corp_id_warning'));
4174
- } else {
4175
- info(t('create_form.corp_id_ok', corpId));
4176
- }
4469
+ // Step 4 & 5: 生成 Schema 并保存,然后更新表单配置
4470
+ const corpId = resolveCorpId(authRef.authData);
4471
+ if (!corpId) {
4472
+ warn(t('create_form.no_corp_id_warning'));
4473
+ } else {
4474
+ info(t('create_form.corp_id_ok', corpId));
4475
+ }
4177
4476
 
4178
- const schema = buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, theme, labelAlign);
4179
- const createValidationRules = collectSmartValidationRulesFromFields(fields).concat(validations || []);
4180
- if (createValidationRules.length > 0) {
4181
- const appliedValidations = applySmartValidations(schema, createValidationRules);
4182
- info('已为创建表单写入 ' + appliedValidations.appliedRules.length + ' 条字段校验');
4477
+ postCreateStage = 'buildFormSchema';
4478
+ const schema = buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, theme, labelAlign);
4479
+ const createValidationRules = collectSmartValidationRulesFromFields(fields).concat(validations || []);
4480
+ if (createValidationRules.length > 0) {
4481
+ const appliedValidations = applySmartValidations(schema, createValidationRules);
4482
+ info('已为创建表单写入 ' + appliedValidations.appliedRules.length + ' 条字段校验');
4483
+ }
4484
+ postCreateStage = 'saveSchemaAndUpdateConfig';
4485
+ const saveAndConfigResult = await saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, serverRevision, 4, {
4486
+ appType,
4487
+ formTitle,
4488
+ formUuid,
4489
+ fieldCount,
4490
+ });
4491
+ configResult = saveAndConfigResult.configResult;
4492
+ } catch (err) {
4493
+ emitCreateFormPostCreateFailure({
4494
+ appType,
4495
+ formTitle,
4496
+ formUuid,
4497
+ fieldCount,
4498
+ stage: postCreateStage,
4499
+ error: err,
4500
+ });
4501
+ throw err;
4183
4502
  }
4184
- const { configResult } = await saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, serverRevision, 4);
4185
4503
 
4186
4504
  // 输出结果
4187
4505
  const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
@@ -4203,11 +4521,25 @@ async function mainCreate(parsedArgs, authRef) {
4203
4521
  ['URL', formUrl],
4204
4522
  ]);
4205
4523
  hint(t('create_form.schema_ok_config_failed'));
4206
- console.log(JSON.stringify(withBrowserHandoff(
4207
- { success: true, formUuid, formTitle, appType, fieldCount, url: formUrl, configWarning: configErrorMsg },
4208
- formUrl,
4209
- { stage: 'create_form_success', title: formTitle },
4210
- parsedArgs.browserOpenMode
4524
+ const configError = createCreateFormError(configErrorMsg, 'CREATE_FORM_UPDATE_CONFIG_FAILED', {
4525
+ appType,
4526
+ formUuid,
4527
+ result: sanitizeFailureResult(configResult),
4528
+ });
4529
+ console.log(JSON.stringify(Object.assign(
4530
+ buildCreateFormPostCreateFailurePayload({
4531
+ appType,
4532
+ formTitle,
4533
+ formUuid,
4534
+ fieldCount,
4535
+ stage: 'updateFormConfig',
4536
+ error: configError,
4537
+ }),
4538
+ {
4539
+ url: formUrl,
4540
+ schemaSaved: true,
4541
+ configWarning: configErrorMsg,
4542
+ }
4211
4543
  )));
4212
4544
  }
4213
4545
  }
@@ -4228,6 +4560,7 @@ async function createFormForLegacyProcess(context, input) {
4228
4560
 
4229
4561
  const { appType, formTitle, fieldsJsonOrFile, layout, theme, labelAlign } = parsedArgs;
4230
4562
  const { fields, validations } = readFieldsDefinition(fieldsJsonOrFile);
4563
+ validateFormFieldDefinitions(fields);
4231
4564
  const fieldCount = countDataFieldDefinitions(fields);
4232
4565
  const createResult = await requestWithAutoLogin(function (auth) {
4233
4566
  return sendPostRequest(
@@ -5193,6 +5526,8 @@ module.exports = {
5193
5526
  buildFormNodeComponent,
5194
5527
  countDataFieldDefinitions,
5195
5528
  collectComponentNames,
5529
+ normalizeFormDefinitionType,
5530
+ validateFormFieldDefinitions,
5196
5531
  ensureDividerThemeAction,
5197
5532
  applyChangesToSchema,
5198
5533
  },
@@ -514,6 +514,40 @@ function getCanvasDistPath(outputPath) {
514
514
  return path.posix.join(distDir, distName);
515
515
  }
516
516
 
517
+ function isOpenYidaProjectCwd(cwd) {
518
+ const currentDir = cwd || process.cwd();
519
+ return path.basename(currentDir) === 'project' && (
520
+ fs.existsSync(path.join(currentDir, 'config.json')) ||
521
+ fs.existsSync(path.join(currentDir, 'pages'))
522
+ );
523
+ }
524
+
525
+ function normalizeOutputPathForProjectCwd(output, options = {}) {
526
+ const cwd = options.cwd || process.cwd();
527
+ const rawOutput = String(output || '');
528
+ if (!rawOutput || path.isAbsolute(rawOutput)) {
529
+ return {
530
+ outputPath: path.resolve(cwd, rawOutput),
531
+ strippedProjectPrefix: false,
532
+ requestedOutput: rawOutput,
533
+ normalizedOutput: rawOutput,
534
+ };
535
+ }
536
+
537
+ const slashOutput = rawOutput.replace(/\\/g, '/').replace(/^\.\//, '');
538
+ const shouldStrip = isOpenYidaProjectCwd(cwd) && (
539
+ slashOutput.startsWith('project/pages/') ||
540
+ slashOutput.startsWith('project/.cache/')
541
+ );
542
+ const normalizedOutput = shouldStrip ? slashOutput.slice('project/'.length) : rawOutput;
543
+ return {
544
+ outputPath: path.resolve(cwd, normalizedOutput),
545
+ strippedProjectPrefix: shouldStrip,
546
+ requestedOutput: rawOutput,
547
+ normalizedOutput,
548
+ };
549
+ }
550
+
517
551
  async function run(args) {
518
552
  const options = parseArgs(args || []);
519
553
  const spec = loadSpec(options.spec);
@@ -533,7 +567,10 @@ async function run(args) {
533
567
  error(t('generate_page.template_not_found', templateFile));
534
568
  }
535
569
 
536
- const outputPath = path.resolve(options.output || spec.output || (mode === 'canvas' ? templateConfig.defaultCanvasOutput : templateConfig.defaultNativeOutput));
570
+ const outputResolution = normalizeOutputPathForProjectCwd(
571
+ options.output || spec.output || (mode === 'canvas' ? templateConfig.defaultCanvasOutput : templateConfig.defaultNativeOutput)
572
+ );
573
+ const outputPath = outputResolution.outputPath;
537
574
  const manifestPath = getManifestPath(outputPath);
538
575
  const templateSource = fs.readFileSync(templateFile, 'utf-8');
539
576
  const ir = normalizePageSpec(spec, {
@@ -561,6 +598,9 @@ async function run(args) {
561
598
  fs.writeFileSync(outputPath, outputSource, 'utf-8');
562
599
  fs.writeFileSync(manifestPath, `${JSON.stringify(ir, null, 2)}\n`, 'utf-8');
563
600
 
601
+ if (outputResolution.strippedProjectPrefix) {
602
+ warn(t('generate_page.output_project_prefix_stripped', outputResolution.requestedOutput, outputResolution.normalizedOutput));
603
+ }
564
604
  success(t('generate_page.done', outputPath));
565
605
  hint(t('generate_page.hint'));
566
606
  reportMaterialStatus(materialPrecheck);
@@ -641,6 +681,7 @@ module.exports = {
641
681
  escapeJsStringValue,
642
682
  getManifestPath,
643
683
  getCanvasDistPath,
684
+ normalizeOutputPathForProjectCwd,
644
685
  inferMode,
645
686
  inferTemplateName,
646
687
  };
@@ -509,6 +509,9 @@ const SUPPORTED_FIELD_TYPES = Object.freeze([
509
509
  ]);
510
510
 
511
511
  function normalizeFieldType(fieldType) {
512
+ if (fieldType === undefined || fieldType === null || fieldType === '') {
513
+ return '';
514
+ }
512
515
  return FIELD_TYPE_ALIAS[fieldType] || fieldType;
513
516
  }
514
517
 
@@ -520,6 +523,57 @@ function isOptionFieldType(fieldType) {
520
523
  return OPTION_FIELD_TYPES.indexOf(normalizeFieldType(fieldType)) !== -1;
521
524
  }
522
525
 
526
+ function readFieldDefinitionType(field) {
527
+ if (!field || typeof field !== 'object' || Array.isArray(field)) {
528
+ return '';
529
+ }
530
+ if (field.type !== undefined && field.type !== null && field.type !== '') {
531
+ return String(field.type).trim();
532
+ }
533
+ if (field.componentType !== undefined && field.componentType !== null && field.componentType !== '') {
534
+ return String(field.componentType).trim();
535
+ }
536
+ if (field.componentName !== undefined && field.componentName !== null && field.componentName !== '') {
537
+ return String(field.componentName).trim();
538
+ }
539
+ return '';
540
+ }
541
+
542
+ function assertSupportedFieldDefinition(field, details) {
543
+ if (!field || typeof field !== 'object' || Array.isArray(field)) {
544
+ throw createFormCompilerError(
545
+ '字段定义必须是对象',
546
+ 'FORM_COMPILER_INVALID_DEFINITION',
547
+ details
548
+ );
549
+ }
550
+ const rawType = readFieldDefinitionType(field);
551
+ const componentName = normalizeFieldType(rawType);
552
+ if (!componentName) {
553
+ throw createFormCompilerError(
554
+ '字段定义缺少 type/componentType/componentName',
555
+ 'FORM_COMPILER_FIELD_TYPE_MISSING',
556
+ Object.assign({
557
+ label: field.label,
558
+ title: field.title,
559
+ }, details)
560
+ );
561
+ }
562
+ if (!isSupportedFieldType(componentName)) {
563
+ throw createFormCompilerError(
564
+ '不支持的字段类型: ' + componentName,
565
+ 'FORM_COMPILER_UNSUPPORTED_FIELD_TYPE',
566
+ Object.assign({
567
+ label: field.label,
568
+ title: field.title,
569
+ type: rawType,
570
+ componentName,
571
+ }, details)
572
+ );
573
+ }
574
+ return componentName;
575
+ }
576
+
523
577
  const COMPONENT_ALIAS_META = Symbol('openyida.componentAlias');
524
578
 
525
579
  function normalizeComponentAlias(field) {
@@ -544,7 +598,9 @@ function normalizeComponentAlias(field) {
544
598
 
545
599
  function buildFieldComponent(field, options) {
546
600
  const compilerOptions = createCompilerOptions(options);
547
- const componentName = normalizeFieldType(field.type);
601
+ const componentName = assertSupportedFieldDefinition(field, {
602
+ semanticPath: compilerOptions.semanticPath,
603
+ });
548
604
  const semanticKey = normalizeSemanticKey(field);
549
605
  if (semanticKey && compilerOptions.requireParentSemanticPath && !compilerOptions.semanticPath) {
550
606
  throw createFormCompilerError(
@@ -1139,10 +1195,16 @@ function buildFieldComponent(field, options) {
1139
1195
  function collectComponentNames(fields) {
1140
1196
  const names = new Set(['Page', 'RootHeader', 'RootContent', 'RootFooter', 'FooterYida', 'FormContainer']);
1141
1197
  fields.forEach(function (field) {
1142
- names.add(field.type);
1143
- if (field.type === 'TableField' && field.children) {
1198
+ const componentName = normalizeFieldType(readFieldDefinitionType(field));
1199
+ if (componentName) {
1200
+ names.add(componentName);
1201
+ }
1202
+ if (componentName === 'TableField' && field.children) {
1144
1203
  field.children.forEach(function (child) {
1145
- names.add(child.type);
1204
+ const childComponentName = normalizeFieldType(readFieldDefinitionType(child));
1205
+ if (childComponentName) {
1206
+ names.add(childComponentName);
1207
+ }
1146
1208
  });
1147
1209
  }
1148
1210
  });
@@ -721,6 +721,7 @@ Examples:
721
721
  config_failed: ' ⚠️ Config update failed: {0}',
722
722
  schema_ok_config_failed: ' Schema saved, but config update failed',
723
723
  schema_saved_config_failed: ' Schema saved, but config update failed',
724
+ create_post_failure_retry_advice: 'Do not repeat create directly. First run openyida list-forms {0} --keyword "{1}" to check for an existing same-title form; if this run already created a blank/existing form, prefer create-form update or a future --resume-form-uuid flow.',
724
725
  error: '\n❌ Error: {0}',
725
726
  usage_create: 'Usage: openyida create-form create <appType> <formTitle> <fieldsJsonFile>',
726
727
  example_create: 'Example: openyida create-form create "APP_XXX" "Employee Info" .cache/openyida/forms/employee-fields.json',
@@ -1073,6 +1074,7 @@ Examples:
1073
1074
  unknown_template: 'Unknown page template: {0}',
1074
1075
  available_templates: 'Available templates: {0}',
1075
1076
  template_not_found: 'Template file not found: {0}',
1077
+ output_project_prefix_stripped: 'Current directory is already an OpenYida project; output path was adjusted from {0} to {1} to avoid project/project.',
1076
1078
  done: 'Page generated: {0}',
1077
1079
  hint: 'Next run openyida compile <file>, or pass --compile to compile immediately.',
1078
1080
  success: 'Page generation complete',
@@ -693,6 +693,7 @@ openyida - 宜搭命令行工具
693
693
  config_failed: ' ⚠️ 配置更新失败: {0}',
694
694
  schema_ok_config_failed: ' Schema 已保存,但配置更新失败',
695
695
  schema_saved_config_failed: ' Schema 已保存,但配置更新失败',
696
+ create_post_failure_retry_advice: '不要直接重复 create。先运行 openyida list-forms {0} --keyword "{1}" 确认同名表单;若是本轮创建的空白/已有表单,优先使用 create-form update 或后续 --resume-form-uuid 复用。',
696
697
  error: '\n❌ 错误: {0}',
697
698
  usage_create: '用法: openyida create-form create <appType> <formTitle> <fieldsJsonFile>',
698
699
  example_create: '示例:openyida create-form create "APP_XXX" "员工信息登记" .cache/openyida/forms/employee-fields.json',
@@ -1067,6 +1068,7 @@ openyida - 宜搭命令行工具
1067
1068
  unknown_template: '未知页面模板:{0}',
1068
1069
  available_templates: '可用模板:{0}',
1069
1070
  template_not_found: '模板文件不存在:{0}',
1071
+ output_project_prefix_stripped: '检测到当前目录已是 OpenYida project,已将输出路径从 {0} 调整为 {1},避免生成 project/project。',
1070
1072
  done: '页面已生成:{0}',
1071
1073
  hint: '建议继续运行 openyida compile <file> 或使用 --compile 直接编译校验。',
1072
1074
  success: '页面生成完成',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.7.22",
3
+ "version": "2026.7.23-1",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -259,6 +259,10 @@ openyida copy
259
259
 
260
260
  fast_build 页面源码默认不得使用 \`this.dataSourceMap.*\`,除非本轮已经明确创建并绑定设计器数据源;默认使用入口型页面或 \`this.utils.yida.*\` 查询已创建表单。
261
261
 
262
+ fast_build 创建/解析多个表单后,页面阶段需要字段映射时,对每个目标表单默认只执行一次 \`openyida get-schema <appType> <formUuid> --field-map-json\`,读取完整 JSON 并写入/复用 \`.cache/<项目名>-schema.json\`;不要用 \`head\` / \`tail\` / \`grep\` 截断 schema stdout 后重复拉取。
263
+
264
+ Canvas 页面实现二选一:走模板路径时先写业务化 \`page-spec.json\` 再 \`openyida generate-page ... --spec ... --compile\`,之后只做必要小范围 Edit/patch;如果已经明确最终页面结构,跳过 \`generate-page\`,直接 Write 最终 \`.canvas.jsx\`。不要 generate-page 后马上 Read 大段源码并全量 Write 覆盖同一路径。
265
+
262
266
  不要默认加载 \`yida-page-uiux\`、\`yida-data-source-connectors\`、\`yida-data-management\`、\`yida-nav-group\`、\`yida-dashboard\`,也不要默认做示例数据、导航整理、截图验收、公开访问、长 PRD 或深读 references;这些只在用户明确要求或 \`full_demo\` / \`deep_design\` 时执行。
263
267
 
264
268
  ## 子技能目录
@@ -149,6 +149,10 @@ schema-managed create/update 必须等待用户对当前 `planId` 显式批准
149
149
 
150
150
  **Canvas 数据边界**:完整应用/真实交付页如果展示列表、看板或详情记录,必须优先把本轮真实 `appType/formUuid/fieldId` 写入 `page-spec.json` 的 `dataBinding.mode=form`;需要演示记录时先写入真实表单再读取。未接真实表单且未写入 demo records 时,页面展示空态/入口,不用前端 seedRows 冒充业务数据。
151
151
 
152
+ **Schema 获取去重**:完整应用创建/解析多个表单后,页面阶段需要字段映射时,对每个目标表单默认只执行一次 `openyida get-schema <appType> <formUuid> --field-map-json`,读取完整 JSON 并合并到 `.cache/<项目名>-schema.json` 复用。不要用 `head`/`tail`/`grep` 截断 get-schema stdout 作为字段证据,也不要因此对同一表单重复拉取多轮 schema。
153
+
154
+ **Canvas 生成路径二选一**:走模板路径时,先写业务化 `page-spec.json` 再 `openyida generate-page ... --spec ... --compile`,后续只读 manifest/摘要并小范围 Edit;不要立即 Read 大段源码再全量 Write 覆盖同一路径。若已经明确最终页面结构,跳过 `generate-page` 直接 Write 最终 `.canvas.jsx`。
155
+
152
156
  **doneWhen**:`yida-app` 发布主页面成功并输出可访问 URL。到这里默认完成;不要发布后继续 TaskCreate、重复读技能或继续规划。
153
157
 
154
158
  **optionalAfterDone**:导航整理、示例数据、公开访问、截图验证、深度视觉方向、数据源/连接器深度接入、报表/大屏,只在用户明确要求或 `yida-app` 模式为 `full_demo` / `deep_design` 时执行。
@@ -227,12 +231,13 @@ schema-managed create/update 必须等待用户对当前 `planId` 显式批准
227
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 只能说“源码已修改,尚未发布”,禁止说“页面已更新 / 已重新发布 / 已上线”。
228
232
  5. **命令输入文件禁止 shell 写入**:当 OpenYida 命令需要 JSON/YAML/CSV/config/script 文件参数时,先使用当前 agent 运行时提供的结构化文件写入工具(如 create_file / Write / file edit tool)创建文件,再把路径传给命令;禁止用 shell heredoc、`cat`/`echo`/`printf`/`tee` 加输出重定向,或把命令 stdout 重定向成业务文件。
229
233
  6. **读文件少用 Bash 噪声**:读取或定位 workspace 文件优先用宿主的 Read / Glob / Grep;OpenYida CLI 已返回成功 JSON、URL 或 `formUuid/appType` 时,不要再用 Bash `cat`/`ls` 做无意义复核。
234
+ 7. **OpenYida CLI 不吞诊断**:不要给 `openyida` 命令加 `2>/dev/null`;失败时保留 stdout/stderr(必要时用 `2>&1` 合并诊断)。遇到 DENIED 或同一命令重复失败,先换策略、改输入或重做只读确认,不要盲目微调后重跑。
230
235
 
231
236
  ### 重要规则(IMPORTANT,影响质量/性能/可维护性)
232
237
 
233
238
  1. **按阶段加载必要技能**:按意图选 1 个主技能;完整应用按阶段加载当下唯一需要的子技能,禁止并发批量读取多个 `SKILL.md` 或预读未来阶段技能。
234
239
  2. **Resource-First**:任何 legacy 写操作前先解析本轮显式资源、agent bound context、workspace cache/config、历史上下文;已有目标资源时默认修改/补齐/发布,只有目标缺失且意图允许创建时才加载 create 类技能。
235
- 3. **优先复用 direct 映射**:仅对 direct/standalone 资源,已有 `.cache/<项目名>-schema.json` 中可确认新鲜的 `appType`/`formUuid`/`fieldId` 可复用;该文件不是 Schema-as-Code state,也不是远端真相。字段缺失、重名或结构变化时执行 `get-schema --compact --resolve-fields`,不得猜测。
240
+ 3. **优先复用 direct 映射**:仅对 direct/standalone 资源,已有 `.cache/<项目名>-schema.json` 中可确认新鲜的 `appType`/`formUuid`/`fieldId` 可复用;该文件不是 Schema-as-Code state,也不是远端真相。字段缺失、重名或结构变化时执行 `get-schema --compact --resolve-fields`;完整应用页面需要多字段/多表单映射时每表单一次性执行 `get-schema --field-map-json` 并缓存完整字段摘要。不得猜测字段 ID,也不要用 `head`/`tail`/`grep` 截断 schema stdout 当证据。
236
241
  4. **模板优先**:复杂产物先用 `openyida sample` 或现有示例生成骨架,再做最小改动。
237
242
  5. **配置承载优先于代码**:字段/公式/联动/报表/审批/集成交给对应技能,自定义页面只做展示与胶水。
238
243
  6. **数据性能优先**:统计聚合用 `yida-report` 服务端聚合,不在前端拉全量后自行聚合。
@@ -36,6 +36,7 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
36
36
  - 只有用户只给应用名称、存在多个候选、resource context 冲突,或需要诊断目标 app 访问失败时,才运行 `openyida app-list [--size N]`。
37
37
  - 已知 `appType` 后,查询该应用下表单/页面用 `openyida list-forms <appType> [--keyword <text>]`;选择页面发布目标时只用 `formType=display`。
38
38
  - 查询表单/页面 Schema、字段 ID 或批量字段摘要用 `openyida get-schema <appType> <formUuid|--all> ...`。
39
+ - 完整应用页面阶段如果需要多个表单的字段映射,默认对每个目标业务表单执行一次 `openyida get-schema <appType> <formUuid> --field-map-json`,读取完整 JSON 并合并到 `.cache/<项目名>-schema.json`;不要对同一表单用 `tail/head/grep` 截断 stdout 后再重复拉取。
39
40
  - 阶段 0 禁止编造 `list-apps` / `get-app`;也不要把 `--app-type` / `--form-uuid` 当成 `list-forms` 或 `get-schema` 的参数。按目的在 `app-list`、`list-forms`、`get-schema` 三者中选择。
40
41
 
41
42
  该阶段只决定普通 OpenYida resource context;schema-managed 路径仍以 schema CLI 的 validate/plan/apply 结果为准。schema-managed create/update 必须停在当前 `planId`,等待用户显式批准后才可执行 `apply`;`nextAction`、错误恢复或本技能判断都不能授予 `mixed/write`。Phase 1 中 report、automation、page config、delete、pull 不从 Manifest fallback 到本技能的 legacy workflow。
@@ -91,6 +92,7 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
91
92
 
92
93
  [Step 6] 编写自定义页面代码 → 默认 use_skill("yida-canvas-custom-page", "生成 Code Canvas 主页面")
93
94
  ↓ 先写业务化 page-spec.json,再 openyida generate-page <模板> --theme-profile yida-app-theme --theme-scope page --spec <page-spec.json> --compile
95
+ ↓ 字段映射来自 `.cache/<项目名>-schema.json`;同一表单不要重复 get-schema,除非刚修改字段或缓存不完整
94
96
  ↓ 本轮已创建/解析业务表单且页面需要列表/看板/详情数据时,必须在 spec.dataBinding 写 mode=form + 真实 appType/formUuid/fieldId;深度接入再加载 yida-canvas-data-binding
95
97
  ↓ 明确要求普通自定义页面 JSX/Jsx 组件链路,或强依赖 this.$ / this.utils.yida.* / this.dataSourceMap 等实例桥时选择 yida-custom-page
96
98
 
@@ -154,11 +156,16 @@ UI 不是独立替代主流程的步骤,而是按模式插入到页面生成
154
156
  | `yida-data-management` | `openyida sample yida-data-management form-field-template` | 表单字段定义和数据插入 |
155
157
  | `yida-create-app` | `openyida sample yida-create-app ipd-app-template` | 完整应用创建示例 |
156
158
 
157
- 代码生成前必须:
159
+ 页面实现必须二选一:
160
+
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
+ - **手写路径**:如果已经明确最终页面结构、数据桥和视觉细节,跳过 `generate-page`,直接 Write 最终 `.canvas.jsx`,再做本地快检和 publish。不要先生成模板再把模板完全覆盖。
163
+
164
+ 选择模板路径时必须:
158
165
 
159
166
  1. 先从 PRD 提炼当前业务自己的 page spec;
160
167
  2. 再执行对应的 `openyida generate-page` 命令生成 Code Canvas 骨架;
161
- 3. 读取 manifest `domainFidelity`,若仍是 sample-reference / draft,则补 spec 或改源码;
168
+ 3. 读取 manifest / CLI 摘要的 `domainFidelity`,若仍是 sample-reference / draft,则补 spec 或小范围改源码;
162
169
  4. 以模板为基础扩展交互和真实数据;
163
170
  5. 验证所有参数名称与 CLI 一致。
164
171
 
@@ -171,7 +178,7 @@ UI 不是独立替代主流程的步骤,而是按模式插入到页面生成
171
178
  | 0. 解析资源上下文 | 无 | 合并本轮显式资源、agent bound context、workspace config/cache、会话历史;本轮显式目标覆盖 bound context;判定 app/page/form/process 的 `source` 和 `allowCreate` | 明确复用、创建缺口或需要 ask_human |
172
179
  | 1. resolve app + app name | `yida-create-app` 仅在 app 缺失且允许创建时加载;`openyida update-app` 仅用于预创建占位 app 改名 | 已有 `appType`/应用 URL/bound app 时直接复用;若 bound/precreated app 仍是占位名,在语义名稳定后执行 `openyida update-app <appType> --name "<语义应用名>"`;否则创建应用并提取真实 `appType` | 拿到真实目标 `appType`,预创建占位 app 已改成语义名或明确跳过,且不会重复创建同类 app |
173
180
  | 2. 记录最小需求 | 无 | 写 `prd/<项目名>.md`:只记录 MVP 假设、核心表单/页面、完成标准;写/更新 `.cache/<项目名>-schema.json` standalone 映射;不要写长 PRD | 业务语义和 ID 存储位置明确 |
174
- | 3. resolve forms | `yida-create-form-page` | 已有目标表单时 update/patch/rule/bind-datasource;缺少支撑 MVP 的核心表单且允许创建时才 create;字段配置文件写入 `.cache/openyida/<项目名>/` | 拿到或确认表单 `formUuid` 和真实 `fieldId` |
181
+ | 3. resolve forms | `yida-create-form-page` | 已有目标表单时 update/patch/rule/bind-datasource;缺少支撑 MVP 的核心表单且允许创建时才 create;字段配置文件写入 `.cache/openyida/<项目名>/`;创建/解析多个表单后,对每个目标表单最多一次性获取完整 `--field-map-json` 并合并写回 `.cache/<项目名>-schema.json` | 拿到或确认表单 `formUuid` 和真实 `fieldId` |
175
182
  | 4. resolve main page | `yida-create-page` 仅在主页面缺失且允许创建时加载 | 已有页面 URL / `formUuid` / bound page 时直接作为主页面;否则创建一个用户主入口 display page | 拿到真实目标页面 `formUuid`,且不会重复创建页面 |
176
183
  | 5. 编写/更新页面 | 默认 `yida-canvas-custom-page`;明确要求 JSX/Jsx 组件链路或实例桥强依赖时选择 `yida-custom-page` | 生成或修改主页面源码;只实现 MVP 首屏和核心操作。可用已解析表单链接、真实空态、表单入口和轻量指标口径完成主页面;若展示业务列表/看板/详情记录,必须接本轮真实表单 `dataBinding.mode=form`,或先写入 demo records 后再读取;不要加载视觉/密度/报表/数据源等额外技能 | 本地源码通过对应页面技能的基础校验;未执行 publish 时仍是“源码已修改,尚未发布” |
177
184
  | 6. 发布页面 | `yida-publish-page` | 按页面链路校验后发布到已解析主页面:Canvas `.canvas.jsx` 使用 `openyida publish` 的 Canvas 编译阶段或 `compileCanvasLocal` 快检;普通自定义页面 `.oyd.jsx` / `.jsx` 跑 `check-page` / `compile`;再执行 `openyida publish <source> <appType> <displayPageFormUuid>` 发布主页面 | 发布成功并获得可访问 URL |
@@ -272,6 +279,7 @@ UI 不是独立替代主流程的步骤,而是按模式插入到页面生成
272
279
  ## 错误处理
273
280
 
274
281
  - 不编造 `appType`、`formUuid`、`fieldId`、`reportId`。
282
+ - OpenYida CLI 不要加 `2>/dev/null`;失败时保留 stdout/stderr 诊断。遇到 DENIED 或同一命令重复失败,先换策略、修改输入文件/参数/登录态/组织或重新只读取证,再重试。
275
283
  - 同一命令失败后,必须改变登录态、组织、参数、输入文件或字段 ID 后才能重试;禁止无修改连续重试。
276
284
  - corpId 与目标组织不一致时先停下,让用户选择重新登录或在当前组织继续。
277
285
  - 已有目标 app/page/form/process 时默认复用;只有用户明确要求新建另一个同类资源,或目标缺失且本次意图允许创建时,才加载 create 类子技能。
@@ -105,6 +105,7 @@ openyida sample yida-canvas-custom-page portal-native-components --output projec
105
105
  8. **门户运行态组件要补必需 props 和局部降级**:`QuickAccessCard` / `RecentlyUsedCard` 必须传 `theme="row-white"` 等必需 props,避免运行态读取 `theme.includes(...)` 报错;所有门户/字段/上传增强组件外层加局部 ErrorBoundary,单个组件不兼容时只降级该块,不让整页进入 Canvas 错误态。
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
+ 11. **页面生成二选一**:选择模板路径时,`openyida generate-page ... --spec ... --compile` 之后只读取 CLI 摘要或 `.openyida-page.json` 判断 `domainFidelity` / dataBinding,并对生成源码做小范围 Edit/patch;禁止立刻 Read 大段源码后全量 Write 覆盖同一路径。选择手写路径时,直接 Write 最终 `.canvas.jsx` 并快检/发布,不要先跑 `generate-page` 再完全覆盖。
108
109
 
109
110
  ## 数据真实性边界
110
111
 
@@ -165,12 +166,14 @@ openyida login --check-only --json
165
166
  # 2. 如需新页面,先创建空白自定义页拿 formUuid
166
167
  openyida create-page <appType> "<页面名>"
167
168
 
168
- # 3. 生成或复制 Canvas 源码
169
+ # 3. 生成或编写 Canvas 源码
170
+ # 模板路径:生成后基于 manifest/摘要和小范围 patch 演进,不全量覆盖生成文件。
169
171
  openyida generate-page workbench-home --theme-profile yida-app-theme --theme-scope page --output project/pages/src/workbench-home.canvas.jsx --compile
170
172
  openyida generate-page dashboard-overview --theme-profile yida-app-theme --theme-scope page --output project/pages/src/dashboard-overview.canvas.jsx --compile
171
173
  openyida generate-page portal-shell-home --theme-profile yida-app-theme --theme-scope page --output project/pages/src/portal-shell-home.canvas.jsx --compile
172
174
  openyida sample yida-canvas-custom-page native-components-smoke --output project/pages/src/native-components-smoke.canvas.jsx
173
175
  openyida sample yida-canvas-custom-page portal-native-components --output project/pages/src/portal-native-components.canvas.jsx
176
+ # 手写路径:已明确最终页面结构时,跳过 generate-page,直接 Write 最终 .canvas.jsx。
174
177
 
175
178
  # 4. 本地 Canvas 快检
176
179
  node -e "const fs=require('fs'); const {compileCanvasLocal}=require('./lib/app/canvas-compile'); const src=fs.readFileSync('project/pages/src/<页面名>.canvas.jsx','utf8'); console.log(compileCanvasLocal(src).importedModules)"
@@ -178,8 +181,8 @@ node -e "const fs=require('fs'); const {compileCanvasLocal}=require('./lib/app/c
178
181
  # 5. 发布(本轮修改源码后的远端完成证据)
179
182
  openyida publish project/pages/src/<页面名>.canvas.jsx <appType> <formUuid>
180
183
 
181
- # 6. 发布后回读 Schema 验收
182
- openyida get-schema <appType> <formUuid> > .cache/openyida/<页面名>-schema.json
184
+ # 6. 发布后回读字段摘要验收;如需留证,用结构化文件写入工具保存 stdout,不用 shell 重定向
185
+ openyida get-schema <appType> <formUuid> --field-map-json
183
186
  ```
184
187
 
185
188
  `openyida check-page` / `openyida compile` 当前面向普通自定义页面 `.oyd.jsx` / `.jsx`;Canvas 以 `compileCanvasLocal` 和 `openyida publish .canvas.jsx` 的 Canvas 编译阶段为准。`compileCanvasLocal` 是发布前快检,不能替代 `openyida publish` 的远端写入证据。
@@ -8,6 +8,8 @@
8
8
 
9
9
  `generate-page` 的模板只用于选定运行时契约、数据桥、主题变量和首版 primitives;它不是最终视觉稿。生成真实页面时,必须结合 `yida-page-uiux` 的视觉方向决策块重写区块顺序、信息层级、局部构图、文案和样式节奏。可以保留模板的编译安全结构和必要 primitive class,但不要照搬默认 Hero、卡片网格、三段式卖点或库存文案。
10
10
 
11
+ 页面生成路径必须二选一:走模板路径时,先写业务化 `page-spec.json` 并执行 `openyida generate-page ... --spec ... --compile`,之后只读取 CLI 摘要或 `.openyida-page.json`,再对生成源码做小范围 Edit/patch;不要立刻 Read 大段源码后全量 Write 覆盖同一路径。若已经明确最终页面结构、数据桥和视觉细节,走手写路径,跳过 `generate-page`,直接 Write 最终 `.canvas.jsx`。
12
+
11
13
  生成器会在 `.openyida-page.json` 中写入 `domainFidelity`,并在 CLI 输出中提示当前页面是否还依赖 sample fallback:
12
14
 
13
15
  - `domain-ready`:主要业务语义已覆盖,sample 只剩编译骨架。
@@ -24,6 +24,7 @@ description: 表单页面创建与更新,支持 19 种业务字段和 Divider
24
24
  - 不要在 update / patch / rule / validation / bind-datasource 模式中使用猜测的 fieldId,必须先用 `yida-get-schema` 获取
25
25
  - 不要用此命令操作数据记录(增删改查),应使用 `yida-data-management`
26
26
  - 不要用 shell heredoc、`cat`/`echo`/`printf`/`tee` 或重定向生成字段、变更、补丁、规则、数据源 JSON 文件
27
+ - OpenYida CLI 不要加 `2>/dev/null`;失败时保留 stdout/stderr 诊断,遇到 DENIED 或重复失败必须换策略
27
28
  - 已有目标表单且用户是改字段/联动/属性时,不要创建新表单;必须走 update/patch/rule/bind-datasource。
28
29
  - 不要用 `GroupContainer` / `PageSection` 承载普通业务分组;普通分组必须优先用 `Divider`
29
30
 
@@ -120,6 +121,15 @@ openyida create-form create <appType> <formTitle> <fieldsJsonOrFile> [--layout d
120
121
  {"success":true,"formUuid":"FORM-XXX","formTitle":"用户信息表","appType":"APP_xxx","fieldCount":4,"url":"{base_url}/APP_xxx/workbench/FORM-XXX"}
121
122
  ```
122
123
 
124
+ ### create 失败恢复决策树
125
+
126
+ create 命令失败后,不要立刻重复同一条 create:
127
+
128
+ 1. 先确认字段 JSON 文件存在,且内容是结构化写入后的最终字段数组/对象,不是半截 JSON、update changes 或 shell 拼接残留。
129
+ 2. 运行 `openyida list-forms <appType> --keyword "<表单名>"` 查同名表单;若本轮刚创建过空白表单或已有同名目标表单,优先走 `create-form update` / `patch` / 后续显式 resume 能力复用,不再 create。
130
+ 3. 只有确认远端没有同名目标表单,并且已经修改输入文件、参数、登录态或组织后,才重试 create。
131
+ 4. 同一 create 命令最多重试 2 次;仍失败时停止并带上完整 stdout/stderr、字段文件路径、appType、表单名和已发现的 formUuid 给用户。
132
+
123
133
  ## update 模式
124
134
 
125
135
  已有 `formUuid` / 表单 URL / bound form 时优先使用本模式;修改字段前必须用 `openyida get-schema` 确认字段 ID 和当前结构。
@@ -15,12 +15,16 @@ description: 确定性解析表单字段 ID(fieldId)和子表路径;agent
15
15
  - 不要缓存过期的 Schema 信息,表单结构变更后必须重新获取
16
16
  - 不要把进程内状态或 CLI 自动派生索引当作跨调用缓存;查询新字段时允许重新拉取完整 Schema
17
17
  - 不要把 `openyida get-schema` 的 stdout 通过 shell 重定向保存成 JSON,也不要用 heredoc、`cat`/`echo`/`printf`/`tee` 生成 Schema 文件
18
+ - 不要把 `openyida get-schema` 的 stdout 再接 `head`、`tail`、`grep`、`sed`、`awk` 等截断/筛选命令作为 Schema 证据;这会丢字段、选项或子表路径,导致后续重复拉取
19
+ - 不要在同一阶段对同一个 `formUuid` 连续执行 `--compact`、`--field-map-json`、完整 Schema 等多轮“探一段 stdout”式查询;除非表单刚被修改、上次命令失败/不完整,或排障需要完整组件 props
18
20
 
19
21
  ## 严格要求 (MUST DO)
20
22
 
21
23
  - **凡是需要用到字段 ID(fieldId)的操作,必须先执行此命令**,不得跳过
22
24
  - 页面开发、数据查询、报表配置或流程规则只需要字段身份时,先执行 `openyida get-schema <appType> <formUuid> --compact --resolve-fields "<字段1,字段2>"`,不要拉取完整 Schema
23
25
  - 页面开发默认使用 compact 输出,只读取必要字段契约,不内联完整 Schema
26
+ - 完整应用页面、看板、列表或详情页需要一个表单的大部分字段,或需要跨多个表单建立 `dataBinding` 时,优先对每个表单执行一次 `openyida get-schema <appType> <formUuid> --field-map-json`,消费完整 JSON 后解析所需字段,不用 shell 截断 stdout
27
+ - 多表单场景同一阶段同一 `formUuid` 默认最多拉取一次字段映射;把 `appType`、`formUuid`、`fieldId`、`label`、`componentName`、`options` 等合并写入 `<projectRoot>/.cache/<项目名>-schema.json`,后续页面 spec 和源码复用该 standalone ID 映射
24
28
  - 执行 compact 查询后,只消费唯一命中的 `fields[]`;`missingFields` 或 `ambiguousFields` 非空时停止,不得猜测或继续写操作
25
29
  - 只有用户明确需要完整组件 props、布局结构、字段数据源配置,或 compact/summary 无法排障时,才执行不带 `--compact`/`--summary-json` 的完整 Schema 输出;拿到完整 Schema 后只读取必要片段,不内联完整 Schema
26
30
  - 已有 `<projectRoot>/.cache/<项目名>-schema.json` 等 standalone ID 映射文件可显式复用;目标字段缺失、重名、结构已变或无法确认新鲜度时,必须重新执行 compact 查询
@@ -74,13 +78,15 @@ openyida get-schema <appType> --all [--summary-json] [--output-dir <dir>] [--key
74
78
 
75
79
  ```bash
76
80
  openyida get-schema APP_XXX FORM-XXX --compact --resolve-fields "访客姓名,状态"
77
- openyida get-schema APP_XXX FORM-XXX --summary-json
81
+ openyida get-schema APP_XXX FORM-XXX --field-map-json
78
82
  ```
79
83
 
80
84
  Agent 只需要少量字段 ID 时,默认使用 `--compact --resolve-fields`,读取 `fields[].label`、`fields[].fieldId`、`fields[].componentType`、`fields[].valueType`、`fields[].path`、`fields[].labelPath` 和 `fields[].parentFieldId`。`path` 是稳定的 fieldId 数组,`labelPath` 是可读路径;所有可用语言的 label 都参与精确匹配。同名字段会进入 `ambiguousFields[].matches`,必须使用完整 `labelPath`、稳定 `path` 或已返回的 fieldId 重新精确选择,禁止取第一个。
81
85
 
82
86
  需要全量字段摘要和选项时继续使用 `--summary-json`。只有需要组件完整 props、布局结构、字段数据源配置或排障时,才执行不带 compact/summary 参数的完整 Schema 输出。
83
87
 
88
+ 消费输出时读取完整 JSON,再由 agent / 脚本解析字段;不要用 `tail -20`、`head -30` 或 `grep` 只看局部 stdout。局部查看可以作为人工调试,但不能作为后续写页面、写数据或配置流程的字段证据。
89
+
84
90
  如需复用输出,使用 agent 的结构化文件写入工具创建:
85
91
 
86
92
  ```text
@@ -167,7 +173,7 @@ Agent compact 模式输出共享 contract,不包含完整 Schema 或 props:
167
173
  |---------|----------|
168
174
  | 命令返回失败 | 确认 appType 和 formUuid 正确,检查登录态 |
169
175
  | 输出被终端截断 | 优先改用 `--summary-json`;确需完整 Schema 时,再将 stdout 通过结构化文件写入工具保存到 `<projectRoot>/.cache/openyida/<项目名或任务名>/<表单名>-schema.json`;不要使用 shell 重定向 |
170
- | 需要多个表单字段 ID | 使用批量 compact 模式:`openyida get-schema <appType> --all --summary-json --output-dir .cache/openyida/<项目名或任务名>/schemas`,默认只读 `index.json` 字段摘要 |
176
+ | 需要多个表单字段 ID | 使用批量摘要或每表单完整字段映射:`openyida get-schema <appType> --all --summary-json --output-dir .cache/openyida/<项目名或任务名>/schemas`,或对目标表单逐个执行一次 `--field-map-json`;默认只读完整 JSON / `index.json` 字段摘要,不用 `head`/`tail`/`grep` 截断 |
171
177
  | 批量部分失败 | 查看 stdout 的 `failedCount` 和 `forms[].errorMsg`,必要时提高 `--retries` 或缩小 `--keyword` 范围 |
172
178
  | 找不到目标字段 | 查看 `missingFields`,确认字段已创建后重新查询;不能手写猜测 fieldId |
173
179
  | 同名字段无法唯一确定 | 查看 `ambiguousFields[].matches[].labelPath` 和稳定 `path`,使用完整路径或 fieldId 重新查询;不得默认取第一个 |