openyida 2026.5.21 → 2026.5.25-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -340,6 +340,9 @@ For overseas apps, pass `--locale en_US` or `--locale ja_JP` on creation command
340
340
  | `openyida verify-short-url <appType> <formUuid> <url>` | Verify a short URL |
341
341
  | `openyida save-share-config <appType> <formUuid> <url> <isOpen> [openAuth]` | Save public access or sharing configuration |
342
342
  | `openyida get-page-config <appType> <formUuid>` | Query public access or sharing configuration |
343
+ | `openyida externalize-form <appType> <formUuid> [--schema-file file]` | Assess public-access blockers and generate external-safe mirror fields |
344
+
345
+ `openyida externalize-form` is useful when a form contains fields such as `AssociationFormField`, `EmployeeField`, or `DepartmentSelectField` that depend on internal organization permissions. It produces a report plus optional `--mirror-fields-output` JSON that can be used with `openyida create-form create` to build a separate public intake form while keeping the internal form and its association fields private.
343
346
 
344
347
  ### Workflow, Reports, and Integrations
345
348
 
package/bin/yida.js CHANGED
@@ -426,7 +426,7 @@ async function main() {
426
426
 
427
427
  case 'copy': {
428
428
  const { run } = require('../lib/core/copy');
429
- run();
429
+ run(args);
430
430
  break;
431
431
  }
432
432
 
@@ -717,6 +717,12 @@ async function main() {
717
717
  break;
718
718
  }
719
719
 
720
+ case 'externalize-form': {
721
+ const { run } = require('../lib/app/externalize-form');
722
+ await run(args);
723
+ break;
724
+ }
725
+
720
726
  case 'update-form-config': {
721
727
  if (args.length < 4) {
722
728
  warn(t('cli.form_config_usage'));
@@ -19,7 +19,6 @@ const {
19
19
  httpGet,
20
20
  requestWithAutoLogin,
21
21
  } = require('../core/utils');
22
- const { t } = require('../core/i18n');
23
22
 
24
23
  const API_PATH = '/query/app/getAppList.json';
25
24
 
@@ -90,11 +89,31 @@ function formatApp(app) {
90
89
  };
91
90
  }
92
91
 
92
+ function hasHelpFlag(args) {
93
+ return (args || []).includes('--help') || (args || []).includes('-h');
94
+ }
95
+
96
+ function printUsage() {
97
+ process.stderr.write([
98
+ 'Usage: openyida app-list [--size N]',
99
+ '',
100
+ 'Options:',
101
+ ' --size N Page size used when fetching apps, default: 20',
102
+ ' --help, -h Show this help',
103
+ '',
104
+ ].join('\n'));
105
+ }
106
+
93
107
  /**
94
108
  * app-list 命令主入口
95
109
  * @param {string[]} args
96
110
  */
97
111
  async function run(args) {
112
+ if (hasHelpFlag(args)) {
113
+ printUsage();
114
+ return;
115
+ }
116
+
98
117
  const pageSizeIndex = args.indexOf('--size');
99
118
  const pageSize = pageSizeIndex !== -1 && args[pageSizeIndex + 1]
100
119
  ? parseInt(args[pageSizeIndex + 1], 10)
@@ -0,0 +1,642 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ loadCookieData,
7
+ triggerLogin,
8
+ resolveBaseUrl,
9
+ httpGet,
10
+ requestWithAutoLogin,
11
+ } = require('../core/utils');
12
+
13
+ const FIELD_COMPONENT_NAMES = new Set([
14
+ 'TextField',
15
+ 'TextareaField',
16
+ 'SelectField',
17
+ 'DateField',
18
+ 'NumberField',
19
+ 'RadioField',
20
+ 'CheckboxField',
21
+ 'EmployeeField',
22
+ 'PhoneField',
23
+ 'EmailField',
24
+ 'CascadeSelectField',
25
+ 'ImageField',
26
+ 'AttachmentField',
27
+ 'TableField',
28
+ 'MultiSelectField',
29
+ 'DepartmentSelectField',
30
+ 'AssociationFormField',
31
+ 'CountrySelectField',
32
+ 'CitySelectField',
33
+ 'RateField',
34
+ 'SignatureField',
35
+ 'SerialNumberField',
36
+ 'AddressField',
37
+ ]);
38
+
39
+ const PUBLIC_BLOCKED_TYPES = new Set([
40
+ 'AssociationFormField',
41
+ 'EmployeeField',
42
+ 'DepartmentSelectField',
43
+ ]);
44
+
45
+ const REVIEW_TYPES = new Set([
46
+ 'AttachmentField',
47
+ 'ImageField',
48
+ 'SignatureField',
49
+ 'TableField',
50
+ 'CascadeSelectField',
51
+ 'CountrySelectField',
52
+ 'CitySelectField',
53
+ 'AddressField',
54
+ ]);
55
+
56
+ const SAFE_CREATE_FORM_TYPES = new Set([
57
+ 'TextField',
58
+ 'TextareaField',
59
+ 'SelectField',
60
+ 'DateField',
61
+ 'NumberField',
62
+ 'RadioField',
63
+ 'CheckboxField',
64
+ 'PhoneField',
65
+ 'EmailField',
66
+ 'MultiSelectField',
67
+ 'RateField',
68
+ 'AttachmentField',
69
+ 'ImageField',
70
+ ]);
71
+
72
+ function readOption(args, names, fallback = '') {
73
+ const optionNames = Array.isArray(names) ? names : [names];
74
+ for (const name of optionNames) {
75
+ const index = args.indexOf(name);
76
+ if (index !== -1 && args[index + 1] && !args[index + 1].startsWith('--')) {
77
+ return args[index + 1];
78
+ }
79
+ }
80
+ return fallback;
81
+ }
82
+
83
+ function parseArgs(args) {
84
+ const parsed = {
85
+ appType: '',
86
+ formUuid: '',
87
+ schemaFile: '',
88
+ output: '',
89
+ mirrorFieldsOutput: '',
90
+ format: 'json',
91
+ target: 'open',
92
+ mirrorTitle: '',
93
+ help: false,
94
+ };
95
+ const positional = [];
96
+
97
+ for (let index = 0; index < args.length; index++) {
98
+ const arg = args[index];
99
+ if (arg === '--help' || arg === '-h') {
100
+ parsed.help = true;
101
+ } else if (arg === '--schema-file' || arg === '--schema') {
102
+ parsed.schemaFile = readOption(args, arg);
103
+ index++;
104
+ } else if (arg === '--output' || arg === '-o') {
105
+ parsed.output = readOption(args, arg);
106
+ index++;
107
+ } else if (arg === '--mirror-fields-output' || arg === '--fields-output') {
108
+ parsed.mirrorFieldsOutput = readOption(args, arg);
109
+ index++;
110
+ } else if (arg === '--format') {
111
+ parsed.format = readOption(args, arg, 'json');
112
+ index++;
113
+ } else if (arg === '--target') {
114
+ parsed.target = readOption(args, arg, 'open');
115
+ index++;
116
+ } else if (arg === '--mirror-title') {
117
+ parsed.mirrorTitle = readOption(args, arg);
118
+ index++;
119
+ } else if (arg.startsWith('--')) {
120
+ throw new Error(`Unknown option: ${arg}`);
121
+ } else {
122
+ positional.push(arg);
123
+ }
124
+ }
125
+
126
+ parsed.appType = positional[0] || '';
127
+ parsed.formUuid = positional[1] || '';
128
+ if (!['json', 'markdown'].includes(parsed.format)) {
129
+ throw new Error(`Unsupported format: ${parsed.format}`);
130
+ }
131
+ if (!['open', 'share'].includes(parsed.target)) {
132
+ throw new Error(`Unsupported target: ${parsed.target}`);
133
+ }
134
+ return parsed;
135
+ }
136
+
137
+ function printUsage() {
138
+ const lines = [
139
+ 'Usage: openyida externalize-form <appType> <formUuid> [options]',
140
+ '',
141
+ 'Options:',
142
+ ' --schema-file <file> Read a saved get-schema JSON file instead of fetching remotely',
143
+ ' --output, -o <file> Write the report to a file',
144
+ ' --mirror-fields-output <file> Write external-safe create-form fields JSON',
145
+ ' --format json|markdown Output format, default: json',
146
+ ' --target open|share Access target, default: open',
147
+ ' --mirror-title <title> Title used in suggested create-form command',
148
+ '',
149
+ 'Examples:',
150
+ ' openyida externalize-form APP_XXX FORM-XXX --schema-file .cache/schema.json',
151
+ ' openyida externalize-form APP_XXX FORM-XXX --mirror-fields-output .cache/external-fields.json',
152
+ ];
153
+ process.stderr.write(`${lines.join('\n')}\n`);
154
+ }
155
+
156
+ function extractText(value) {
157
+ if (value === null || value === undefined) {
158
+ return '';
159
+ }
160
+ if (typeof value === 'string') {
161
+ return value;
162
+ }
163
+ if (typeof value === 'number' || typeof value === 'boolean') {
164
+ return String(value);
165
+ }
166
+ if (Array.isArray(value)) {
167
+ return value.map(extractText).filter(Boolean).join(', ');
168
+ }
169
+ if (typeof value === 'object') {
170
+ return extractText(
171
+ value.zh_CN ||
172
+ value.en_US ||
173
+ value.ja_JP ||
174
+ value.value ||
175
+ value.text ||
176
+ value.label ||
177
+ value.name ||
178
+ value.title ||
179
+ ''
180
+ );
181
+ }
182
+ return '';
183
+ }
184
+
185
+ function isRequired(props) {
186
+ if (!props) {
187
+ return false;
188
+ }
189
+ if (props.required === true) {
190
+ return true;
191
+ }
192
+ const validation = Array.isArray(props.validation) ? props.validation : [];
193
+ return validation.some(rule => rule && rule.type === 'required');
194
+ }
195
+
196
+ function getPages(schemaResult) {
197
+ if (schemaResult && schemaResult.content && Array.isArray(schemaResult.content.pages)) {
198
+ return schemaResult.content.pages;
199
+ }
200
+ if (schemaResult && Array.isArray(schemaResult.pages)) {
201
+ return schemaResult.pages;
202
+ }
203
+ return [];
204
+ }
205
+
206
+ function collectFieldNodes(schemaResult) {
207
+ const fields = [];
208
+ const pages = getPages(schemaResult);
209
+
210
+ function traverse(node, parents) {
211
+ if (!node) {
212
+ return;
213
+ }
214
+ const props = node.props || {};
215
+ const label = extractText(props.label);
216
+ const nextParents = FIELD_COMPONENT_NAMES.has(node.componentName) && label
217
+ ? parents.concat(label)
218
+ : parents;
219
+
220
+ if (FIELD_COMPONENT_NAMES.has(node.componentName)) {
221
+ fields.push({
222
+ componentName: node.componentName,
223
+ props,
224
+ label,
225
+ fieldId: props.fieldId || '',
226
+ required: isRequired(props),
227
+ behavior: props.behavior || '',
228
+ path: nextParents.join(' > '),
229
+ });
230
+ }
231
+
232
+ const children = Array.isArray(node.children) ? node.children : [];
233
+ children.forEach(child => traverse(child, nextParents));
234
+ }
235
+
236
+ pages.forEach((page) => {
237
+ const roots = page && page.componentsTree ? page.componentsTree : [];
238
+ roots.forEach(root => traverse(root, []));
239
+ });
240
+
241
+ return fields;
242
+ }
243
+
244
+ function getRisk(field, target) {
245
+ if (target === 'share') {
246
+ if (field.componentName === 'AssociationFormField') {
247
+ return {
248
+ level: 'review',
249
+ reason: 'Association fields require internal form data permission and should be verified for org-share scenarios.',
250
+ };
251
+ }
252
+ return { level: 'safe', reason: 'Internal share keeps the visitor inside the organization permission boundary.' };
253
+ }
254
+
255
+ if (PUBLIC_BLOCKED_TYPES.has(field.componentName)) {
256
+ return {
257
+ level: 'blocked',
258
+ reason: `${field.componentName} depends on authenticated organization data and is not reliable for anonymous public access.`,
259
+ };
260
+ }
261
+ if (REVIEW_TYPES.has(field.componentName)) {
262
+ return {
263
+ level: 'review',
264
+ reason: `${field.componentName} may need a browser verification or a simpler external intake field.`,
265
+ };
266
+ }
267
+ return { level: 'safe', reason: 'Primitive input field, suitable for external intake.' };
268
+ }
269
+
270
+ function associationTarget(props) {
271
+ const associationForm = props && props.associationForm ? props.associationForm : {};
272
+ return {
273
+ appType: associationForm.appType || '',
274
+ formUuid: associationForm.formUuid || '',
275
+ formTitle: associationForm.formTitle || '',
276
+ mainFieldId: associationForm.mainFieldId || '',
277
+ mainFieldLabel: extractText(associationForm.mainFieldLabel),
278
+ };
279
+ }
280
+
281
+ function planField(field, target) {
282
+ const risk = getRisk(field, target);
283
+ const plan = {
284
+ label: field.label || field.fieldId || field.componentName,
285
+ fieldId: field.fieldId,
286
+ componentName: field.componentName,
287
+ required: field.required,
288
+ behavior: field.behavior,
289
+ path: field.path,
290
+ riskLevel: risk.level,
291
+ reason: risk.reason,
292
+ externalStrategy: 'keep',
293
+ };
294
+
295
+ if (field.componentName === 'AssociationFormField') {
296
+ plan.associationTarget = associationTarget(field.props);
297
+ plan.externalStrategy = 'snapshot-and-resolve-internal';
298
+ } else if (field.componentName === 'EmployeeField') {
299
+ plan.externalStrategy = 'collect-name-or-contact';
300
+ } else if (field.componentName === 'DepartmentSelectField') {
301
+ plan.externalStrategy = 'collect-department-text';
302
+ } else if (risk.level === 'review') {
303
+ plan.externalStrategy = 'verify-or-simplify';
304
+ }
305
+ return plan;
306
+ }
307
+
308
+ function mirrorLabel(label, suffix) {
309
+ if (!label) {
310
+ return suffix;
311
+ }
312
+ return `${label}${suffix}`;
313
+ }
314
+
315
+ function associationSnapshotLabel(plan) {
316
+ const target = plan.associationTarget || {};
317
+ const targetMainLabel = target.mainFieldLabel || '';
318
+ if (targetMainLabel) {
319
+ return targetMainLabel;
320
+ }
321
+ if (plan.label && plan.label.endsWith('名称')) {
322
+ return plan.label;
323
+ }
324
+ return mirrorLabel(plan.label, '名称');
325
+ }
326
+
327
+ function mirrorFieldForPlan(plan) {
328
+ const base = {
329
+ label: plan.label,
330
+ required: plan.required,
331
+ sourceFieldId: plan.fieldId,
332
+ sourceComponentName: plan.componentName,
333
+ sourceStrategy: plan.externalStrategy,
334
+ };
335
+
336
+ if (plan.componentName === 'AssociationFormField') {
337
+ const target = plan.associationTarget || {};
338
+ return [
339
+ {
340
+ type: 'TextField',
341
+ label: associationSnapshotLabel(plan),
342
+ required: plan.required,
343
+ sourceFieldId: plan.fieldId,
344
+ sourceComponentName: plan.componentName,
345
+ sourceStrategy: 'association-main-field-snapshot',
346
+ targetFormUuid: target.formUuid,
347
+ targetMainFieldId: target.mainFieldId,
348
+ },
349
+ {
350
+ type: 'TextField',
351
+ label: mirrorLabel(plan.label, '业务标识'),
352
+ required: false,
353
+ sourceFieldId: plan.fieldId,
354
+ sourceComponentName: plan.componentName,
355
+ sourceStrategy: 'association-business-key-for-internal-resolution',
356
+ targetFormUuid: target.formUuid,
357
+ },
358
+ ];
359
+ }
360
+
361
+ if (plan.componentName === 'EmployeeField') {
362
+ return [
363
+ { ...base, type: 'TextField', label: mirrorLabel(plan.label, '姓名') },
364
+ { ...base, type: 'TextField', label: mirrorLabel(plan.label, '联系方式'), required: false },
365
+ ];
366
+ }
367
+
368
+ if (plan.componentName === 'DepartmentSelectField') {
369
+ return [{ ...base, type: 'TextField', label: mirrorLabel(plan.label, '名称') }];
370
+ }
371
+
372
+ if (SAFE_CREATE_FORM_TYPES.has(plan.componentName)) {
373
+ return [{ ...base, type: plan.componentName }];
374
+ }
375
+
376
+ if (plan.componentName === 'SerialNumberField') {
377
+ return [];
378
+ }
379
+
380
+ return [{ ...base, type: 'TextareaField', label: mirrorLabel(plan.label, '说明'), required: false }];
381
+ }
382
+
383
+ function buildMirrorFields(fields) {
384
+ const mirrorFields = [];
385
+ fields.forEach((field) => {
386
+ mirrorFields.push(...mirrorFieldForPlan(field));
387
+ });
388
+ return mirrorFields;
389
+ }
390
+
391
+ function summarize(fields) {
392
+ const summary = {
393
+ totalFields: fields.length,
394
+ safeFields: 0,
395
+ reviewFields: 0,
396
+ blockedFields: 0,
397
+ associationFields: 0,
398
+ authBoundFields: 0,
399
+ };
400
+
401
+ fields.forEach((field) => {
402
+ if (field.riskLevel === 'safe') {
403
+ summary.safeFields++;
404
+ } else if (field.riskLevel === 'review') {
405
+ summary.reviewFields++;
406
+ } else if (field.riskLevel === 'blocked') {
407
+ summary.blockedFields++;
408
+ }
409
+ if (field.componentName === 'AssociationFormField') {
410
+ summary.associationFields++;
411
+ }
412
+ if (PUBLIC_BLOCKED_TYPES.has(field.componentName)) {
413
+ summary.authBoundFields++;
414
+ }
415
+ });
416
+ return summary;
417
+ }
418
+
419
+ function buildRecommendedActions(parsed, report) {
420
+ const actions = [
421
+ 'Create a separate external intake form from mirrorFields instead of exposing the internal form directly.',
422
+ 'Keep association fields in the internal form and resolve them after submission by business key or manual review.',
423
+ 'Copy human-readable snapshots into the external form so public visitors never need target-form permissions.',
424
+ ];
425
+
426
+ if (parsed.mirrorFieldsOutput) {
427
+ const title = parsed.mirrorTitle || `${report.formTitle || 'External Intake'} External`;
428
+ actions.unshift(
429
+ `Create mirror form: openyida create-form create ${parsed.appType} "${title}" ${parsed.mirrorFieldsOutput}`
430
+ );
431
+ }
432
+
433
+ if (report.summary.blockedFields === 0 && report.summary.reviewFields === 0) {
434
+ actions.unshift('No blocked fields were found; public access can still be verified with save-share-config and Chrome.');
435
+ }
436
+ return actions;
437
+ }
438
+
439
+ function inferFormTitle(schemaResult, formUuid) {
440
+ const content = schemaResult && schemaResult.content ? schemaResult.content : schemaResult;
441
+ return extractText(
442
+ content && (
443
+ content.formName ||
444
+ content.formTitle ||
445
+ content.title ||
446
+ content.name
447
+ )
448
+ ) || formUuid;
449
+ }
450
+
451
+ function analyzeSchema(schemaResult, options = {}) {
452
+ const target = options.target || 'open';
453
+ const fields = collectFieldNodes(schemaResult).map(field => planField(field, target));
454
+ const report = {
455
+ success: true,
456
+ appType: options.appType || '',
457
+ formUuid: options.formUuid || '',
458
+ formTitle: inferFormTitle(schemaResult, options.formUuid || ''),
459
+ target,
460
+ source: options.source || 'schema',
461
+ summary: summarize(fields),
462
+ fields,
463
+ mirrorFields: buildMirrorFields(fields),
464
+ recommendedActions: [],
465
+ notes: [
466
+ 'OpenYida cannot relax Yida platform permission boundaries for anonymous visitors.',
467
+ 'This plan keeps internal association fields private and generates external-safe snapshot fields.',
468
+ 'For fully automated sync, add an internal automation or API worker that resolves submitted business keys.',
469
+ ],
470
+ };
471
+ report.recommendedActions = buildRecommendedActions(options, report);
472
+ return report;
473
+ }
474
+
475
+ function renderMarkdown(report) {
476
+ const lines = [
477
+ `# External Access Plan: ${report.formTitle || report.formUuid}`,
478
+ '',
479
+ `- App: ${report.appType || '-'}`,
480
+ `- Form: ${report.formUuid || '-'}`,
481
+ `- Target: ${report.target}`,
482
+ `- Total fields: ${report.summary.totalFields}`,
483
+ `- Blocked fields: ${report.summary.blockedFields}`,
484
+ `- Review fields: ${report.summary.reviewFields}`,
485
+ '',
486
+ '## Fields',
487
+ '',
488
+ '| Label | Type | Risk | Strategy | Reason |',
489
+ '| --- | --- | --- | --- | --- |',
490
+ ];
491
+
492
+ report.fields.forEach((field) => {
493
+ lines.push(
494
+ `| ${field.label || '-'} | ${field.componentName} | ${field.riskLevel} | ${field.externalStrategy} | ${field.reason} |`
495
+ );
496
+ });
497
+
498
+ lines.push('', '## Recommended Actions', '');
499
+ report.recommendedActions.forEach(action => lines.push(`- ${action}`));
500
+ lines.push('', '## Mirror Fields', '', '```json');
501
+ lines.push(JSON.stringify(report.mirrorFields, null, 2));
502
+ lines.push('```', '');
503
+ return lines.join('\n');
504
+ }
505
+
506
+ function writeJson(filePath, data) {
507
+ const resolved = path.resolve(filePath);
508
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
509
+ fs.writeFileSync(resolved, JSON.stringify(data, null, 2), 'utf-8');
510
+ return resolved;
511
+ }
512
+
513
+ function writeText(filePath, text) {
514
+ const resolved = path.resolve(filePath);
515
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
516
+ fs.writeFileSync(resolved, text, 'utf-8');
517
+ return resolved;
518
+ }
519
+
520
+ function resolveOutputPath(filePath) {
521
+ const resolved = path.resolve(filePath);
522
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
523
+ return resolved;
524
+ }
525
+
526
+ function loadSchemaFile(filePath) {
527
+ const raw = fs.readFileSync(path.resolve(filePath), 'utf-8');
528
+ return JSON.parse(raw);
529
+ }
530
+
531
+ function createAuthRef() {
532
+ let cookieData = loadCookieData();
533
+ if (!cookieData) {
534
+ cookieData = triggerLogin();
535
+ }
536
+ return {
537
+ csrfToken: cookieData.csrf_token,
538
+ cookies: cookieData.cookies,
539
+ baseUrl: resolveBaseUrl(cookieData),
540
+ cookieData,
541
+ };
542
+ }
543
+
544
+ async function fetchSchema(appType, formUuid, authRef) {
545
+ return requestWithAutoLogin((auth) => {
546
+ return httpGet(
547
+ auth.baseUrl,
548
+ `/alibaba/web/${appType}/_view/query/formdesign/getFormSchema.json`,
549
+ { formUuid, schemaVersion: 'V5' },
550
+ auth.cookies
551
+ );
552
+ }, authRef);
553
+ }
554
+
555
+ function ensureSuccessfulSchema(result) {
556
+ if (!result || result.success === false || result.__needLogin || result.__csrfExpired) {
557
+ const message = result && (result.errorMsg || result.message)
558
+ ? result.errorMsg || result.message
559
+ : 'Failed to fetch form schema';
560
+ throw new Error(message);
561
+ }
562
+ }
563
+
564
+ async function loadSchema(parsed) {
565
+ if (parsed.schemaFile) {
566
+ return {
567
+ schema: loadSchemaFile(parsed.schemaFile),
568
+ source: path.resolve(parsed.schemaFile),
569
+ };
570
+ }
571
+ const authRef = createAuthRef();
572
+ const schema = await fetchSchema(parsed.appType, parsed.formUuid, authRef);
573
+ ensureSuccessfulSchema(schema);
574
+ return { schema, source: 'remote' };
575
+ }
576
+
577
+ async function run(args) {
578
+ let parsed;
579
+ try {
580
+ parsed = parseArgs(args || []);
581
+ } catch (error) {
582
+ console.error(error.message);
583
+ printUsage();
584
+ process.exit(1);
585
+ }
586
+
587
+ if (parsed.help) {
588
+ printUsage();
589
+ return;
590
+ }
591
+ if (!parsed.appType || !parsed.formUuid) {
592
+ printUsage();
593
+ process.exit(1);
594
+ }
595
+
596
+ try {
597
+ const loaded = await loadSchema(parsed);
598
+ const report = analyzeSchema(loaded.schema, {
599
+ ...parsed,
600
+ source: loaded.source,
601
+ });
602
+ const files = {};
603
+
604
+ if (parsed.mirrorFieldsOutput) {
605
+ files.mirrorFields = writeJson(parsed.mirrorFieldsOutput, report.mirrorFields);
606
+ }
607
+ if (parsed.output) {
608
+ files.report = resolveOutputPath(parsed.output);
609
+ }
610
+ report.files = files;
611
+ if (parsed.output) {
612
+ if (parsed.format === 'markdown') {
613
+ writeText(files.report, renderMarkdown(report));
614
+ } else {
615
+ writeJson(files.report, report);
616
+ }
617
+ }
618
+
619
+ const finalOutput = parsed.format === 'markdown'
620
+ ? renderMarkdown(report)
621
+ : JSON.stringify(report, null, 2);
622
+ console.log(finalOutput);
623
+ } catch (error) {
624
+ console.error(`externalize-form failed: ${error.message}`);
625
+ process.exit(1);
626
+ }
627
+ }
628
+
629
+ module.exports = {
630
+ parseArgs,
631
+ collectFieldNodes,
632
+ analyzeSchema,
633
+ renderMarkdown,
634
+ run,
635
+ _private: {
636
+ extractText,
637
+ isRequired,
638
+ planField,
639
+ buildMirrorFields,
640
+ summarize,
641
+ },
642
+ };
@@ -110,6 +110,9 @@ const COMMAND_GROUPS = [
110
110
  command('verify-short-url', ['verify-short-url'], 'verify-short-url <appType> ...', 'help.cmd_verify_url'),
111
111
  command('save-share-config', ['save-share-config'], 'save-share-config <appType> ...', 'help.cmd_save_share'),
112
112
  command('get-page-config', ['get-page-config'], 'get-page-config <appType> <formUuid>', 'help.cmd_get_page_config'),
113
+ command('externalize-form', ['externalize-form'], 'externalize-form <appType> <formUuid> [--schema-file file]', 'help.cmd_externalize_form', {
114
+ output: 'json|markdown',
115
+ }),
113
116
  ],
114
117
  },
115
118
  {
package/lib/core/copy.js CHANGED
@@ -81,6 +81,30 @@ function mergeCopyDir(sourceDir, destDir) {
81
81
  return copiedCount;
82
82
  }
83
83
 
84
+ function resolveExistingPath(targetPath) {
85
+ try {
86
+ return fs.realpathSync(targetPath);
87
+ } catch {
88
+ return path.resolve(targetPath);
89
+ }
90
+ }
91
+
92
+ function isSameDirectory(a, b) {
93
+ const pathA = resolveExistingPath(a);
94
+ const pathB = resolveExistingPath(b);
95
+ if (process.platform === 'win32') {
96
+ return pathA.toLowerCase() === pathB.toLowerCase();
97
+ }
98
+ return pathA === pathB;
99
+ }
100
+
101
+ function clearDirectoryContents(dir) {
102
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
103
+ for (const entry of entries) {
104
+ fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true });
105
+ }
106
+ }
107
+
84
108
  /**
85
109
  * 强制复制目录:先清空目标目录,再完整复制。
86
110
  * @returns {number} 复制的文件数量
@@ -89,7 +113,11 @@ function forceCopyDir(sourceDir, destDir) {
89
113
  if (!fs.existsSync(sourceDir)) {return 0;}
90
114
 
91
115
  if (fs.existsSync(destDir)) {
92
- fs.rmSync(destDir, { recursive: true, force: true });
116
+ if (isSameDirectory(destDir, process.cwd())) {
117
+ clearDirectoryContents(destDir);
118
+ } else {
119
+ fs.rmSync(destDir, { recursive: true, force: true });
120
+ }
93
121
  console.log(t('copy.cleared', destDir));
94
122
  }
95
123
 
@@ -182,9 +210,11 @@ function createSymlink(sourceDir, destLink) {
182
210
  * @param {string|null} activeToolName
183
211
  * @param {string|null} activeProjectRoot
184
212
  * @param {Array} envResults
213
+ * @param {object} [options]
214
+ * @param {boolean} [options.allowCurrentDir=false] - 未检测到活跃 AI 工具时,是否允许使用当前目录
185
215
  * @returns {string} 目标根目录路径
186
216
  */
