yidaconnector 2026.6.11

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/bin/yida.js +670 -0
  4. package/lib/app/form-navigation.js +58 -0
  5. package/lib/app/get-schema.js +538 -0
  6. package/lib/auth/auth.js +294 -0
  7. package/lib/auth/cdp-browser-login.js +390 -0
  8. package/lib/auth/codex-login.js +71 -0
  9. package/lib/auth/login.js +475 -0
  10. package/lib/auth/org.js +363 -0
  11. package/lib/auth/qr-login.js +1563 -0
  12. package/lib/core/chalk.js +384 -0
  13. package/lib/core/check-update.js +82 -0
  14. package/lib/core/cli-error.js +39 -0
  15. package/lib/core/command-manifest.js +106 -0
  16. package/lib/core/env-cmd.js +545 -0
  17. package/lib/core/env-manager.js +601 -0
  18. package/lib/core/env.js +287 -0
  19. package/lib/core/i18n.js +177 -0
  20. package/lib/core/locales/ar.js +805 -0
  21. package/lib/core/locales/de.js +805 -0
  22. package/lib/core/locales/en.js +1623 -0
  23. package/lib/core/locales/es.js +805 -0
  24. package/lib/core/locales/fr.js +805 -0
  25. package/lib/core/locales/hi.js +805 -0
  26. package/lib/core/locales/ja.js +1197 -0
  27. package/lib/core/locales/ko.js +807 -0
  28. package/lib/core/locales/pt.js +805 -0
  29. package/lib/core/locales/vi.js +805 -0
  30. package/lib/core/locales/zh-HK.js +1233 -0
  31. package/lib/core/locales/zh.js +1584 -0
  32. package/lib/core/query-data.js +781 -0
  33. package/lib/core/redact.js +100 -0
  34. package/lib/core/utils.js +799 -0
  35. package/lib/core/yida-client.js +117 -0
  36. package/package.json +94 -0
  37. package/project/config.json +4 -0
  38. package/project/pages/src/demo-birthday-game.oyd.jsx +832 -0
  39. package/project/pages/src/demo-chip-insight.oyd.jsx +983 -0
  40. package/project/pages/src/demo-compat-smoke.oyd.jsx +58 -0
  41. package/project/pages/src/demo-crm-batch-entry.oyd.jsx +805 -0
  42. package/project/pages/src/demo-crm-dashboard.oyd.jsx +677 -0
  43. package/project/pages/src/demo-future-vision-2026.oyd.jsx +1102 -0
  44. package/project/pages/src/demo-ppt.oyd.jsx +1192 -0
  45. package/project/pages/src/demo-salary-calculator.oyd.jsx +904 -0
  46. package/project/pages/src/yidaconnector-knowledge-doc.oyd.jsx +1714 -0
  47. package/project/prd/demo-birthday-game.md +39 -0
  48. package/project/prd/demo-crm.md +463 -0
  49. package/project/prd/demo-dingtalk-ai-solution-center.md +425 -0
  50. package/project/prd/demo-future-vision-2026.md +78 -0
  51. package/project/prd/demo-salary-calculator.md +101 -0
  52. package/scripts/build-skills-package.js +406 -0
  53. package/scripts/check-syntax.js +59 -0
  54. package/scripts/demo-dws.sh +106 -0
  55. package/scripts/e2e-real/cleanup.js +67 -0
  56. package/scripts/e2e-real/fixtures/form-fields.json +18 -0
  57. package/scripts/e2e-real/full-runner.js +1566 -0
  58. package/scripts/e2e-real/runner.js +293 -0
  59. package/scripts/e2e-real/skill-coverage.js +115 -0
  60. package/scripts/generate-command-docs.js +109 -0
  61. package/scripts/nightly-smoke.js +134 -0
  62. package/scripts/postinstall.js +545 -0
  63. package/scripts/solution-center-runner.js +368 -0
  64. package/scripts/validate-ci.sh +50 -0
  65. package/scripts/validate-command-manifest.js +119 -0
  66. package/scripts/validate-package-size.js +78 -0
  67. package/scripts/validate-skills.js +247 -0
  68. package/scripts/validate-structure.js +66 -0
  69. package/yida-skills/SKILL.md +163 -0
  70. package/yida-skills/references/yida-api.md +1309 -0
  71. package/yida-skills/skills/large-file-write/SKILL.md +91 -0
  72. package/yida-skills/skills/large-file-write/references/write-patterns.md +149 -0
  73. package/yida-skills/skills/large-file-write/scripts/write.js +157 -0
  74. package/yida-skills/skills/yida-data-management/SKILL.md +252 -0
  75. package/yida-skills/skills/yida-data-management/references/api-matrix.md +49 -0
  76. package/yida-skills/skills/yida-data-management/references/data-format-guide.md +159 -0
  77. package/yida-skills/skills/yida-data-management/references/verified-endpoints.md +62 -0
  78. package/yida-skills/skills/yida-login/SKILL.md +159 -0
  79. package/yida-skills/skills/yida-logout/SKILL.md +67 -0
