openyida 2026.5.15 → 2026.5.17

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
@@ -258,12 +258,14 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
258
258
  | `openyida env setup` | Choose a customer-friendly login environment preset: public, overseas, Alibaba intranet, or private deployment |
259
259
  | `openyida env <list\|show\|switch\|add\|remove>` | Manage public/private Yida environment profiles |
260
260
  | `openyida commands [--json]` | Emit the machine-readable command manifest |
261
- | `openyida login [--qr\|--agent-qr\|--codex\|--browser] [--env <name>\|--overseas] [--corp-id <corpId>]` | Log in to Yida |
261
+ | `openyida login [--qr\|--agent-qr\|--codex\|--browser] [--env <name>\|--overseas\|--yidaapps] [--corp-id <corpId>]` | Log in to Yida |
262
262
  | `openyida logout` | Log out or switch account |
263
263
  | `openyida auth <status\|login\|refresh\|logout>` | Manage login status |
264
264
  | `openyida org list` | List accessible organizations |
265
265
  | `openyida org switch --corp-id <corpId>` | Switch organization without logging in again |
266
266
 
267
+ Environment selectors such as `--env intl`, `--overseas`, and `--yidaapps` can be used on login-required commands to choose the target Yida environment for that run.
268
+
267
269
  ### Applications
268
270
 
269
271
  | Command | Description |
@@ -289,7 +291,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
289
291
  | `openyida build-page <sourceFile> [--output file\|--write]` | Build/fix Yida-compatible page source from OpenYida authoring JSX |
290
292
  | `openyida check-page <sourceFile> [--compat] [--json]` | Check page compatibility; `.oyd.jsx` is compatibility-built before linting |
291
293
  | `openyida compile <sourceFile> [--compat]` | Compile a custom page locally; `.oyd.jsx` sources are compatibility-built first |
292
- | `openyida publish <sourceFile> <appType> <formUuid> [--compat] [--health-check] [--open\|--no-open]` | Compile and publish a custom page |
294
+ | `openyida publish <sourceFile> <appType> <formUuid> [--compat] [--health-check] [--force] [--open\|--no-open]` | Compile and publish a custom display page; by default the target must be `formType=display` |
293
295
  | `openyida update-form-config <appType> <formUuid> <isRenderNav> <title>` | Update page/form display configuration |
294
296
 
295
297
  ### Data, Permissions, and Sharing