187
- function resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults) {
217
+ function resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults, options = {}) {
188
218
  const activeResult = envResults.find((r) => r.displayName === activeToolName);
189
219
  const isWukong = activeResult && activeResult.dirName === '.real';
190
220
 
@@ -201,6 +231,10 @@ function resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults) {
201
231
  return process.cwd();
202
232
  }
203
233
 
234
+ if (options.allowCurrentDir) {
235
+ return process.cwd();
236
+ }
237
+
204
238
  // 未检测到活跃工具
205
239
  warn(t('copy.no_ai_tool'));
206
240
  envResults.forEach((r) => {
@@ -223,13 +257,13 @@ function copyItem(label, sourceDir, destDir, isForce) {
223
257
 
224
258
  /**
225
259
  * 执行 copy 命令主逻辑。
260
+ * @param {string[]} [args=process.argv.slice(3)] 命令参数
226
261
  */
227
- function run() {
262
+ function run(args = process.argv.slice(3)) {
228
263
  const { c, sep, banner, info, success, hint, label, fail: chalkFail, listItem } = require('./chalk');
229
264
 
230
265
  banner(t('copy.title'), { stderr: false });
231
266
 
232
- const args = process.argv.slice(3);
233
267
  const isForce = args.includes('--force');
234
268
  const wantsSkills = args.includes('-skills');
235
269
  const wantsProject = args.includes('-project');
@@ -250,7 +284,9 @@ function run() {
250
284
  const { activeToolName, activeProjectRoot, results: envResults } = detectEnvironment();
251
285
  const activeEnvResult = envResults.find((r) => r.isActive);
252
286
  const isWukong = activeEnvResult && activeEnvResult.dirName === '.real';
253
- const destBase = resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults);
287
+ const destBase = resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults, {
288
+ allowCurrentDir: isForce,
289
+ });
254
290
  label('Target', destBase, { stderr: false });
255
291
  if (isForce) {
256
292
  warn(t('copy.force_mode'), false);
@@ -345,8 +381,8 @@ function run() {
345
381
  }
346
382
 
347
383
  // 4. 打印汇总
348
- const copyCount = results.filter(r => r.type === 'copy').reduce((sum, r) => sum + r.count, 0);
349
- const linkCount = results.filter(r => r.type === 'symlink').length;
384
+ const copyCount = results.filter((r) => r.type === 'copy').reduce((sum, r) => sum + r.count, 0);
385
+ const linkCount = results.filter((r) => r.type === 'symlink').length;
350
386
  console.log('');
351
387
  console.log(` ${sep()}`);
352
388
  success(t('copy.done'), false);
@@ -369,4 +405,10 @@ function run() {
369
405
  console.log(` ${sep()}\n`);
370
406
  }
371
407
 
372
- module.exports = { run };
408
+ module.exports = {
409
+ run,
410
+ _internal: {
411
+ forceCopyDir,
412
+ resolveDestBaseFromEnv,
413
+ },
414
+ };
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'التحقق من الرابط المختصر',
54
54
  cmd_save_share: 'حفظ إعدادات الوصول العام / المشاركة',
55
55
  cmd_get_page_config: 'استعلام إعدادات الوصول العام للصفحة',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'التقارير',
57
58
  cmd_create_report: 'إنشاء تقرير Yida',
58
59
  cmd_append_chart: 'إضافة رسم بياني إلى تقرير موجود',
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'Kurz-URL überprüfen',
54
54
  cmd_save_share: 'Öffentlichen Zugang / Freigabe speichern',
55
55
  cmd_get_page_config: 'Öffentliche Zugangskonfiguration abfragen',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'Berichte',
57
58
  cmd_create_report: 'Yida-Bericht erstellen',
58
59
  cmd_append_chart: 'Diagramm zu bestehendem Bericht hinzufügen',
@@ -56,6 +56,7 @@ module.exports = {
56
56
  cmd_verify_url: 'Verify short URL',
57
57
  cmd_save_share: 'Save public access / share config',
58
58
  cmd_get_page_config: 'Query page public access config',
59
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
59
60
  group_report: 'Reports',
60
61
  cmd_create_report: 'Create a Yida report',
61
62
  cmd_append_chart: 'Append chart to existing report',
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'Verificar URL corta',
54
54
  cmd_save_share: 'Guardar configuración de acceso público / compartir',
55
55
  cmd_get_page_config: 'Consultar configuración de acceso público',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'Informes',
57
58
  cmd_create_report: 'Crear informe Yida',
58
59
  cmd_append_chart: 'Agregar gráfico a informe existente',
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'Vérifier l\'URL courte',
54
54
  cmd_save_share: 'Enregistrer la configuration d\'accès public / partage',
55
55
  cmd_get_page_config: 'Consulter la configuration d\'accès public',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'Rapports',
57
58
  cmd_create_report: 'Créer un rapport Yida',
58
59
  cmd_append_chart: 'Ajouter un graphique à un rapport existant',
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'शॉर्ट URL सत्यापित करें',
54
54
  cmd_save_share: 'सार्वजनिक पहुंच / शेयर कॉन्फ़िगरेशन सहेजें',
55
55
  cmd_get_page_config: 'पेज सार्वजनिक पहुंच कॉन्फ़िगरेशन पूछें',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'रिपोर्ट',
57
58
  cmd_create_report: 'Yida रिपोर्ट बनाएं',
58
59
  cmd_append_chart: 'मौजूदा रिपोर्ट में चार्ट जोड़ें',
@@ -55,6 +55,7 @@ module.exports = {
55
55
  cmd_verify_url: '短縮 URL を検証',
56
56
  cmd_save_share: '公開アクセス / 共有設定を保存',
57
57
  cmd_get_page_config: 'ページ公開アクセス設定を照会',
58
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
58
59
  group_report: 'レポート',
59
60
  cmd_create_report: '宜搭レポートを作成',
60
61
  cmd_append_chart: '既存レポートにチャートを追加',
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: '단축 URL 확인',
54
54
  cmd_save_share: '공개 접근 / 공유 설정 저장',
55
55
  cmd_get_page_config: '페이지 공개 접근 설정 조회',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: '보고서',
57
58
  cmd_create_report: 'Yida 보고서 생성',
58
59
  cmd_append_chart: '기존 보고서에 차트 추가',
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'Verificar URL curta',
54
54
  cmd_save_share: 'Salvar configuração de acesso público / compartilhamento',
55
55
  cmd_get_page_config: 'Consultar configuração de acesso público',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'Relatórios',
57
58
  cmd_create_report: 'Criar relatório Yida',
58
59
  cmd_append_chart: 'Adicionar gráfico a relatório existente',
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'Xác minh URL ngắn',
54
54
  cmd_save_share: 'Lưu cấu hình truy cập công khai / chia sẻ',
55
55
  cmd_get_page_config: 'Truy vấn cấu hình truy cập công khai',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'Báo cáo',
57
58
  cmd_create_report: 'Tạo báo cáo Yida',
58
59
  cmd_append_chart: 'Thêm biểu đồ vào báo cáo hiện có',
@@ -55,6 +55,7 @@ module.exports = {
55
55
  cmd_verify_url: '驗證短連結 URL',
56
56
  cmd_save_share: '儲存公開訪問 / 分享設定',
57
57
  cmd_get_page_config: '查詢頁面公開訪問設定',
58
+ cmd_externalize_form: '生成外部開放安全評估和鏡像欄位方案',
58
59
  group_report: '報表',
59
60
  cmd_create_report: '建立宜搭報表',
60
61
  cmd_append_chart: '向已有報表追加圖表',
@@ -56,6 +56,7 @@ module.exports = {
56
56
  cmd_verify_url: '验证短链接 URL',
57
57
  cmd_save_share: '保存公开访问 / 分享配置',
58
58
  cmd_get_page_config: '查询页面公开访问配置',
59
+ cmd_externalize_form: '生成外部开放安全评估和镜像字段方案',
59
60
  group_report: '报表',
60
61
  cmd_create_report: '创建宜搭报表',
61
62
  cmd_append_chart: '向已有报表追加图表',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.5.21",
3
+ "version": "2026.5.25-beta.0",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",