openyida 2026.8.25 → 2026.8.27-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.
Files changed (61) hide show
  1. package/README.md +6 -5
  2. package/bin/yida.js +3 -6
  3. package/lib/aggregate-table/aggregate-table.js +37 -14
  4. package/lib/aggregate-table/contract.js +234 -0
  5. package/lib/app/canvas-compile.js +169 -0
  6. package/lib/app/create-form/api-path.js +11 -2
  7. package/lib/app/create-form.js +109 -339
  8. package/lib/app/create-page.js +25 -13
  9. package/lib/app/form-detail-style.js +3 -28
  10. package/lib/app/import-app.js +13 -31
  11. package/lib/app/publish.js +18 -74
  12. package/lib/connector/api.js +144 -36
  13. package/lib/connector/connector-create.js +4 -4
  14. package/lib/connector/connector-delete.js +7 -10
  15. package/lib/connector/connector-test.js +3 -2
  16. package/lib/connector/contract.js +98 -0
  17. package/lib/core/command-manifest.js +10 -19
  18. package/lib/core/locales/en.js +23 -25
  19. package/lib/core/locales/zh.js +23 -23
  20. package/lib/core/query-data.js +111 -12
  21. package/lib/core/utils.js +41 -0
  22. package/lib/core/yida-client.js +19 -0
  23. package/lib/integration/integration-create.js +12 -0
  24. package/lib/integration/integration-update.js +113 -0
  25. package/lib/report/append.js +18 -1
  26. package/lib/report/chart-builder.js +26 -0
  27. package/lib/report/contract.js +158 -0
  28. package/lib/report/data-model.js +44 -48
  29. package/lib/report/field-utils.js +47 -3
  30. package/lib/report/http.js +28 -5
  31. package/lib/report/index.js +23 -1
  32. package/package.json +3 -7
  33. package/yida-skills/references/yida-api.md +0 -29
  34. package/yida-skills/skills/yida-app/references/common-issues.md +2 -0
  35. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +1 -0
  36. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +10 -8
  37. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +8 -6
  38. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +14 -0
  39. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +2 -0
  40. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +7 -5
  41. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +1 -0
  42. package/yida-skills/skills/yida-connector/SKILL.md +6 -6
  43. package/yida-skills/skills/yida-form-detail/references/injection-guide.md +0 -22
  44. package/yida-skills/skills/yida-integration/SKILL.md +23 -5
  45. package/yida-skills/skills/yida-publish-page/SKILL.md +3 -3
  46. package/yida-skills/skills/yida-report/references/schema-builder-details.md +1 -1
  47. package/docs/capabilities.md +0 -222
  48. package/scripts/build-skills-package.js +0 -513
  49. package/scripts/check-release-risks.js +0 -254
  50. package/scripts/check-syntax.js +0 -112
  51. package/scripts/demo-dws.sh +0 -106
  52. package/scripts/generate-command-docs.js +0 -155
  53. package/scripts/i18n-baseline.json +0 -50
  54. package/scripts/nightly-smoke.js +0 -116
  55. package/scripts/solution-center-runner.js +0 -368
  56. package/scripts/validate-ci.sh +0 -64
  57. package/scripts/validate-command-manifest.js +0 -403
  58. package/scripts/validate-i18n.js +0 -197
  59. package/scripts/validate-package-size.js +0 -109
  60. package/scripts/validate-skills.js +0 -608
  61. package/scripts/validate-structure.js +0 -81
package/README.md CHANGED
@@ -320,8 +320,6 @@ OPENYIDA_E2E=1 npm run eval:all -- --skill yida-dashboard --screenshot
320
320
  # whitelist. Use the Node version you start it with, so run `nvm use 20` first.
321
321
  npm run eval:dashboard # http://127.0.0.1:4500
322
322
 
323
- # You can also run eval directly via the CLI:
324
- openyida eval --mode comprehensive --skill yida-dashboard
325
323
  ```
326
324
 
327
325
  Configuration precedence is `CLI flag > env (OPENYIDA_EVAL_*) > scripts/eval/eval.config.json > defaults`.
@@ -349,6 +347,9 @@ openyida connector list
349
347
  openyida integration create APP_XXX FORM_XXX "Sync customer data"
350
348
  openyida integration create APP_XXX FORM_XXX "Approval result notify" \
351
349
  --events processFinish --approval-actions agree,disagree --receivers 123456
350
+ # Capability probe only; currently fails closed before auth/spec reads/remote writes
351
+ openyida integration update APP_XXX FORM_XXX LPROC_XXX \
352
+ --spec .cache/openyida/integration/desired-spec.json
352
353
  openyida create-report APP_XXX "Sales Dashboard" .cache/openyida/reports/charts.json
353
354
  openyida append-chart APP_XXX REPORT_XXX .cache/openyida/reports/chart.json
354
355
  ```
