openyida 2026.8.25 → 2026.8.26

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 (44) hide show
  1. package/README.md +6 -2
  2. package/bin/yida.js +3 -0
  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 -1
  18. package/lib/core/locales/en.js +23 -24
  19. package/lib/core/locales/zh.js +23 -22
  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/contract.js +158 -0
  27. package/lib/report/http.js +28 -5
  28. package/lib/report/index.js +23 -1
  29. package/package.json +2 -1
  30. package/scripts/i18n-baseline.json +45 -23
  31. package/scripts/validate-i18n.js +45 -15
  32. package/yida-skills/references/yida-api.md +0 -29
  33. package/yida-skills/skills/yida-app/references/common-issues.md +2 -0
  34. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +1 -0
  35. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +10 -8
  36. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +8 -6
  37. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +14 -0
  38. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +2 -0
  39. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +7 -5
  40. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +1 -0
  41. package/yida-skills/skills/yida-connector/SKILL.md +6 -6
  42. package/yida-skills/skills/yida-form-detail/references/injection-guide.md +0 -22
  43. package/yida-skills/skills/yida-integration/SKILL.md +23 -5
  44. package/yida-skills/skills/yida-publish-page/SKILL.md +3 -3
package/README.md CHANGED
@@ -349,6 +349,9 @@ openyida connector list
349
349
  openyida integration create APP_XXX FORM_XXX "Sync customer data"
350
350
  openyida integration create APP_XXX FORM_XXX "Approval result notify" \
351
351
  --events processFinish --approval-actions agree,disagree --receivers 123456
352
+ # Capability probe only; currently fails closed before auth/spec reads/remote writes
353
+ openyida integration update APP_XXX FORM_XXX LPROC_XXX \
354
+ --spec .cache/openyida/integration/desired-spec.json
352
355
  openyida create-report APP_XXX "Sales Dashboard" .cache/openyida/reports/charts.json
353
356
  openyida append-chart APP_XXX REPORT_XXX .cache/openyida/reports/chart.json
354
357
  ```
@@ -461,7 +464,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
461
464
  | `openyida connector list` | List HTTP connectors |
462
465
  | `openyida connector create "name" "domain" ...` | Create a connector |
463
466
  | `openyida connector detail <id>` | View connector details |
464
- | `openyida connector delete <id>` | Delete a connector |
467
+ | `openyida connector delete <id> [--force]` | Show manual deletion guidance (CLI does not delete) |
465
468
  | `openyida connector add-action --operations <file> --connector-id <id>` | Add an action |
466
469
  | `openyida connector list-actions <id>` | List actions |
467
470
  | `openyida connector delete-action <id> <operation-id>` | Delete an action |
@@ -477,6 +480,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
477
480
  | Command | Description |
478
481
  |---------|-------------|
479
482
  | `openyida integration create <appType> ... [--spec file.json]` | Create integration automation flow |
483
+ | `openyida integration update <appType> <formUuid> <processCode> --spec <desired-spec.json> [--publish]` | Probe integration update capability (currently blocked without full readback) |
480
484
  | `openyida integration list <appType> [--form-uuid <uuid>] [--status y\|n] [--json]` | List integration automation flows |
481
485
  | `openyida integration enable <appType> <formUuid> <processCode>` | Enable integration automation flow |
482
486
  | `openyida integration disable <appType> <formUuid> <processCode>` | Disable integration automation flow |
@@ -537,7 +541,7 @@ Form field definitions can include `alias` or `componentAlias` to populate Yida
537
541
 
538
542
  #### Workflow, Reports, and Integrations
539
543
 
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,...>`.
544
+ `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
545
 
542
546
  ## Agent Skills
543
547
 
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);
@@ -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 = {