@@ -0,0 +1,601 @@
1
+ /**
2
+ * env-manager.js - 多环境配置管理
3
+ *
4
+ * 支持公有云与私有化宜搭并存,通过环境配置文件管理多套端点和登录态。
5
+ *
6
+ * 配置文件:{projectRoot}/.cache/yidaconnector-envs.json
7
+ * Cookie 隔离:.cache/cookies-{envName}.json
8
+ *
9
+ * 优先级(高 → 低):
10
+ * 1. 环境变量 YIDACONNECTOR_ENDPOINT
11
+ * 2. cookieData.base_url(登录后实际跳转域名)
12
+ * 3. 环境变量 YIDACONNECTOR_ENV 指定的环境配置
13
+ * 4. 当前激活的环境配置(yidaconnector-envs.json current 字段)
14
+ * 5. 默认公有云 https://www.aliwork.com
15
+ *
16
+ * 导出函数:
17
+ * loadEnvsConfig() - 读取环境配置文件(不存在则返回默认公有云配置)
18
+ * saveEnvsConfig(config) - 写入环境配置文件
19
+ * getCurrentEnvConfig() - 获取当前激活的环境配置(含环境变量覆盖)
20
+ * getCookieFilePath(root) - 获取当前环境的 Cookie 文件绝对路径
21
+ * migrateOldCookieFile() - 迁移旧版 cookies.json → cookies-public.json
22
+ * resolveEndpoint() - 解析最终 baseUrl(含完整优先级)
23
+ * resolveLoginUrl() - 解析最终登录 URL
24
+ * deriveBaseUrlFromLoginState() - 从 Cookie + 浏览器当前 URL 解析实际 baseUrl
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const { findProjectRoot } = require('./utils');
32
+
33
+ const DEFAULT_BASE_URL = 'https://www.aliwork.com';
34
+ const DEFAULT_LOGIN_URL = 'https://www.aliwork.com/workPlatform';
35
+ const INTERNATIONAL_BASE_URL = 'https://www.yidaapps.com';
36
+ const DINGTALK_OAUTH_CLIENT_ID = 'suite9xvlxxerybljwheo';
37
+ const DINGTALK_LOGIN_ORIGIN = 'https://login.dingtalk.com';
38
+ const DINGTALK_INTL_LOGIN_ORIGIN = 'https://login.dingtalk.io';
39
+ const ALIBABA_INTERNAL_BASE_URL = 'https://yida-group.alibaba-inc.com';
40
+ const ALIBABA_INTERNAL_LOGIN_URL = `${ALIBABA_INTERNAL_BASE_URL}/workPlatform`;
41
+ const ENVS_CONFIG_FILE = 'yidaconnector-envs.json';
42
+
43
+ function normalizeUrlOrigin(value, fallback) {
44
+ const raw = value || fallback;
45
+ const trimmed = String(raw || '').trim();
46
+ if (!trimmed) {return fallback;}
47
+ const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
48
+ ? trimmed
49
+ : `https://${trimmed}`;
50
+ try {
51
+ return new URL(withProtocol).origin.replace(/\/+$/, '');
52
+ } catch {
53
+ return fallback;
54
+ }
55
+ }
56
+
57
+ function buildDingtalkOAuthLoginUrl(options = {}) {
58
+ const loginOrigin = normalizeUrlOrigin(options.loginOrigin || DINGTALK_LOGIN_ORIGIN, DINGTALK_LOGIN_ORIGIN);
59
+ const baseUrl = normalizeUrlOrigin(options.baseUrl || DEFAULT_BASE_URL, DEFAULT_BASE_URL);
60
+ const continueUrl = `${baseUrl}${options.continuePath || '/workPlatform'}`;
61
+ const callbackUrl = `${baseUrl}/dingtalk_sso_call_back?continue=${encodeURIComponent(continueUrl)}`;
62
+ const params = new URLSearchParams({
63
+ redirect_uri: callbackUrl,
64
+ response_type: 'code',
65
+ client_id: options.clientId || DINGTALK_OAUTH_CLIENT_ID,
66
+ scope: 'openid corpid',
67
+ lang: options.lang || 'zh_CN',
68
+ });
69
+ if (options.forceLogin) {
70
+ params.set('FEForceLogin', 'true');
71
+ }
72
+
73
+ return `${loginOrigin}/oauth2/auth?${params.toString()}`;
74
+ }
75
+
76
+ // 海外 YiDA / DingTalk International 登录入口。
77
+ // 必须满足三个条件才能让国际版钉钉扫码识别:
78
+ // 1. login origin 为 login.dingtalk.io
79
+ // 2. redirect_uri 落在 www.yidaapps.com(否则登完跳回国内域名,海外后端拿不到 session)
80
+ // 3. 追加 FEForceLogin=true,强制走国际版登录流程
81
+ const INTERNATIONAL_LOGIN_URL = buildDingtalkOAuthLoginUrl({
82
+ loginOrigin: DINGTALK_INTL_LOGIN_ORIGIN,
83
+ baseUrl: INTERNATIONAL_BASE_URL,
84
+ lang: 'en_US',
85
+ forceLogin: true,
86
+ });
87
+ const LEGACY_INTERNATIONAL_LOGIN_URL = buildDingtalkOAuthLoginUrl({
88
+ loginOrigin: DINGTALK_INTL_LOGIN_ORIGIN,
89
+ baseUrl: DEFAULT_BASE_URL,
90
+ lang: 'en_US',
91
+ });
92
+
93
+ /** 默认公有云环境配置 */
94
+ const DEFAULT_PUBLIC_ENV = {
95
+ baseUrl: DEFAULT_BASE_URL,
96
+ loginUrl: DEFAULT_LOGIN_URL,
97
+ description: '阿里云公有云宜搭',
98
+ cookieFile: 'cookies-public.json',
99
+ };
100
+
101
+ /** 阿里内网宜搭环境配置 */
102
+ const DEFAULT_ALIBABA_INTERNAL_ENV = {
103
+ baseUrl: ALIBABA_INTERNAL_BASE_URL,
104
+ loginUrl: ALIBABA_INTERNAL_LOGIN_URL,
105
+ description: '阿里内网宜搭',
106
+ cookieFile: 'cookies-alibaba.json',
107
+ };
108
+
109
+ /** 海外版 YiDA / DingTalk International 环境配置 */
110
+ const DEFAULT_INTERNATIONAL_ENV = {
111
+ baseUrl: INTERNATIONAL_BASE_URL,
112
+ loginUrl: INTERNATIONAL_LOGIN_URL,
113
+ description: '海外版 YiDA Apps / DingTalk International(www.yidaapps.com)',
114
+ cookieFile: 'cookies-intl.json',
115
+ };
116
+
117
+ const BUILTIN_ENVIRONMENTS = {
118
+ public: DEFAULT_PUBLIC_ENV,
119
+ intl: DEFAULT_INTERNATIONAL_ENV,
120
+ alibaba: DEFAULT_ALIBABA_INTERNAL_ENV,
121
+ };
122
+
123
+ const ENV_ALIASES = {
124
+ public: 'public',
125
+ aliyun: 'public',
126
+ domestic: 'public',
127
+ china: 'public',
128
+ '国内': 'public',
129
+ '国内版': 'public',
130
+ '中国': 'public',
131
+ '中国版': 'public',
132
+ '国内宜搭': 'public',
133
+ '中国宜搭': 'public',
134
+ overseas: 'intl',
135
+ oversea: 'intl',
136
+ international: 'intl',
137
+ global: 'intl',
138
+ abroad: 'intl',
139
+ intl: 'intl',
140
+ '海外': 'intl',
141
+ '海外版': 'intl',
142
+ '国际': 'intl',
143
+ '国际版': 'intl',
144
+ '全球': 'intl',
145
+ '全球版': 'intl',
146
+ '海外宜搭': 'intl',
147
+ '海外yida': 'intl',
148
+ '国际宜搭': 'intl',
149
+ '全球宜搭': 'intl',
150
+ '日本': 'intl',
151
+ '日本宜搭': 'intl',
152
+ '日本yida': 'intl',
153
+ alibaba: 'alibaba',
154
+ internal: 'alibaba',
155
+ intranet: 'alibaba',
156
+ '阿里': 'alibaba',
157
+ '阿里内网': 'alibaba',
158
+ '内网': 'alibaba',
159
+ };
160
+
161
+ const SHARED_COOKIE_DOMAINS = new Set([
162
+ 'aliwork.com',
163
+ 'yidaapps.com',
164
+ 'alibaba-inc.com',
165
+ 'yidaapps.com',
166
+ ]);
167
+
168
+ const KNOWN_YIDA_HOSTS = new Set([
169
+ 'www.aliwork.com',
170
+ 'www.yidaapps.com',
171
+ 'yida-group.alibaba-inc.com',
172
+ 'www.yidaapps.com',
173
+ ]);
174
+
175
+ function cloneBuiltinEnvironments() {
176
+ return Object.fromEntries(
177
+ Object.entries(BUILTIN_ENVIRONMENTS).map(([name, envConfig]) => [name, { ...envConfig }])
178
+ );
179
+ }
180
+
181
+ function buildDefaultEnvsConfig() {
182
+ return {
183
+ current: 'public',
184
+ environments: cloneBuiltinEnvironments(),
185
+ };
186
+ }
187
+
188
+ function resolveEnvNameAlias(envName) {
189
+ if (!envName) {return envName;}
190
+ const normalized = String(envName).trim().toLowerCase();
191
+ return ENV_ALIASES[normalized] || envName;
192
+ }
193
+
194
+ function ensureBuiltinEnvironments(config) {
195
+ if (!config.environments) { config.environments = {}; }
196
+ for (const [envName, envConfig] of Object.entries(BUILTIN_ENVIRONMENTS)) {
197
+ if (!config.environments[envName]) {
198
+ config.environments[envName] = { ...envConfig };
199
+ }
200
+ }
201
+ if (isLegacyInternationalEnv(config.environments.intl)) {
202
+ config.environments.intl = { ...DEFAULT_INTERNATIONAL_ENV };
203
+ }
204
+ return config;
205
+ }
206
+
207
+ function isLegacyInternationalEnv(envConfig) {
208
+ return !!(
209
+ envConfig &&
210
+ normalizeBaseUrl(envConfig.baseUrl, null) === DEFAULT_BASE_URL &&
211
+ (!envConfig.loginUrl || envConfig.loginUrl === LEGACY_INTERNATIONAL_LOGIN_URL) &&
212
+ (!envConfig.cookieFile || envConfig.cookieFile === 'cookies-intl.json')
213
+ );
214
+ }
215
+
216
+ function normalizeBaseUrl(value, fallback = null) {
217
+ if (!value) { return fallback; }
218
+ const trimmed = String(value).trim();
219
+ if (!trimmed) { return fallback; }
220
+ const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
221
+ ? trimmed
222
+ : `https://${trimmed}`;
223
+ try {
224
+ return new URL(withProtocol).origin.replace(/\/+$/, '');
225
+ } catch {
226
+ return fallback;
227
+ }
228
+ }
229
+
230
+ function normalizeHostname(value) {
231
+ if (!value) { return ''; }
232
+ const trimmed = String(value).trim().replace(/^\./, '').toLowerCase();
233
+ if (!trimmed) { return ''; }
234
+ try {
235
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
236
+ return new URL(trimmed).hostname.toLowerCase();
237
+ }
238
+ if (trimmed.includes('/')) {
239
+ return new URL(`https://${trimmed}`).hostname.toLowerCase();
240
+ }
241
+ } catch {
242
+ return '';
243
+ }
244
+ return trimmed;
245
+ }
246
+
247
+ function isSharedCookieDomain(hostname) {
248
+ return SHARED_COOKIE_DOMAINS.has(normalizeHostname(hostname));
249
+ }
250
+
251
+ function isYidaServiceHost(hostname) {
252
+ const host = normalizeHostname(hostname);
253
+ if (!host) { return false; }
254
+ if (KNOWN_YIDA_HOSTS.has(host)) { return true; }
255
+ if (host.endsWith('.aliwork.com') && host !== 'aliwork.com') { return true; }
256
+ if (host.endsWith('.yidaapps.com') && host !== 'yidaapps.com') { return true; }
257
+ if (host.endsWith('.alibaba-inc.com') && host !== 'alibaba-inc.com') {
258
+ return host.startsWith('yida-') || host.includes('.yida-') || host.includes('.yida.');
259
+ }
260
+ return false;
261
+ }
262
+
263
+ function isYidaAppsHost(hostname) {
264
+ const host = normalizeHostname(hostname);
265
+ return host === 'yidaapps.com' || host.endsWith('.yidaapps.com');
266
+ }
267
+
268
+ function inferEnvironmentNameFromUrl(value) {
269
+ const redirectBaseUrl = deriveBaseUrlFromDingtalkOAuthUrl(value, null);
270
+ if (redirectBaseUrl) {
271
+ return inferEnvironmentNameFromUrl(redirectBaseUrl);
272
+ }
273
+
274
+ const host = normalizeHostname(value);
275
+ if (!host) { return null; }
276
+
277
+ if (host === 'login.dingtalk.io' || host.endsWith('.dingtalk.io')) {
278
+ return 'intl';
279
+ }
280
+ if (host === 'yidaapps.com' || host.endsWith('.yidaapps.com')) {
281
+ return 'intl';
282
+ }
283
+ if (host === 'aliwork.com' || host.endsWith('.aliwork.com')) {
284
+ return 'public';
285
+ }
286
+ if (host === 'yida-group.alibaba-inc.com') {
287
+ return 'alibaba';
288
+ }
289
+ if (host.endsWith('.alibaba-inc.com') && isYidaServiceHost(host)) {
290
+ return 'alibaba';
291
+ }
292
+
293
+ return null;
294
+ }
295
+
296
+ function isDefaultWorkPlatformLoginUrl(loginUrl, baseUrl) {
297
+ const loginOrigin = normalizeBaseUrl(loginUrl, null);
298
+ const baseOrigin = normalizeBaseUrl(baseUrl, null);
299
+ if (!loginOrigin || !baseOrigin || loginOrigin !== baseOrigin) {
300
+ return false;
301
+ }
302
+
303
+ try {
304
+ const parsedUrl = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(loginUrl) ? loginUrl : `https://${loginUrl}`);
305
+ return parsedUrl.pathname.replace(/\/+$/, '') === '/workPlatform' &&
306
+ !parsedUrl.search &&
307
+ !parsedUrl.hash;
308
+ } catch {
309
+ return false;
310
+ }
311
+ }
312
+
313
+ function inferLoginUrlForBaseUrl(baseUrl, fallbackLoginUrl) {
314
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl, DEFAULT_BASE_URL);
315
+ if (isYidaAppsHost(normalizedBaseUrl)) {
316
+ return buildDingtalkOAuthLoginUrl({
317
+ loginOrigin: DINGTALK_INTL_LOGIN_ORIGIN,
318
+ baseUrl: normalizedBaseUrl,
319
+ lang: 'en_US',
320
+ forceLogin: true,
321
+ });
322
+ }
323
+ return fallbackLoginUrl || `${normalizedBaseUrl}/workPlatform`;
324
+ }
325
+
326
+ function cookieDomainToBaseUrl(domain, fallbackUrl) {
327
+ const host = normalizeHostname(domain);
328
+ if (!host || isSharedCookieDomain(host)) {
329
+ return null;
330
+ }
331
+
332
+ const fallbackOrigin = normalizeBaseUrl(fallbackUrl, null);
333
+ if (fallbackOrigin) {
334
+ const fallbackHost = normalizeHostname(fallbackOrigin);
335
+ if (host === fallbackHost) {
336
+ return fallbackOrigin;
337
+ }
338
+ }
339
+
340
+ if (isYidaServiceHost(host)) {
341
+ return `https://${host}`;
342
+ }
343
+
344
+ return null;
345
+ }
346
+
347
+ function deriveBaseUrlFromCookies(cookies = [], fallbackUrl = DEFAULT_BASE_URL) {
348
+ const fallbackOrigin = normalizeBaseUrl(fallbackUrl, DEFAULT_BASE_URL);
349
+ const cookieList = Array.isArray(cookies) ? cookies : [];
350
+ const preferredCookieNames = ['yida_user_cookie', 'tianshu_csrf_token'];
351
+
352
+ for (const cookieName of preferredCookieNames) {
353
+ const cookie = cookieList.find((item) => item && item.name === cookieName && item.domain);
354
+ const baseUrl = cookie ? cookieDomainToBaseUrl(cookie.domain, fallbackOrigin) : null;
355
+ if (baseUrl) { return baseUrl; }
356
+ }
357
+
358
+ return fallbackOrigin;
359
+ }
360
+
361
+ function deriveBaseUrlFromLoginState(cookies = [], loginUrl = DEFAULT_LOGIN_URL, currentUrl = null) {
362
+ const fallbackBaseUrl = deriveBaseUrlFromUrl(loginUrl, currentUrl);
363
+ const cookieBaseUrl = deriveBaseUrlFromCookies(cookies, fallbackBaseUrl);
364
+ const currentOrigin = normalizeBaseUrl(currentUrl, null);
365
+ const currentHost = normalizeHostname(currentOrigin);
366
+ const loginHost = normalizeHostname(normalizeBaseUrl(loginUrl, null));
367
+
368
+ if (currentOrigin && isYidaServiceHost(currentHost) && currentHost !== loginHost) {
369
+ return currentOrigin;
370
+ }
371
+
372
+ return cookieBaseUrl;
373
+ }
374
+
375
+ function deriveBaseUrlFromUrl(fallbackBaseUrl, candidateUrl) {
376
+ let fallbackOrigin = normalizeBaseUrl(fallbackBaseUrl, DEFAULT_BASE_URL);
377
+ const fallbackHost = normalizeHostname(fallbackOrigin);
378
+ if (!isYidaServiceHost(fallbackHost)) {
379
+ const callbackOrigin = deriveBaseUrlFromDingtalkOAuthUrl(fallbackBaseUrl, null);
380
+ if (callbackOrigin) {
381
+ fallbackOrigin = callbackOrigin;
382
+ }
383
+ }
384
+
385
+ const candidateOrigin = normalizeBaseUrl(candidateUrl, null);
386
+ if (!candidateOrigin) { return fallbackOrigin; }
387
+
388
+ const candidateHost = normalizeHostname(candidateOrigin);
389
+ return isYidaServiceHost(candidateHost) ? candidateOrigin : fallbackOrigin;
390
+ }
391
+
392
+ function deriveBaseUrlFromDingtalkOAuthUrl(oauthUrl, fallbackUrl) {
393
+ if (!oauthUrl) { return fallbackUrl || null; }
394
+
395
+ try {
396
+ const parsedUrl = new URL(oauthUrl);
397
+ const host = normalizeHostname(parsedUrl.hostname);
398
+ const isDingtalkLoginHost = host.endsWith('dingtalk.com') || host.endsWith('dingtalk.io');
399
+ if (!isDingtalkLoginHost || !parsedUrl.pathname.startsWith('/oauth2/')) {
400
+ return fallbackUrl || null;
401
+ }
402
+
403
+ const redirectUri = parsedUrl.searchParams.get('redirect_uri');
404
+ const redirectOrigin = normalizeBaseUrl(redirectUri, null);
405
+ if (redirectOrigin && isYidaServiceHost(normalizeHostname(redirectOrigin))) {
406
+ return redirectOrigin;
407
+ }
408
+ } catch {
409
+ // ignore malformed URLs
410
+ }
411
+
412
+ return fallbackUrl || null;
413
+ }
414
+
415
+ // ── 配置文件读写 ──────────────────────────────────────
416
+
417
+ /**
418
+ * 读取环境配置文件。
419
+ * 若文件不存在,返回含默认公有云环境的配置(不写入磁盘)。
420
+ * @param {string} [projectRoot]
421
+ * @returns {{ current: string, environments: object }}
422
+ */
423
+ function loadEnvsConfig(projectRoot) {
424
+ const root = projectRoot || findProjectRoot();
425
+ const configPath = path.join(root, '.cache', ENVS_CONFIG_FILE);
426
+
427
+ if (!fs.existsSync(configPath)) {
428
+ return buildDefaultEnvsConfig();
429
+ }
430
+
431
+ try {
432
+ const raw = fs.readFileSync(configPath, 'utf-8').trim();
433
+ const parsed = JSON.parse(raw);
434
+ // 确保内置环境始终存在,已有同名环境不覆盖,保证用户配置优先
435
+ return ensureBuiltinEnvironments(parsed);
436
+ } catch {
437
+ return buildDefaultEnvsConfig();
438
+ }
439
+ }
440
+
441
+ /**
442
+ * 写入环境配置文件。
443
+ * @param {object} config
444
+ * @param {string} [projectRoot]
445
+ */
446
+ function saveEnvsConfig(config, projectRoot) {
447
+ const root = projectRoot || findProjectRoot();
448
+ const cacheDir = path.join(root, '.cache');
449
+ const configPath = path.join(cacheDir, ENVS_CONFIG_FILE);
450
+
451
+ fs.mkdirSync(cacheDir, { recursive: true });
452
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
453
+ }
454
+
455
+ // ── 当前环境解析 ──────────────────────────────────────
456
+
457
+ /**
458
+ * 获取当前激活的环境配置对象。
459
+ * 优先级:YIDACONNECTOR_ENV 环境变量 > config.current > 'public'
460
+ * @param {string} [projectRoot]
461
+ * @returns {{ name: string, config: object }}
462
+ */
463
+ function getCurrentEnvConfig(projectRoot) {
464
+ const envsConfig = loadEnvsConfig(projectRoot);
465
+ const envName = resolveEnvNameAlias(process.env.YIDACONNECTOR_ENV || envsConfig.current || 'public');
466
+ const envConfig = envsConfig.environments[envName] || envsConfig.environments.public || DEFAULT_PUBLIC_ENV;
467
+
468
+ return { name: envName, config: envConfig };
469
+ }
470
+
471
+ // ── Cookie 文件路径 ───────────────────────────────────
472
+
473
+ /**
474
+ * 获取当前环境的 Cookie 文件绝对路径。
475
+ * 若环境配置不存在,兜底使用 cookies-public.json。
476
+ * @param {string} [projectRoot]
477
+ * @returns {string}
478
+ */
479
+ function getCookieFilePath(projectRoot) {
480
+ const root = projectRoot || findProjectRoot();
481
+ const { config: envConfig } = getCurrentEnvConfig(root);
482
+ const cookieFileName = envConfig.cookieFile || 'cookies-public.json';
483
+ return path.join(root, '.cache', cookieFileName);
484
+ }
485
+
486
+ // ── 旧版 Cookie 迁移 ──────────────────────────────────
487
+
488
+ /**
489
+ * 将旧版 cookies.json 迁移为 cookies-public.json。
490
+ * 仅在旧文件存在且新文件不存在时执行,保证向后兼容。
491
+ * @param {string} [projectRoot]
492
+ * @returns {boolean} 是否执行了迁移
493
+ */
494
+ function migrateOldCookieFile(projectRoot) {
495
+ const root = projectRoot || findProjectRoot();
496
+ const oldFile = path.join(root, '.cache', 'cookies.json');
497
+ const newFile = path.join(root, '.cache', 'cookies-public.json');
498
+
499
+ if (fs.existsSync(oldFile) && !fs.existsSync(newFile)) {
500
+ try {
501
+ fs.copyFileSync(oldFile, newFile);
502
+ // 保留旧文件作为备份,不删除,避免其他工具依赖
503
+ return true;
504
+ } catch {
505
+ return false;
506
+ }
507
+ }
508
+ return false;
509
+ }
510
+
511
+ // ── 端点解析 ──────────────────────────────────────────
512
+
513
+ /**
514
+ * 解析最终的 baseUrl,按优先级:
515
+ * 1. YIDACONNECTOR_ENDPOINT 环境变量
516
+ * 2. cookieData.base_url(登录后实际跳转域名)
517
+ * 3. 当前激活环境配置的 baseUrl
518
+ * 4. 默认公有云
519
+ * @param {object} [cookieData]
520
+ * @param {string} [projectRoot]
521
+ * @returns {string}
522
+ */
523
+ function resolveEndpoint(cookieData, projectRoot) {
524
+ // 优先级 1:环境变量强制指定
525
+ if (process.env.YIDACONNECTOR_ENDPOINT) {
526
+ return normalizeBaseUrl(process.env.YIDACONNECTOR_ENDPOINT, DEFAULT_BASE_URL);
527
+ }
528
+
529
+ // 优先级 2:登录缓存中记录的实际服务域名
530
+ if (cookieData && cookieData.base_url) {
531
+ return normalizeBaseUrl(cookieData.base_url, DEFAULT_BASE_URL);
532
+ }
533
+
534
+ // 优先级 3:当前激活环境配置
535
+ const { config: envConfig } = getCurrentEnvConfig(projectRoot);
536
+ if (envConfig.baseUrl) {
537
+ return normalizeBaseUrl(envConfig.baseUrl, DEFAULT_BASE_URL);
538
+ }
539
+
540
+ return DEFAULT_BASE_URL;
541
+ }
542
+
543
+ /**
544
+ * 解析最终的登录 URL,按优先级:
545
+ * 1. YIDACONNECTOR_LOGIN_URL 环境变量
546
+ * 2. 当前激活环境配置的 loginUrl
547
+ * 3. 默认公有云登录 URL
548
+ * @param {string} [projectRoot]
549
+ * @returns {string}
550
+ */
551
+ function resolveLoginUrl(projectRoot) {
552
+ if (process.env.YIDACONNECTOR_LOGIN_URL) {
553
+ return process.env.YIDACONNECTOR_LOGIN_URL;
554
+ }
555
+
556
+ if (process.env.YIDACONNECTOR_ENDPOINT) {
557
+ return inferLoginUrlForBaseUrl(process.env.YIDACONNECTOR_ENDPOINT);
558
+ }
559
+
560
+ const { config: envConfig } = getCurrentEnvConfig(projectRoot);
561
+ const baseUrl = normalizeBaseUrl(envConfig.baseUrl, DEFAULT_BASE_URL);
562
+ if (!envConfig.loginUrl || isDefaultWorkPlatformLoginUrl(envConfig.loginUrl, baseUrl)) {
563
+ return inferLoginUrlForBaseUrl(baseUrl, envConfig.loginUrl || DEFAULT_LOGIN_URL);
564
+ }
565
+
566
+ return envConfig.loginUrl || DEFAULT_LOGIN_URL;
567
+ }
568
+
569
+ module.exports = {
570
+ DEFAULT_BASE_URL,
571
+ DEFAULT_LOGIN_URL,
572
+ INTERNATIONAL_BASE_URL,
573
+ DINGTALK_OAUTH_CLIENT_ID,
574
+ DINGTALK_LOGIN_ORIGIN,
575
+ DINGTALK_INTL_LOGIN_ORIGIN,
576
+ INTERNATIONAL_LOGIN_URL,
577
+ ALIBABA_INTERNAL_BASE_URL,
578
+ ALIBABA_INTERNAL_LOGIN_URL,
579
+ DEFAULT_PUBLIC_ENV,
580
+ DEFAULT_INTERNATIONAL_ENV,
581
+ DEFAULT_ALIBABA_INTERNAL_ENV,
582
+ buildDingtalkOAuthLoginUrl,
583
+ resolveEnvNameAlias,
584
+ loadEnvsConfig,
585
+ saveEnvsConfig,
586
+ getCurrentEnvConfig,
587
+ getCookieFilePath,
588
+ migrateOldCookieFile,
589
+ resolveEndpoint,
590
+ resolveLoginUrl,
591
+ normalizeBaseUrl,
592
+ normalizeHostname,
593
+ isYidaServiceHost,
594
+ isYidaAppsHost,
595
+ inferEnvironmentNameFromUrl,
596
+ inferLoginUrlForBaseUrl,
597
+ deriveBaseUrlFromDingtalkOAuthUrl,
598
+ deriveBaseUrlFromCookies,
599
+ deriveBaseUrlFromLoginState,
600
+ deriveBaseUrlFromUrl,
601
+ };