@@ -461,7 +462,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
461
462
  | `openyida connector list` | List HTTP connectors |
462
463
  | `openyida connector create "name" "domain" ...` | Create a connector |
463
464
  | `openyida connector detail <id>` | View connector details |
464
- | `openyida connector delete <id>` | Delete a connector |
465
+ | `openyida connector delete <id> [--force]` | Show manual deletion guidance (CLI does not delete) |
465
466
  | `openyida connector add-action --operations <file> --connector-id <id>` | Add an action |
466
467
  | `openyida connector list-actions <id>` | List actions |
467
468
  | `openyida connector delete-action <id> <operation-id>` | Delete an action |
@@ -477,6 +478,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
477
478
  | Command | Description |
478
479
  |---------|-------------|
479
480
  | `openyida integration create <appType> ... [--spec file.json]` | Create integration automation flow |
481
+ | `openyida integration update <appType> <formUuid> <processCode> --spec <desired-spec.json> [--publish]` | Probe integration update capability (currently blocked without full readback) |
480
482
  | `openyida integration list <appType> [--form-uuid <uuid>] [--status y\|n] [--json]` | List integration automation flows |
481
483
  | `openyida integration enable <appType> <formUuid> <processCode>` | Enable integration automation flow |
482
484
  | `openyida integration disable <appType> <formUuid> <processCode>` | Disable integration automation flow |
@@ -497,7 +499,6 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
497
499
  | `openyida copy [--force]` | Copy project working directory |
498
500
  | `openyida sample [--list]` | Output code samples/templates |
499
501
  | `openyida doctor [--fix]` | Environment diagnostics & auto-fix |
500
- | `openyida eval --mode <mode> [--skill <name>] [--runs N]` | Multi-dimensional skill evaluation (doc quality, routing accuracy, safety, etc.) |
501
502
  | `openyida db-seq-fix [--fix]` | Detect and repair PostgreSQL sequence drift |
502
503
  | `openyida formula evaluate <formula\|file> [--schema file]` | Static-check Yida formula syntax and field refs |
503
504
  | `openyida update` | Check and update to latest version |
@@ -537,7 +538,7 @@ Form field definitions can include `alias` or `componentAlias` to populate Yida
537
538
 
538
539
  #### Workflow, Reports, and Integrations
539
540
 
540
- `openyida integration create` supports form events (`insert`, `update`, `delete`, `comment`) and approval events (`processFinish`, `activityTask`; aliases: `approval`, `approvalNode`). Approval events require `--approval-actions agree,disagree,terminated`; `activityTask` also requires `--approval-node-ids <nodeId,...>`.
541
+ `openyida integration create` supports form events (`insert`, `update`, `delete`, `comment`) and approval events (`processFinish`, `activityTask`; aliases: `approval`, `approvalNode`). Approval events require `--approval-actions agree,disagree,terminated`; `activityTask` also requires `--approval-node-ids <nodeId,...>`. Passing `--process-code` selects a full graph replacement and now requires explicit `--replace`; it is not a safe update. `integration update` currently performs capability detection only: because full platform `processJson` + `viewJson` readback is unproven, it writes a redacted local probe artifact and returns `PLATFORM_PROBE_REQUIRED` before authentication, spec reading, or remote writes. It does not edit a flow.
541
542
 
542
543
  ## Agent Skills
543
544
 