package/bin/yida.js CHANGED
@@ -291,6 +291,7 @@ function applyLoginEnvironmentFlags(cliArgs) {
291
291
  '--overseas': 'intl',
292
292
  '--international': 'intl',
293
293
  '--global': 'intl',
294
+ '--yidaapps': 'intl',
294
295
  '--alibaba': 'alibaba',
295
296
  '--internal': 'alibaba',
296
297
  '--intranet': 'alibaba',
@@ -317,6 +318,11 @@ function applyLoginEnvironmentFlags(cliArgs) {
317
318
  return filteredArgs;
318
319
  }
319
320
 
321
+ function applyGlobalEnvironmentFlags() {
322
+ const filteredArgs = applyLoginEnvironmentFlags(args);
323
+ args.splice(0, args.length, ...filteredArgs);
324
+ }
325
+
320
326
  // 解析全局 --quiet 开关:从 args 中剔除并设置 YIDA_QUIET=1,让 chalk.js
321
327
  // 的所有装饰输出(banner/step/info/...)变 no-op,AI 即可直接 `... --quiet | jq`。
322
328
  function applyQuietFlag() {
@@ -329,6 +335,7 @@ function applyQuietFlag() {
329
335
 
330
336
  async function main() {
331
337
  applyQuietFlag();
338
+ applyGlobalEnvironmentFlags();
332
339
 
333
340
  if (!command || command === '--help' || command === '-h') {
334
341
  handleFirstRunGuide();
@@ -611,7 +618,7 @@ async function main() {
611
618
  case 'publish': {
612
619
  // 参数顺序:<源文件路径> <appType> <formUuid>
613
620
  // publish.js 内部读取顺序:argv[2]=appType, argv[3]=formUuid, argv[4]=sourceFile
614
- const passThroughFlags = new Set(['--skip-lint', '--health-check', '--check', '--open', '--no-open', '--compat', '--modern']);
621
+ const passThroughFlags = new Set(['--skip-lint', '--health-check', '--check', '--open', '--no-open', '--compat', '--modern', '--force']);
615
622
  const forwardedFlags = args.filter(arg => passThroughFlags.has(arg));
616
623
  const filteredArgs = args.filter(arg => !passThroughFlags.has(arg));
617
624
  if (filteredArgs.length < 3) {
@@ -26,6 +26,7 @@ const { banner, step, label, success, fail, warn, info, error, result, usage, hi
26
26
  const { compileSource } = require('./page-compiler');
27
27
  const { runLintCheck } = require('./page-linter');
28
28
  const { buildPageFile, isAuthoringPath } = require('./page-compat');
29
+ const { fetchFormPageList } = require('./form-navigation');
29
30
  const { parseOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
30
31
 
31
32
  // ── 配置读取 ──────────────────────────────────────────
@@ -43,7 +44,8 @@ function parseArgs() {
43
44
  const skipLint = args.includes('--skip-lint');
44
45
  const healthCheck = args.includes('--health-check') || args.includes('--check');
45
46
  const compat = args.includes('--compat') || args.includes('--modern');
46
- const filteredArgs = args.filter(arg => arg !== '--skip-lint' && arg !== '--health-check' && arg !== '--check' && arg !== '--compat' && arg !== '--modern');
47
+ const force = args.includes('--force');
48
+ const filteredArgs = args.filter(arg => arg !== '--skip-lint' && arg !== '--health-check' && arg !== '--check' && arg !== '--compat' && arg !== '--modern' && arg !== '--force');
47
49
 
48
50
  if (filteredArgs.length < 3) {
49
51
  usage(t('publish.usage'), t('publish.example'));
@@ -56,6 +58,7 @@ function parseArgs() {
56
58
  skipLint,
57
59
  healthCheck,
58
60
  compat,
61
+ force,
59
62
  browserOpenMode: openOption.mode,
60
63
  };
61
64
  }
@@ -522,6 +525,76 @@ function warnDuplicateSourceMismatches(sourcePath) {
522
525
  return mismatches;
523
526
  }
524
527
 
528
+ function normalizeFormType(formType) {
529
+ return String(formType || '').trim().toLowerCase();
530
+ }
531
+
532
+ function findPublishTarget(forms, formUuid) {
533
+ return (Array.isArray(forms) ? forms : []).find((form) => form && form.formUuid === formUuid) || null;
534
+ }
535
+
536
+ function isCustomPageTarget(form) {
537
+ return normalizeFormType(form && form.formType) === 'display';
538
+ }
539
+
540
+ async function verifyPublishTarget(appType, formUuid, authRef, options = {}) {
541
+ if (options.force) {
542
+ return { ok: true, skipped: true };
543
+ }
544
+
545
+ try {
546
+ const forms = await fetchFormPageList(appType, authRef);
547
+ const target = findPublishTarget(forms, formUuid);
548
+
549
+ if (!target) {
550
+ return { ok: false, reason: 'not_found' };
551
+ }
552
+
553
+ if (!isCustomPageTarget(target)) {
554
+ return { ok: false, reason: 'wrong_type', target };
555
+ }
556
+
557
+ return { ok: true, target };
558
+ } catch (targetError) {
559
+ return { ok: false, reason: 'fetch_failed', error: targetError };
560
+ }
561
+ }
562
+
563
+ async function ensurePublishTargetOrExit(appType, formUuid, authRef, options = {}) {
564
+ if (options.force) {
565
+ warn(t('publish.target_check_forced'));
566
+ return null;
567
+ }
568
+
569
+ info(t('publish.target_checking'));
570
+ const check = await verifyPublishTarget(appType, formUuid, authRef, options);
571
+
572
+ if (check.ok) {
573
+ const targetName = check.target.formName || formUuid;
574
+ const targetType = check.target.formType || 'display';
575
+ success(t('publish.target_check_ok', targetName, targetType));
576
+ return check.target;
577
+ }
578
+
579
+ if (check.reason === 'wrong_type') {
580
+ const target = check.target || {};
581
+ const targetName = target.formName || formUuid;
582
+ const targetType = target.formType || '-';
583
+ fail(t('publish.target_type_invalid', formUuid, targetType));
584
+ hint(t('publish.target_type_hint', targetName, targetType));
585
+ } else if (check.reason === 'not_found') {
586
+ fail(t('publish.target_not_found', formUuid));
587
+ } else {
588
+ const message = check.error && check.error.message ? check.error.message : t('common.unknown_error');
589
+ fail(t('publish.target_check_failed', message));
590
+ }
591
+
592
+ hint(t('publish.target_list_hint', appType));
593
+ hint(t('publish.target_force_hint'));
594
+ process.exit(1);
595
+ return null;
596
+ }
597
+
525
598
  function sendHealthCheckRequest(pageUrl, cookies) {
526
599
  return new Promise((resolve) => {
527
600
  const parsedUrl = new URL(pageUrl);
@@ -575,7 +648,7 @@ function sendHealthCheckRequest(pageUrl, cookies) {
575
648
  // ── 主流程 ────────────────────────────────────────────
576
649
 
577
650
  async function main() {
578
- const { appType, formUuid, sourceFile, skipLint, healthCheck, compat, browserOpenMode } = parseArgs();
651
+ const { appType, formUuid, sourceFile, skipLint, healthCheck, compat, force, browserOpenMode } = parseArgs();
579
652
 
580
653
  let sourcePath = path.resolve(sourceFile);
581
654
  if (!fs.existsSync(sourcePath)) {
@@ -625,6 +698,16 @@ async function main() {
625
698
  }
626
699
  let { csrf_token: csrfToken, cookies } = cookieData;
627
700
  let baseUrl = resolveBaseUrl(cookieData);
701
+ const authRef = {
702
+ csrfToken,
703
+ cookies,
704
+ baseUrl,
705
+ cookieData,
706
+ };
707
+ await ensurePublishTargetOrExit(appType, formUuid, authRef, { force });
708
+ csrfToken = authRef.csrfToken;
709
+ cookies = authRef.cookies;
710
+ baseUrl = authRef.baseUrl;
628
711
 
629
712
  banner(t('publish.title'));
630
713
  label('Base URL:', baseUrl);
@@ -744,4 +827,8 @@ if (require.main === module) {
744
827
  module.exports = main;
745
828
  module.exports.findDuplicateSourceMismatches = findDuplicateSourceMismatches;
746
829
  module.exports.sendHealthCheckRequest = sendHealthCheckRequest;
830
+ module.exports.normalizeFormType = normalizeFormType;
831
+ module.exports.findPublishTarget = findPublishTarget;
832
+ module.exports.isCustomPageTarget = isCustomPageTarget;
833
+ module.exports.verifyPublishTarget = verifyPublishTarget;
747
834
  }
@@ -20,7 +20,7 @@ const COMMAND_GROUPS = [
20
20
  id: 'auth',
21
21
  titleKey: 'help.group_auth',
22
22
  commands: [
23
- command('login', ['login'], 'login [--qr|--agent-qr|--codex|--browser] [--env <name>|--overseas] [--corp-id <corpId>]', 'help.cmd_login', {
23
+ command('login', ['login'], 'login [--qr|--agent-qr|--codex|--browser] [--env <name>|--overseas|--yidaapps] [--corp-id <corpId>]', 'help.cmd_login', {
24
24
  requiresLogin: false,
25
25
  output: 'json',
26
26
  }),
@@ -63,7 +63,7 @@ const COMMAND_GROUPS = [
63
63
  command('build-page', ['build-page'], 'build-page <sourceFile> [--output file|--write]', 'help.cmd_build_page', { requiresLogin: false }),
64
64
  command('check-page', ['check-page'], 'check-page <src> [--compat]', 'help.cmd_check_page', { output: 'text|json' }),
65
65
  command('compile', ['compile'], 'compile <src>', 'help.cmd_compile', { requiresLogin: false }),
66
- command('publish', ['publish'], 'publish <src> <appType> <formUuid> [--health-check] [--open|--no-open]', 'help.cmd_publish'),
66
+ command('publish', ['publish'], 'publish <src> <appType> <formUuid> [--health-check] [--force] [--open|--no-open]', 'help.cmd_publish'),
67
67
  command('update-form-config', ['update-form-config'], 'update-form-config <appType> ...', 'help.cmd_update_form_config'),
68
68
  ],
69
69
  },
@@ -23,6 +23,7 @@ const {
23
23
  DEFAULT_INTERNATIONAL_ENV,
24
24
  DEFAULT_ALIBABA_INTERNAL_ENV,
25
25
  resolveEnvNameAlias,
26
+ inferLoginUrlForBaseUrl,
26
27
  } = require('./env-manager');
27
28
 
28
29
  // ── 颜色常量 ──────────────────────────────────────────
@@ -101,6 +102,9 @@ function detectSetupRecommendation() {
101
102
  if ((process.env.OPENYIDA_LOGIN_URL || '').includes('login.dingtalk.io')) {
102
103
  return 'intl';
103
104
  }
105
+ if ((process.env.OPENYIDA_ENDPOINT || '').includes('yidaapps.com')) {
106
+ return 'intl';
107
+ }
104
108
  if ((process.env.OPENYIDA_ENDPOINT || '').includes('alibaba-inc.com')) {
105
109
  return 'alibaba';
106
110
  }
@@ -332,9 +336,12 @@ async function cmdAdd(envName) {
332
336
  process.exit(1);
333
337
  }
334
338
 
335
- // 自动推导登录 URL
336
- const inferredLoginUrl = baseUrl.replace(/\/+$/, '') + '/workPlatform';
337
- const defaultLoginUrl = existingConfig?.loginUrl || inferredLoginUrl;
339
+ // 自动推导登录 URL:yidaapps.com 必须走 login.dingtalk.io OAuth。
340
+ const inferredLoginUrl = inferLoginUrlForBaseUrl(baseUrl);
341
+ const workPlatformLoginUrl = baseUrl.replace(/\/+$/, '') + '/workPlatform';
342
+ const defaultLoginUrl = existingConfig?.loginUrl && existingConfig.loginUrl !== workPlatformLoginUrl
343
+ ? existingConfig.loginUrl
344
+ : inferredLoginUrl;
338
345
  const loginUrl = await askQuestion(`${BOLD}登录页面地址${RESET}:`, defaultLoginUrl);
339
346
 
340
347
  const defaultDescription = existingConfig?.description || '';
@@ -390,7 +397,7 @@ async function cmdSetup() {
390
397
  },
391
398
  {
392
399
  key: 'intl',
393
- title: '海外 DingTalk / Dingtalk Wukong',
400
+ title: '海外 YiDA Apps / DingTalk / Dingtalk Wukong',
394
401
  baseUrl: DEFAULT_INTERNATIONAL_ENV.baseUrl,
395
402
  loginUrl: DEFAULT_INTERNATIONAL_ENV.loginUrl,
396
403
  },
@@ -31,6 +31,7 @@ const { findProjectRoot } = require('./utils');
31
31
 
32
32
  const DEFAULT_BASE_URL = 'https://www.aliwork.com';
33
33
  const DEFAULT_LOGIN_URL = 'https://www.aliwork.com/workPlatform';
34
+ const INTERNATIONAL_BASE_URL = 'https://www.yidaapps.com';
34
35
  const DINGTALK_OAUTH_CLIENT_ID = 'suite9xvlxxerybljwheo';
35
36
  const DINGTALK_LOGIN_ORIGIN = 'https://login.dingtalk.com';
36
37
  const DINGTALK_INTL_LOGIN_ORIGIN = 'https://login.dingtalk.io';
@@ -69,6 +70,11 @@ function buildDingtalkOAuthLoginUrl(options = {}) {
69
70
  }
70
71
 
71
72
  const INTERNATIONAL_LOGIN_URL = buildDingtalkOAuthLoginUrl({
73
+ loginOrigin: DINGTALK_INTL_LOGIN_ORIGIN,
74
+ baseUrl: INTERNATIONAL_BASE_URL,
75
+ lang: 'en_US',
76
+ });
77
+ const LEGACY_INTERNATIONAL_LOGIN_URL = buildDingtalkOAuthLoginUrl({
72
78
  loginOrigin: DINGTALK_INTL_LOGIN_ORIGIN,
73
79
  baseUrl: DEFAULT_BASE_URL,
74
80
  lang: 'en_US',
@@ -92,9 +98,9 @@ const DEFAULT_ALIBABA_INTERNAL_ENV = {
92
98
 
93
99
  /** 海外版 DingTalk / Wukong 环境配置 */
94
100
  const DEFAULT_INTERNATIONAL_ENV = {
95
- baseUrl: DEFAULT_BASE_URL,
101
+ baseUrl: INTERNATIONAL_BASE_URL,
96
102
  loginUrl: INTERNATIONAL_LOGIN_URL,
97
- description: '海外版 DingTalk / Wukong(login.dingtalk.io)',
103
+ description: '海外版 YiDA Apps / DingTalk / Wukong(login.dingtalk.io)',
98
104
  cookieFile: 'cookies-intl.json',
99
105
  };
100
106
 
@@ -122,11 +128,13 @@ const ENV_ALIASES = {
122
128
 
123
129
  const SHARED_COOKIE_DOMAINS = new Set([
124
130
  'aliwork.com',
131
+ 'yidaapps.com',
125
132
  'alibaba-inc.com',
126
133
  ]);
127
134
 
128
135
  const KNOWN_YIDA_HOSTS = new Set([
129
136
  'www.aliwork.com',
137
+ 'www.yidaapps.com',
130
138
  'yida-group.alibaba-inc.com',
131
139
  ]);
132
140
 
@@ -156,9 +164,21 @@ function ensureBuiltinEnvironments(config) {
156
164
  config.environments[envName] = { ...envConfig };
157
165
  }
158
166
  }
167
+ if (isLegacyInternationalEnv(config.environments.intl)) {
168
+ config.environments.intl = { ...DEFAULT_INTERNATIONAL_ENV };
169
+ }
159
170
  return config;
160
171
  }
161
172
 
173
+ function isLegacyInternationalEnv(envConfig) {
174
+ return !!(
175
+ envConfig &&
176
+ normalizeBaseUrl(envConfig.baseUrl, null) === DEFAULT_BASE_URL &&
177
+ (!envConfig.loginUrl || envConfig.loginUrl === LEGACY_INTERNATIONAL_LOGIN_URL) &&
178
+ (!envConfig.cookieFile || envConfig.cookieFile === 'cookies-intl.json')
179
+ );
180
+ }
181
+
162
182
  function normalizeBaseUrl(value, fallback = null) {
163
183
  if (!value) { return fallback; }
164
184
  const trimmed = String(value).trim();
@@ -199,12 +219,47 @@ function isYidaServiceHost(hostname) {
199
219
  if (!host) { return false; }
200
220
  if (KNOWN_YIDA_HOSTS.has(host)) { return true; }
201
221
  if (host.endsWith('.aliwork.com') && host !== 'aliwork.com') { return true; }
222
+ if (host.endsWith('.yidaapps.com') && host !== 'yidaapps.com') { return true; }
202
223
  if (host.endsWith('.alibaba-inc.com') && host !== 'alibaba-inc.com') {
203
224
  return host.startsWith('yida-') || host.includes('.yida-') || host.includes('.yida.');
204
225
  }
205
226
  return false;
206
227
  }
207
228
 
229
+ function isYidaAppsHost(hostname) {
230
+ const host = normalizeHostname(hostname);
231
+ return host === 'yidaapps.com' || host.endsWith('.yidaapps.com');
232
+ }
233
+
234
+ function isDefaultWorkPlatformLoginUrl(loginUrl, baseUrl) {
235
+ const loginOrigin = normalizeBaseUrl(loginUrl, null);
236
+ const baseOrigin = normalizeBaseUrl(baseUrl, null);
237
+ if (!loginOrigin || !baseOrigin || loginOrigin !== baseOrigin) {
238
+ return false;
239
+ }
240
+
241
+ try {
242
+ const parsedUrl = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(loginUrl) ? loginUrl : `https://${loginUrl}`);
243
+ return parsedUrl.pathname.replace(/\/+$/, '') === '/workPlatform' &&
244
+ !parsedUrl.search &&
245
+ !parsedUrl.hash;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+
251
+ function inferLoginUrlForBaseUrl(baseUrl, fallbackLoginUrl) {
252
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl, DEFAULT_BASE_URL);
253
+ if (isYidaAppsHost(normalizedBaseUrl)) {
254
+ return buildDingtalkOAuthLoginUrl({
255
+ loginOrigin: DINGTALK_INTL_LOGIN_ORIGIN,
256
+ baseUrl: normalizedBaseUrl,
257
+ lang: 'en_US',
258
+ });
259
+ }
260
+ return fallbackLoginUrl || `${normalizedBaseUrl}/workPlatform`;
261
+ }
262
+
208
263
  function cookieDomainToBaseUrl(domain, fallbackUrl) {
209
264
  const host = normalizeHostname(domain);
210
265
  if (!host || isSharedCookieDomain(host)) {
@@ -241,7 +296,15 @@ function deriveBaseUrlFromCookies(cookies = [], fallbackUrl = DEFAULT_BASE_URL)
241
296
  }
242
297
 
243
298
  function deriveBaseUrlFromUrl(fallbackBaseUrl, candidateUrl) {
244
- const fallbackOrigin = normalizeBaseUrl(fallbackBaseUrl, DEFAULT_BASE_URL);
299
+ let fallbackOrigin = normalizeBaseUrl(fallbackBaseUrl, DEFAULT_BASE_URL);
300
+ const fallbackHost = normalizeHostname(fallbackOrigin);
301
+ if (!isYidaServiceHost(fallbackHost)) {
302
+ const callbackOrigin = deriveBaseUrlFromDingtalkOAuthUrl(fallbackBaseUrl, null);
303
+ if (callbackOrigin) {
304
+ fallbackOrigin = callbackOrigin;
305
+ }
306
+ }
307
+
245
308
  const candidateOrigin = normalizeBaseUrl(candidateUrl, null);
246
309
  if (!candidateOrigin) { return fallbackOrigin; }
247
310
 
@@ -249,6 +312,29 @@ function deriveBaseUrlFromUrl(fallbackBaseUrl, candidateUrl) {
249
312
  return isYidaServiceHost(candidateHost) ? candidateOrigin : fallbackOrigin;
250
313
  }
251
314
 
315
+ function deriveBaseUrlFromDingtalkOAuthUrl(oauthUrl, fallbackUrl) {
316
+ if (!oauthUrl) { return fallbackUrl || null; }
317
+
318
+ try {
319
+ const parsedUrl = new URL(oauthUrl);
320
+ const host = normalizeHostname(parsedUrl.hostname);
321
+ const isDingtalkLoginHost = host.endsWith('dingtalk.com') || host.endsWith('dingtalk.io');
322
+ if (!isDingtalkLoginHost || !parsedUrl.pathname.startsWith('/oauth2/')) {
323
+ return fallbackUrl || null;
324
+ }
325
+
326
+ const redirectUri = parsedUrl.searchParams.get('redirect_uri');
327
+ const redirectOrigin = normalizeBaseUrl(redirectUri, null);
328
+ if (redirectOrigin && isYidaServiceHost(normalizeHostname(redirectOrigin))) {
329
+ return redirectOrigin;
330
+ }
331
+ } catch {
332
+ // ignore malformed URLs
333
+ }
334
+
335
+ return fallbackUrl || null;
336
+ }
337
+
252
338
  // ── 配置文件读写 ──────────────────────────────────────
253
339
 
254
340
  /**
@@ -398,13 +484,23 @@ function resolveLoginUrl(projectRoot) {
398
484
  return process.env.OPENYIDA_LOGIN_URL;
399
485
  }
400
486
 
487
+ if (process.env.OPENYIDA_ENDPOINT) {
488
+ return inferLoginUrlForBaseUrl(process.env.OPENYIDA_ENDPOINT);
489
+ }
490
+
401
491
  const { config: envConfig } = getCurrentEnvConfig(projectRoot);
492
+ const baseUrl = normalizeBaseUrl(envConfig.baseUrl, DEFAULT_BASE_URL);
493
+ if (!envConfig.loginUrl || isDefaultWorkPlatformLoginUrl(envConfig.loginUrl, baseUrl)) {
494
+ return inferLoginUrlForBaseUrl(baseUrl, envConfig.loginUrl || DEFAULT_LOGIN_URL);
495
+ }
496
+
402
497
  return envConfig.loginUrl || DEFAULT_LOGIN_URL;
403
498
  }
404
499
 
405
500
  module.exports = {
406
501
  DEFAULT_BASE_URL,
407
502
  DEFAULT_LOGIN_URL,
503
+ INTERNATIONAL_BASE_URL,
408
504
  DINGTALK_OAUTH_CLIENT_ID,
409
505
  DINGTALK_LOGIN_ORIGIN,
410
506
  DINGTALK_INTL_LOGIN_ORIGIN,
@@ -426,6 +522,9 @@ module.exports = {
426
522
  normalizeBaseUrl,
427
523
  normalizeHostname,
428
524
  isYidaServiceHost,
525
+ isYidaAppsHost,
526
+ inferLoginUrlForBaseUrl,
527
+ deriveBaseUrlFromDingtalkOAuthUrl,
429
528
  deriveBaseUrlFromCookies,
430
529
  deriveBaseUrlFromUrl,
431
530
  };
@@ -461,6 +461,15 @@ module.exports = {
461
461
  lint_passed: ' ✅ فحص الكود ناجح\n',
462
462
  lint_skipped: '\n⏭️ تم تخطي فحص الكود المسبق (--skip-lint)\n',
463
463
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
464
+ target_checking: ' Checking publish target type...',
465
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
466
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
467
+ target_check_failed: 'Could not verify publish target: {0}',
468
+ target_not_found: 'Publish target was not found in app navigation: {0}',
469
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
470
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
471
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
472
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
464
473
  step_compile: '\n📦 Step 1: تجميع المصدر وبناء المخطط\n',
465
474
  reading_source: '[1/4] جارٍ قراءة مصدر {0}...',
466
475
  compiling: '[2/4] جارٍ تجميع {0} بـ Babel...',
@@ -461,6 +461,15 @@ module.exports = {
461
461
  lint_passed: ' ✅ Code-Prüfung bestanden\n',
462
462
  lint_skipped: '\n⏭️ Code-Vorprüfung übersprungen (--skip-lint)\n',
463
463
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
464
+ target_checking: ' Checking publish target type...',
465
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
466
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
467
+ target_check_failed: 'Could not verify publish target: {0}',
468
+ target_not_found: 'Publish target was not found in app navigation: {0}',
469
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
470
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
471
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
472
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
464
473
  step_compile: '\n📦 Step 1: Quellcode kompilieren und Schema erstellen\n',
465
474
  reading_source: '[1/4] {0}-Quellcode wird gelesen...',
466
475
  compiling: '[2/4] Babel kompiliert {0}...',
@@ -959,6 +959,15 @@ Examples:
959
959
  lint_passed: ' ✅ Code check passed\n',
960
960
  lint_skipped: '\n⏭️ Skipped code pre-check (--skip-lint)\n',
961
961
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
962
+ target_checking: ' Checking publish target type...',
963
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
964
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
965
+ target_check_failed: 'Could not verify publish target: {0}',
966
+ target_not_found: 'Publish target was not found in app navigation: {0}',
967
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
968
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
969
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
970
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
962
971
  step_compile: '\n📦 Step 1: Compile source & build Schema\n',
963
972
  reading_source: '[1/4] Reading {0} source...',
964
973
  compiling: '[2/4] Babel compiling {0}...',
@@ -461,6 +461,15 @@ module.exports = {
461
461
  lint_passed: ' ✅ Verificación de código aprobada\n',
462
462
  lint_skipped: '\n⏭️ Pre-verificación de código omitida (--skip-lint)\n',
463
463
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
464
+ target_checking: ' Checking publish target type...',
465
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
466
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
467
+ target_check_failed: 'Could not verify publish target: {0}',
468
+ target_not_found: 'Publish target was not found in app navigation: {0}',
469
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
470
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
471
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
472
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
464
473
  step_compile: '\n📦 Step 1: Compilar fuente y construir esquema\n',
465
474
  reading_source: '[1/4] Leyendo fuente {0}...',
466
475
  compiling: '[2/4] Compilando {0} con Babel...',
@@ -461,6 +461,15 @@ module.exports = {
461
461
  lint_passed: ' ✅ Vérification du code réussie\n',
462
462
  lint_skipped: '\n⏭️ Pré-vérification du code ignorée (--skip-lint)\n',
463
463
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
464
+ target_checking: ' Checking publish target type...',
465
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
466
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
467
+ target_check_failed: 'Could not verify publish target: {0}',
468
+ target_not_found: 'Publish target was not found in app navigation: {0}',
469
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
470
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
471
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
472
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
464
473
  step_compile: '\n📦 Step 1 : Compilation et construction du schéma\n',
465
474
  reading_source: '[1/4] Lecture de la source {0}...',
466
475
  compiling: '[2/4] Compilation Babel de {0}...',
@@ -461,6 +461,15 @@ module.exports = {
461
461
  lint_passed: ' ✅ कोड जांच पास\n',
462
462
  lint_skipped: '\n⏭️ कोड पूर्व-जांच छोड़ी गई (--skip-lint)\n',
463
463
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
464
+ target_checking: ' Checking publish target type...',
465
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
466
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
467
+ target_check_failed: 'Could not verify publish target: {0}',
468
+ target_not_found: 'Publish target was not found in app navigation: {0}',
469
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
470
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
471
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
472
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
464
473
  step_compile: '\n📦 Step 1: स्रोत संकलित करें और स्कीमा बनाएं\n',
465
474
  reading_source: '[1/4] {0} स्रोत पढ़ा जा रहा है...',
466
475
  compiling: '[2/4] Babel से {0} संकलित हो रहा है...',
@@ -881,6 +881,15 @@ openyida - Yida CLI ツール
881
881
  lint_passed: ' ✅ コードチェック合格\n',
882
882
  lint_skipped: '\n⏭️ コードプレチェックをスキップしました(--skip-lint)\n',
883
883
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
884
+ target_checking: ' Checking publish target type...',
885
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
886
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
887
+ target_check_failed: 'Could not verify publish target: {0}',
888
+ target_not_found: 'Publish target was not found in app navigation: {0}',
889
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
890
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
891
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
892
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
884
893
  step_compile: '\n📦 Step 1: ソースをコンパイルして Schema を構築\n',
885
894
  reading_source: '[1/4] {0} のソースを読み込み中...',
886
895
  compiling: '[2/4] Babel で {0} をコンパイル中...',
@@ -463,6 +463,15 @@ module.exports = {
463
463
  lint_passed: ' ✅ 코드 검사 통과\n',
464
464
  lint_skipped: '\n⏭️ 코드 사전 검사 건너뜀 (--skip-lint)\n',
465
465
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
466
+ target_checking: ' Checking publish target type...',
467
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
468
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
469
+ target_check_failed: 'Could not verify publish target: {0}',
470
+ target_not_found: 'Publish target was not found in app navigation: {0}',
471
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
472
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
473
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
474
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
466
475
  step_compile: '\n📦 Step 1: 소스 컴파일 및 스키마 빌드\n',
467
476
  reading_source: '[1/4] {0} 소스 읽는 중...',
468
477
  compiling: '[2/4] Babel로 {0} 컴파일 중...',
@@ -461,6 +461,15 @@ module.exports = {
461
461
  lint_passed: ' ✅ Verificação de código aprovada\n',
462
462
  lint_skipped: '\n⏭️ Pré-verificação de código ignorada (--skip-lint)\n',
463
463
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
464
+ target_checking: ' Checking publish target type...',
465
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
466
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
467
+ target_check_failed: 'Could not verify publish target: {0}',
468
+ target_not_found: 'Publish target was not found in app navigation: {0}',
469
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
470
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
471
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
472
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
464
473
  step_compile: '\n📦 Step 1: Compilar fonte e construir esquema\n',
465
474
  reading_source: '[1/4] Lendo fonte {0}...',
466
475
  compiling: '[2/4] Compilando {0} com Babel...',
@@ -461,6 +461,15 @@ module.exports = {
461
461
  lint_passed: ' ✅ Kiểm tra mã thành công\n',
462
462
  lint_skipped: '\n⏭️ Đã bỏ qua kiểm tra mã trước (--skip-lint)\n',
463
463
  duplicate_source_mismatch: ' ⚠️ Found another copy with the same file name but different content. Publishing: {0}; other copy: {1}',
464
+ target_checking: ' Checking publish target type...',
465
+ target_check_forced: ' ⚠️ Skipped publish target type check (--force)',
466
+ target_check_ok: ' ✅ Publish target confirmed: {0} ({1})',
467
+ target_check_failed: 'Could not verify publish target: {0}',
468
+ target_not_found: 'Publish target was not found in app navigation: {0}',
469
+ target_type_invalid: 'Publish target is not a custom display page: {0} current type is {1}',
470
+ target_type_hint: 'The current target "{0}" does not look like a custom page (type: {1}). Do not publish JSX to data forms or process forms.',
471
+ target_list_hint: 'Run openyida list-forms {0} --keyword <page name>, then choose a custom page formUuid with formType=display.',
472
+ target_force_hint: 'Append --force only when you intentionally bypass this guard; it overwrites the target Schema and should be used only for a known custom page.',
464
473
  step_compile: '\n📦 Step 1: Biên dịch nguồn và xây dựng schema\n',
465
474
  reading_source: '[1/4] Đang đọc nguồn {0}...',
466
475
  compiling: '[2/4] Đang biên dịch {0} với Babel...',
@@ -792,6 +792,15 @@ openyida - 宜搭命令列工具
792
792
  lint_passed: ' ✅ 程式碼檢查通過\n',
793
793
  lint_skipped: '\n⏭️ 跳過程式碼預檢(--skip-lint)\n',
794
794
  duplicate_source_mismatch: ' ⚠️ 偵測到同名雙副本但內容不一致。目前發布:{0};另一份:{1}',
795
+ target_checking: ' 正在校驗發布目標類型...',
796
+ target_check_forced: ' ⚠️ 已跳過發布目標類型校驗(--force)',
797
+ target_check_ok: ' ✅ 發布目標已確認:{0} ({1})',
798
+ target_check_failed: '無法校驗發布目標:{0}',
799
+ target_not_found: '未在應用程式導航中找到發布目標:{0}',
800
+ target_type_invalid: '發布目標不是自訂展示頁面:{0} 目前類型為 {1}',
801
+ target_type_hint: '目前目標「{0}」看起來不是自訂頁面(類型:{1})。請不要把 JSX 發布到資料表或流程表單。',
802
+ target_list_hint: '可先執行 openyida list-forms {0} --keyword <頁面名>,選擇 formType=display 的自訂頁面 formUuid。',
803
+ target_force_hint: '確認要繞過保護時可追加 --force;這會覆蓋目標 Schema,請只在明確知道目標是自訂頁面時使用。',
795
804
  step_compile: '\n📦 Step 1:編譯原始碼 & 建構 Schema\n',
796
805
  reading_source: '[1/4] 讀取 {0} 原始碼...',
797
806
  compiling: '[2/4] Babel 編譯 {0}...',
@@ -961,6 +961,15 @@ openyida - 宜搭命令行工具
961
961
  lint_passed: ' ✅ 代码检查通过\n',
962
962
  lint_skipped: '\n⏭️ 跳过代码预检(--skip-lint)\n',
963
963
  duplicate_source_mismatch: ' ⚠️ 检测到同名双副本但内容不一致。当前发布: {0};另一份: {1}',
964
+ target_checking: ' 正在校验发布目标类型...',
965
+ target_check_forced: ' ⚠️ 已跳过发布目标类型校验(--force)',
966
+ target_check_ok: ' ✅ 发布目标已确认:{0} ({1})',
967
+ target_check_failed: '无法校验发布目标:{0}',
968
+ target_not_found: '未在应用导航中找到发布目标:{0}',
969
+ target_type_invalid: '发布目标不是自定义展示页面:{0} 当前类型为 {1}',
970
+ target_type_hint: '当前目标「{0}」看起来不是自定义页面(类型:{1})。请不要把 JSX 发布到数据表或流程表单。',
971
+ target_list_hint: '可先运行 openyida list-forms {0} --keyword <页面名>,选择 formType=display 的自定义页面 formUuid。',
972
+ target_force_hint: '确认要绕过保护时可追加 --force;这会覆盖目标 Schema,请只在明确知道目标是自定义页面时使用。',
964
973
  step_compile: '\n📦 Step 1: 编译源码 & 构建 Schema\n',
965
974
  reading_source: '[1/4] 读取 {0} 源码...',
966
975
  compiling: '[2/4] Babel 编译 {0}...',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.5.15",
3
+ "version": "2026.5.17",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -44,16 +44,27 @@ description: 将 JSX 源码编译发布到宜搭自定义页面。Babel 转 ES5
44
44
  ## 命令
45
45
 
46
46
  ```bash
47
- openyida publish <源文件路径> <appType> <formUuid> [--compat] [--health-check]
47
+ openyida publish <源文件路径> <appType> <formUuid> [--compat] [--health-check] [--force]
48
48
  ```
49
49
 
50
50
  | 参数 | 必填 | 说明 |
51
51
  |------|------|------|
52
52
  | `源文件路径` | 是 | JSX 源码路径,推荐 `project/pages/src/my-page.oyd.jsx` |
53
53
  | `appType` | 是 | 应用 ID |
54
- | `formUuid` | 是 | 自定义页面 ID |
54
+ | `formUuid` | 是 | 自定义页面 ID,必须是 `openyida list-forms <appType>` 返回的 `formType=display` 目标,不要使用数据底表或流程表单 ID |
55
55
  | `--compat` / `--modern` | 否 | 对普通 `.jsx` 也强制启用 OpenYida 兼容构建;`.oyd.jsx` 默认自动启用 |
56
56
  | `--health-check` | 否 | 发布成功后请求页面 URL,回显 HTTP 健康检查结果,避免只看到 200 接口返回但首屏坏掉 |
57
+ | `--force` | 否 | 显式绕过发布目标类型保护;只有确认目标是自定义页面但导航接口暂时无法识别时才使用 |
58
+
59
+ ## 发布目标确认
60
+
61
+ 发布前先确认目标页面,避免把 JSX 覆盖到数据底表:
62
+
63
+ ```bash
64
+ openyida list-forms <appType> --keyword <页面名>
65
+ ```
66
+
67
+ 只选择 `formType=display` 的 `formUuid` 作为发布目标。源码里用于 `this.utils.yida` 读写数据的普通表单常量(如 `FORM_SKILL`、`FORM_DATA`、`FORM_TABLE`)通常是数据底表,不能作为 `openyida publish` 的第三个参数。
57
68
 
58
69
  ## OpenYida 兼容编译
59
70
 
@@ -99,6 +110,7 @@ body { background-color: #f2f3f5; }
99
110
  | OpenYida 兼容构建失败 | 查看 `check-page --json` 的 `build.errors`,通常是不支持的 Hook、非空 useEffect deps、未支持 import |
100
111
  | Babel 编译失败 | 检查 JSX 语法;如果是现代 React authoring,确认文件是 `.oyd.jsx` 或发布时加 `--compat` |
101
112
  | UglifyJS 压缩失败 | 检查是否有 ES6+ 语法未被 Babel 转译,确认 export function 格式正确 |
113
+ | 发布目标不是自定义展示页面 | 运行 `openyida list-forms <appType> --keyword <页面名>`,改用 `formType=display` 的页面 ID;不要对数据底表追加 `--force` |
102
114
  | saveFormSchema 接口失败(401) | 执行 `openyida login` 重新登录后重试 |
103
115
  | corpId 不匹配 | 询问用户是否切换组织或创建新应用,不得强行发布 |
104
116
  | 发布后页面空白 | 检查 `renderJsx` 函数是否正确导出,检查浏览器控制台报错 |