package/bin/yida.js CHANGED
@@ -1173,6 +1173,9 @@ async function main() {
1173
1173
  if (subCommand === 'create') {
1174
1174
  const { run: runIntegration } = require('../lib/integration/integration-create');
1175
1175
  await runIntegration(subArgs);
1176
+ } else if (subCommand === 'update') {
1177
+ const { run: runIntegrationUpdate } = require('../lib/integration/integration-update');
1178
+ await runIntegrationUpdate(subArgs);
1176
1179
  } else if (subCommand === 'list') {
1177
1180
  const { runList } = require('../lib/integration/integration-list');
1178
1181
  await runList(subArgs);
@@ -1255,12 +1258,6 @@ async function main() {
1255
1258
  break;
1256
1259
  }
1257
1260
 
1258
- case 'eval': {
1259
- const { main: evalMain } = require('../scripts/eval/runner');
1260
- await evalMain(args);
1261
- break;
1262
- }
1263
-
1264
1261
  default: {
1265
1262
  throwUnknownCommand(command, args);
1266
1263
  }
@@ -8,16 +8,13 @@ const { t } = require('../core/i18n');
8
8
  const { buildYidaTitleI18n, normalizeYidaLocale, resolveContentLocale } = require('../core/yida-i18n');
9
9
  const { parseOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
10
10
  const { fetchFormPageList, resolveLocalizedText } = require('../app/form-navigation');
11
+ const {
12
+ DESIGN_KEYS,
13
+ assertAggregateDesignConfig,
14
+ assertAggregateDesignReadback,
15
+ } = require('./contract');
11
16
 
12
17
  const FORM_TYPE_VIRTUAL_VIEW = 'virtualView';
13
- const DESIGN_KEYS = [
14
- 'relationForms',
15
- 'relationships',
16
- 'aggregatedFields',
17
- 'auxFields',
18
- 'formulaFields',
19
- 'validators',
20
- ];
21
18
 
22
19
  function hasHelpFlag(args) {
23
20
  return (args || []).includes('--help') || (args || []).includes('-h');
@@ -133,7 +130,7 @@ async function checkVirtualViewFeature(authRef, appType) {
133
130
  }
134
131
 
135
132
  async function createEmptyVirtualView(authRef, appType, title) {
136
- return createYidaClient({ authRef }).postForm(
133
+ return createYidaClient({ authRef }).postFormOnce(
137
134
  `/dingtalk/web/${appType}/query/formdesign/saveFormSchemaInfo.json`,
138
135
  buildCreateEmptyPostData(title)
139
136
  );
@@ -157,7 +154,11 @@ async function postVirtualViewDesign(authRef, appType, formUuid, designInfo, act
157
154
  throw new Error(`Unknown aggregate table action: ${action}`);
158
155
  }
159
156
 
160
- return createYidaClient({ authRef }).postForm(
157
+ const client = createYidaClient({ authRef });
158
+ const post = action === 'save' || action === 'publish'
159
+ ? client.postFormOnce.bind(client)
160
+ : client.postForm.bind(client);
161
+ return post(
161
162
  `/alibaba/web/${appType}/query/virtualview/${endpoint}`,
162
163
  buildDesignPostData(formUuid, designInfo, gmtModified)
163
164
  );
@@ -318,6 +319,11 @@ async function runCreateEmpty(args) {
318
319
  const createResult = await createEmptyVirtualView(authRef, appType, name);
319
320
  const created = assertSuccess(createResult, t('aggregate_table.create_empty'));
320
321
  const formUuid = (created && created.formUuid) || created;
322
+ if ((typeof formUuid !== 'string' && typeof formUuid !== 'number') || String(formUuid).length === 0) {
323
+ const error = new Error('Aggregate table creation returned no recoverable identity.');
324
+ error.code = 'AGGREGATE_CREATE_IDENTITY_MISSING';
325
+ throw error;
326
+ }
321
327
  const designUrl = buildDesignUrl(authRef.baseUrl, appType, formUuid, { fromNew: true });
322
328
  const workbenchUrl = buildWorkbenchUrl(authRef.baseUrl, appType, formUuid);
323
329
 
@@ -401,6 +407,7 @@ async function runPreview(args) {
401
407
 
402
408
  const authRef = await createAuthRef();
403
409
  const designInfo = normalizeDesignConfig(readJsonInput(input), formUuid);
410
+ assertAggregateDesignConfig(designInfo, { mode: 'draft' });
404
411
  const result = await postVirtualViewDesign(authRef, appType, formUuid, designInfo, 'preview');
405
412
  const rows = assertSuccess(result, t('aggregate_table.preview'));
406
413
  const rowCount = Array.isArray(rows) ? rows.length : 0;
@@ -439,13 +446,28 @@ async function runSaveOrPublish(args, action) {
439
446
  }
440
447
 
441
448
  const authRef = await createAuthRef();
442
- const { designInfo, gmtModified } = await loadDesignForMutation(authRef, appType, formUuid, input, action);
443
- if (isPublish && designInfo.relationForms.length === 0) {
444
- throw new Error(t('aggregate_table.publish_requires_source'));
445
- }
449
+ const { designInfo, currentConfig, gmtModified } = await loadDesignForMutation(authRef, appType, formUuid, input, action);
450
+ assertAggregateDesignConfig(designInfo, { mode: isPublish ? 'publish' : 'draft' });
446
451
 
447
452
  const result = await postVirtualViewDesign(authRef, appType, formUuid, designInfo, action, gmtModified);
448
453
  const content = assertSuccess(result, isPublish ? t('aggregate_table.publish') : t('aggregate_table.save'));
454
+ const readbackResult = await getVirtualViewConfig(authRef, appType, formUuid);
455
+ const readback = assertSuccess(readbackResult, t('aggregate_table.inspect'));
456
+ assertAggregateDesignReadback(designInfo, readback);
457
+ const responseRevision = content && content.gmtModified;
458
+ const readbackRevision = readback && readback.gmtModified;
459
+ if ((responseRevision === undefined || responseRevision === null) &&
460
+ (readbackRevision === undefined || readbackRevision === null)) {
461
+ const error = new Error('AGGREGATE_WRITE_REVISION_MISSING');
462
+ error.code = 'AGGREGATE_WRITE_REVISION_MISSING';
463
+ throw error;
464
+ }
465
+ if ((responseRevision === undefined || responseRevision === null) &&
466
+ currentConfig && String(readbackRevision) === String(currentConfig.gmtModified)) {
467
+ const error = new Error('AGGREGATE_WRITE_REVISION_UNCHANGED');
468
+ error.code = 'AGGREGATE_WRITE_REVISION_UNCHANGED';
469
+ throw error;
470
+ }
449
471
  const designUrl = buildDesignUrl(authRef.baseUrl, appType, formUuid);
450
472
  const workbenchUrl = buildWorkbenchUrl(authRef.baseUrl, appType, formUuid);
451
473
 
@@ -465,6 +487,7 @@ async function runSaveOrPublish(args, action) {
465
487
  formUuid,
466
488
  gmtModified: content && content.gmtModified,
467
489
  response: content,
490
+ readbackVerified: true,
468
491
  designUrl,
469
492
  workbenchUrl,
470
493
  }, designUrl, { stage: `aggregate_table_${action}_success`, title: formUuid }, flags.openMode), null, 2));
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ const { isDeepStrictEqual } = require('util');
4
+
5
+ const DESIGN_KEYS = [
6
+ 'relationForms',
7
+ 'relationships',
8
+ 'aggregatedFields',
9
+ 'auxFields',
10
+ 'formulaFields',
11
+ 'validators',
12
+ ];
13
+
14
+ // Mirrors the designer defaults read from configLimit. Only limits that map to
15
+ // persisted design arrays are enforced here.
16
+ const DEFAULT_LIMITS = Object.freeze({
17
+ relationForms: 10,
18
+ relationships: 10,
19
+ formulaFields: 10,
20
+ auxFields: 10,
21
+ });
22
+
23
+ function addError(errors, code, path) {
24
+ errors.push({ code, path });
25
+ }
26
+
27
+ function isNonEmptyValue(value) {
28
+ return value !== null && value !== undefined && String(value).length > 0;
29
+ }
30
+
31
+ function validateArrayShapes(config, errors) {
32
+ for (const key of DESIGN_KEYS) {
33
+ if (!Array.isArray(config && config[key])) {
34
+ addError(errors, 'AGGREGATE_DESIGN_ARRAY_REQUIRED', key);
35
+ }
36
+ }
37
+ }
38
+
39
+ function validateLimits(config, limits, errors) {
40
+ const limitCodes = {
41
+ relationForms: 'AGGREGATE_RELATION_FORMS_LIMIT',
42
+ relationships: 'AGGREGATE_RELATIONSHIPS_LIMIT',
43
+ formulaFields: 'AGGREGATE_FORMULA_FIELDS_LIMIT',
44
+ auxFields: 'AGGREGATE_AUX_FIELDS_LIMIT',
45
+ };
46
+
47
+ for (const [key, limit] of Object.entries(limits)) {
48
+ if (Array.isArray(config[key]) && config[key].length > limit) {
49
+ addError(errors, limitCodes[key] || 'AGGREGATE_DESIGN_LIMIT', key);
50
+ }
51
+ }
52
+ }
53
+
54
+ function validateEntries(config, errors) {
55
+ if (Array.isArray(config.relationForms)) {
56
+ config.relationForms.forEach((item, index) => {
57
+ if (!item || !isNonEmptyValue(item.formUuid)) {
58
+ addError(errors, 'AGGREGATE_RELATION_FORM_INVALID', `relationForms[${index}]`);
59
+ }
60
+ });
61
+ }
62
+
63
+ if (Array.isArray(config.relationships)) {
64
+ config.relationships.forEach((item, index) => {
65
+ const relationshipInfos = item && item.relationshipInfos;
66
+ if (!Array.isArray(relationshipInfos) || relationshipInfos.length === 0 ||
67
+ relationshipInfos.some((entry) => entry === null || entry === undefined)) {
68
+ addError(errors, 'AGGREGATE_RELATIONSHIP_INVALID', `relationships[${index}]`);
69
+ }
70
+ });
71
+ }
72
+
73
+ if (Array.isArray(config.aggregatedFields)) {
74
+ config.aggregatedFields.forEach((item, index) => {
75
+ if (!item || !isNonEmptyValue(item.name)) {
76
+ addError(errors, 'AGGREGATE_COLUMN_INVALID', `aggregatedFields[${index}]`);
77
+ }
78
+ });
79
+ }
80
+
81
+ if (Array.isArray(config.formulaFields)) {
82
+ config.formulaFields.forEach((item, index) => {
83
+ if (!item || !isNonEmptyValue(item.formula)) {
84
+ addError(errors, 'AGGREGATE_FORMULA_FIELD_INVALID', `formulaFields[${index}]`);
85
+ }
86
+ });
87
+ }
88
+
89
+ if (Array.isArray(config.validators)) {
90
+ config.validators.forEach((item, index) => {
91
+ if (!item || !isNonEmptyValue(item.formula)) {
92
+ addError(errors, 'AGGREGATE_VALIDATOR_INVALID', `validators[${index}]`);
93
+ }
94
+ });
95
+ }
96
+ }
97
+
98
+ function validatePublishCompleteness(config, errors) {
99
+ const required = [
100
+ ['relationForms', 'AGGREGATE_RELATION_FORMS_REQUIRED'],
101
+ ['relationships', 'AGGREGATE_RELATIONSHIPS_REQUIRED'],
102
+ ['aggregatedFields', 'AGGREGATE_COLUMNS_REQUIRED'],
103
+ ['formulaFields', 'AGGREGATE_FORMULA_FIELDS_REQUIRED'],
104
+ ];
105
+
106
+ for (const [key, code] of required) {
107
+ if (!Array.isArray(config[key]) || config[key].length === 0) {
108
+ addError(errors, code, key);
109
+ }
110
+ }
111
+ }
112
+
113
+ function validateAggregateDesignConfig(config, options = {}) {
114
+ const mode = options.mode || 'draft';
115
+ if (mode !== 'draft' && mode !== 'publish') {
116
+ throw new Error(`Unsupported aggregate contract mode: ${mode}`);
117
+ }
118
+
119
+ const safeConfig = config && typeof config === 'object' && !Array.isArray(config)
120
+ ? config
121
+ : {};
122
+ const limits = { ...DEFAULT_LIMITS, ...(options.limits || {}) };
123
+ const errors = [];
124
+
125
+ validateArrayShapes(safeConfig, errors);
126
+ validateLimits(safeConfig, limits, errors);
127
+ validateEntries(safeConfig, errors);
128
+ if (mode === 'publish') {
129
+ validatePublishCompleteness(safeConfig, errors);
130
+ }
131
+
132
+ return { valid: errors.length === 0, errors };
133
+ }
134
+
135
+ function assertAggregateDesignConfig(config, options = {}) {
136
+ const result = validateAggregateDesignConfig(config, options);
137
+ if (result.valid) {
138
+ return config;
139
+ }
140
+
141
+ const error = new Error(result.errors.map((item) => item.code).join(', '));
142
+ error.name = 'AggregateDesignContractError';
143
+ error.code = 'AGGREGATE_DESIGN_CONTRACT_INVALID';
144
+ error.details = result.errors;
145
+ throw error;
146
+ }
147
+
148
+ function projectAggregateDesignConfig(config) {
149
+ const safeConfig = config && typeof config === 'object' ? config : {};
150
+ const projected = {};
151
+ for (const key of DESIGN_KEYS) {
152
+ projected[key] = Array.isArray(safeConfig[key]) ? safeConfig[key] : [];
153
+ }
154
+ return projected;
155
+ }
156
+
157
+ function isI18nValue(value) {
158
+ return value && typeof value === 'object' && !Array.isArray(value) && (
159
+ value.type === 'i18n' ||
160
+ Object.prototype.hasOwnProperty.call(value, 'zh_CN') ||
161
+ Object.prototype.hasOwnProperty.call(value, 'en_US') ||
162
+ Object.prototype.hasOwnProperty.call(value, 'pureEn_US')
163
+ );
164
+ }
165
+
166
+ function canonicalizeI18nValue(value) {
167
+ const zhCN = value.zh_CN || value.value || '';
168
+ const enUS = value.en_US || value.pureEn_US || zhCN;
169
+ return {
170
+ type: value.type || 'i18n',
171
+ zh_CN: String(zhCN),
172
+ en_US: String(enUS),
173
+ };
174
+ }
175
+
176
+ function isEmptyFilter(value) {
177
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
178
+ return false;
179
+ }
180
+ const rules = value.rules;
181
+ return (rules === undefined || rules === null || (Array.isArray(rules) && rules.length === 0)) &&
182
+ !value.logicOperator;
183
+ }
184
+
185
+ function canonicalizeAggregateValue(value, key = '') {
186
+ if (isI18nValue(value)) {
187
+ return canonicalizeI18nValue(value);
188
+ }
189
+ if (key === 'filter' && isEmptyFilter(value)) {
190
+ return {};
191
+ }
192
+ if (Array.isArray(value)) {
193
+ return value.map((item) => canonicalizeAggregateValue(item));
194
+ }
195
+ if (!value || typeof value !== 'object') {
196
+ if (key === 'precision' && value !== undefined && value !== null && value !== '') {
197
+ return String(value);
198
+ }
199
+ return value;
200
+ }
201
+
202
+ const result = {};
203
+ for (const childKey of Object.keys(value)) {
204
+ const childValue = value[childKey];
205
+ if ((childKey === 'filterState' && (childValue === null || childValue === undefined)) ||
206
+ (childKey === 'parentId' && (childValue === '' || childValue === null || childValue === undefined))) {
207
+ continue;
208
+ }
209
+ result[childKey] = canonicalizeAggregateValue(childValue, childKey);
210
+ }
211
+ return result;
212
+ }
213
+
214
+ function assertAggregateDesignReadback(expected, actual) {
215
+ const expectedProjection = canonicalizeAggregateValue(projectAggregateDesignConfig(expected));
216
+ const actualProjection = canonicalizeAggregateValue(projectAggregateDesignConfig(actual));
217
+ if (isDeepStrictEqual(expectedProjection, actualProjection)) {
218
+ return actualProjection;
219
+ }
220
+
221
+ const error = new Error('AGGREGATE_DESIGN_READBACK_MISMATCH');
222
+ error.name = 'AggregateDesignReadbackError';
223
+ error.code = 'AGGREGATE_DESIGN_READBACK_MISMATCH';
224
+ throw error;
225
+ }
226
+
227
+ module.exports = {
228
+ DEFAULT_LIMITS,
229
+ DESIGN_KEYS,
230
+ assertAggregateDesignConfig,
231
+ assertAggregateDesignReadback,
232
+ projectAggregateDesignConfig,
233
+ validateAggregateDesignConfig,
234
+ };
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  const Babel = require('@babel/standalone');
32
+ const globals = require('globals');
32
33
  const {
33
34
  assertNoEmojiInArtifactName,
34
35
  assertNoEmojiInText,
@@ -46,6 +47,7 @@ const {
46
47
  findFormDetailLinkIssues,
47
48
  formatFormDetailLinkMessage,
48
49
  } = require('./form-detail-link-guard');
50
+ const { t } = require('../core/i18n');
49
51
 
50
52
  /**
51
53
  * 依赖 → window 别名白名单,用于把 import 改写到运行时全局对象。
@@ -132,6 +134,15 @@ const BARE_DEPENDENCY_GLOBALS = Object.freeze({
132
134
  },
133
135
  });
134
136
 
137
+ // 以标准 ECMAScript + Browser 环境定义为完整白名单;Canvas 装配器额外
138
+ // 注入 iframeWindow / parentWindow。Node 专属全局不属于页面运行时,不能放行。
139
+ const CANVAS_RUNTIME_GLOBAL_IDENTIFIERS = new Set([
140
+ ...Object.keys(globals.builtin),
141
+ ...Object.keys(globals.browser),
142
+ 'iframeWindow',
143
+ 'parentWindow',
144
+ ]);
145
+
135
146
  const LUCIDE_REACT_PACKAGE = 'lucide-react';
136
147
  const LUCIDE_DYNAMIC_ICON_ALIAS = 'DynamicIcon';
137
148
  const LUCIDE_MODULE_ALIAS = 'LucideReact';
@@ -267,6 +278,159 @@ function assertNoBareDependencyGlobals(source, options = {}) {
267
278
  });
268
279
  }
269
280
 
281
+ function findUnboundIdentifierIssues(source, options = {}) {
282
+ const issuesByName = new Map();
283
+ const allowedIdentifiers = new Set(CANVAS_RUNTIME_GLOBAL_IDENTIFIERS);
284
+ (options.implicitIdentifiers || []).forEach((name) => allowedIdentifiers.add(name));
285
+
286
+ const guardPlugin = () => ({
287
+ name: 'yida-canvas-unbound-identifier-guard',
288
+ visitor: {
289
+ ReferencedIdentifier(path) {
290
+ const name = path.node.name;
291
+ if (allowedIdentifiers.has(name) || path.scope.hasBinding(name) || issuesByName.has(name)) {
292
+ return;
293
+ }
294
+ issuesByName.set(name, {
295
+ name,
296
+ line: path.node.loc && path.node.loc.start ? path.node.loc.start.line : 0,
297
+ column: path.node.loc && path.node.loc.start ? path.node.loc.start.column : 0,
298
+ });
299
+ },
300
+ },
301
+ });
302
+
303
+ Babel.transform(source, {
304
+ filename: 'canvas.tsx',
305
+ presets: [
306
+ ['typescript', { allExtensions: true, isTSX: true, allowDeclareFields: true }],
307
+ ['react', { runtime: 'classic' }],
308
+ ],
309
+ plugins: [guardPlugin],
310
+ sourceType: 'module',
311
+ compact: false,
312
+ babelrc: false,
313
+ configFile: false,
314
+ });
315
+
316
+ return Array.from(issuesByName.values());
317
+ }
318
+
319
+ function formatUnboundIdentifierMessage(issues) {
320
+ const summary = issues
321
+ .map(issue => `${issue.name} (${issue.line}:${issue.column + 1})`)
322
+ .join(', ');
323
+ return t('publish.canvas_unbound_identifiers', summary);
324
+ }
325
+
326
+ function assertNoUnboundIdentifiers(source, options = {}) {
327
+ const issues = findUnboundIdentifierIssues(source, options);
328
+ if (issues.length === 0) {
329
+ return;
330
+ }
331
+ throw new CliError(formatUnboundIdentifierMessage(issues), {
332
+ code: 'OPENYIDA_CANVAS_UNBOUND_IDENTIFIER',
333
+ details: {
334
+ stage: 'canvas_compile',
335
+ sourcePath: options.sourcePath || '',
336
+ issues,
337
+ },
338
+ });
339
+ }
340
+
341
+ function findUnavailableCanvasInstanceApiIssues(source) {
342
+ const issues = [];
343
+ const seen = new Set();
344
+
345
+ function addIssue(path, api) {
346
+ if (seen.has(api)) {
347
+ return;
348
+ }
349
+ seen.add(api);
350
+ issues.push({
351
+ api,
352
+ line: path.node.loc && path.node.loc.start ? path.node.loc.start.line : 0,
353
+ column: path.node.loc && path.node.loc.start ? path.node.loc.start.column : 0,
354
+ });
355
+ }
356
+
357
+ const guardPlugin = ({ types: types }) => ({
358
+ name: 'yida-canvas-instance-api-guard',
359
+ visitor: {
360
+ MemberExpression(path) {
361
+ const object = path.node.object;
362
+ const property = path.node.property;
363
+ const propertyName = path.node.computed && types.isStringLiteral(property)
364
+ ? property.value
365
+ : (types.isIdentifier(property) ? property.name : '');
366
+
367
+ if (types.isMemberExpression(object)) {
368
+ const owner = object.object;
369
+ const ownerProperty = object.property;
370
+ const ownerPropertyName = object.computed && types.isStringLiteral(ownerProperty)
371
+ ? ownerProperty.value
372
+ : (types.isIdentifier(ownerProperty) ? ownerProperty.name : '');
373
+ if (
374
+ types.isIdentifier(owner, { name: 'props' }) &&
375
+ ownerPropertyName === 'utils' &&
376
+ propertyName === 'getDataList'
377
+ ) {
378
+ addIssue(path, 'props.utils.getDataList');
379
+ return;
380
+ }
381
+ if (
382
+ types.isThisExpression(owner) &&
383
+ ownerPropertyName === 'utils' &&
384
+ propertyName === 'yida'
385
+ ) {
386
+ addIssue(path, 'this.utils.yida');
387
+ return;
388
+ }
389
+ }
390
+ if (!types.isThisExpression(object)) {
391
+ return;
392
+ }
393
+ if (propertyName === 'dataSourceMap') {
394
+ addIssue(path, 'this.dataSourceMap');
395
+ } else if (propertyName === '$') {
396
+ addIssue(path, 'this.$');
397
+ }
398
+ },
399
+ },
400
+ });
401
+
402
+ Babel.transform(source, {
403
+ filename: 'canvas.tsx',
404
+ presets: [
405
+ ['typescript', { allExtensions: true, isTSX: true, allowDeclareFields: true }],
406
+ ['react', { runtime: 'classic' }],
407
+ ],
408
+ plugins: [guardPlugin],
409
+ sourceType: 'module',
410
+ compact: false,
411
+ babelrc: false,
412
+ configFile: false,
413
+ });
414
+
415
+ return issues;
416
+ }
417
+
418
+ function assertNoUnavailableCanvasInstanceApis(source, options = {}) {
419
+ const issues = findUnavailableCanvasInstanceApiIssues(source);
420
+ if (issues.length === 0) {
421
+ return;
422
+ }
423
+ const summary = issues.map(issue => issue.api).join(', ');
424
+ throw new CliError(t('publish.canvas_instance_api_unavailable', summary), {
425
+ code: 'OPENYIDA_CANVAS_INSTANCE_API_UNAVAILABLE',
426
+ details: {
427
+ stage: 'canvas_compile',
428
+ sourcePath: options.sourcePath || '',
429
+ issues,
430
+ },
431
+ });
432
+ }
433
+
270
434
  /**
271
435
  * 从源码里正则抽取裸包名(过滤相对/绝对路径),去重排序。
272
436
  * 对齐画布运行时的依赖抽取行为。
@@ -617,6 +781,7 @@ function compileCanvasLocal(source, options = {}) {
617
781
  },
618
782
  });
619
783
  }
784
+ assertNoUnavailableCanvasInstanceApis(source, options);
620
785
  assertNoBareDependencyGlobals(source, options);
621
786
  assertNoManualDependencyGlobals(source, options);
622
787
 
@@ -643,6 +808,10 @@ function compileCanvasLocal(source, options = {}) {
643
808
  // 'react' 进入依赖清单(运行时据此注入 React UMD)。
644
809
  const usesJsxRuntime = /\bReact\.createElement\b|\bReact\.Fragment\b/.test(intermediate);
645
810
  const hasReactBinding = /\b(var|let|const)\s+React\b/.test(intermediate) || /\bimport\s+React\b/.test(intermediate);
811
+ assertNoUnboundIdentifiers(source, {
812
+ sourcePath: options.sourcePath,
813
+ implicitIdentifiers: usesJsxRuntime ? ['React'] : [],
814
+ });
646
815
  if (usesJsxRuntime && !hasReactBinding) {
647
816
  intermediate = "import React from 'react';\n" + intermediate;
648
817
  if (!importedModules.includes('react')) {
@@ -10,14 +10,23 @@
10
10
  * @param {object} [options] 可选项
11
11
  * @param {string} [options.prefix] 额外的路径前缀(会以 / 拼接)
12
12
  * @param {string} [options.namespace] 命名空间,默认 dingtalk
13
+ * @param {'formdesign'|'formnav'} [options.queryModule] 查询模块,默认 formdesign
13
14
  * @param {boolean} [options.addTimestamp] 是否追加 _stamp 时间戳查询参数
14
15
  * @returns {string} 拼接完成的 API 请求路径
15
16
  */
16
17
  function buildApiPath(appType, apiName, options = {}) {
17
- const { prefix = '', namespace = 'dingtalk', addTimestamp = false } = options;
18
+ const {
19
+ prefix = '',
20
+ namespace = 'dingtalk',
21
+ queryModule = 'formdesign',
22
+ addTimestamp = false,
23
+ } = options;
24
+ if (queryModule !== 'formdesign' && queryModule !== 'formnav') {
25
+ throw new Error(`Unsupported query module: ${queryModule}`);
26
+ }
18
27
  const prefixPath = prefix ? `/${prefix}` : '';
19
28
  const timestamp = addTimestamp ? `?_stamp=${Date.now()}` : '';
20
- return `/${namespace}/web/${appType}${prefixPath}/query/formdesign/${apiName}.json${timestamp}`;
29
+ return `/${namespace}/web/${appType}${prefixPath}/query/${queryModule}/${apiName}.json${timestamp}`;
21
30
  }
22
31
 
23
32
  module.exports = {