openyida 2026.5.12 → 2026.5.13-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
@@ -316,6 +316,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
316
316
  | `openyida integration enable <appType> <formUuid> <processCode>` | Enable an automation flow |
317
317
  | `openyida integration disable <appType> <formUuid> <processCode>` | Disable an automation flow |
318
318
  | `openyida dws <command> [args]` | Access DingTalk CLI capabilities such as contacts, calendar, todo, and approval |
319
+ | `openyida dingtalk-link <url> [--target fullScreen] [--legacy-scheme] [--json]` | Generate DingTalk AppLink URLs for opening pages in DingTalk; use `--legacy-scheme` only when old `dingtalk://` links are required |
319
320
 
320
321
  ### Utilities
321
322
 
package/bin/yida.js CHANGED
@@ -262,6 +262,15 @@ function shouldUseAgentLogin(cliArgs) {
262
262
  return isAgentConversationEnvironment();
263
263
  }
264
264
 
265
+ function shouldUsePlaywrightFallbackInAgentLogin() {
266
+ const { detectActiveTool } = require('../lib/core/utils');
267
+ const activeTool = detectActiveTool();
268
+ if (activeTool && activeTool.tool === 'wukong') {
269
+ return true;
270
+ }
271
+ return process.env.OPENYIDA_AGENT_PLAYWRIGHT_FALLBACK === '1';
272
+ }
273
+
265
274
  function shouldUseCodexQrLogin(cliArgs) {
266
275
  if (cliArgs.includes('--codex-qr') || cliArgs.includes('--agent-qr')) {return true;}
267
276
  return false;
@@ -392,7 +401,9 @@ async function main() {
392
401
  printLoginResult(cachedResult);
393
402
  } else {
394
403
  const { interactiveLogin } = require('../lib/auth/login');
395
- const browserResult = interactiveLogin({ playwrightFallback: false });
404
+ const browserResult = interactiveLogin({
405
+ playwrightFallback: shouldUsePlaywrightFallbackInAgentLogin(),
406
+ });
396
407
  if (browserResult) {
397
408
  printLoginResult(browserResult);
398
409
  } else {
@@ -879,6 +890,13 @@ async function main() {
879
890
  await runDws(args);
880
891
  break;
881
892
  }
893
+
894
+ case 'dingtalk-link': {
895
+ const { run: runDingTalkLink } = require('../lib/dingtalk/dingtalk-link');
896
+ await runDingTalkLink(args);
897
+ break;
898
+ }
899
+
882
900
  case 'export-conversation': {
883
901
  const { exportConversation } = require('../lib/conversation/export-conversation');
884
902
  // 解析选项
@@ -13,6 +13,7 @@ const path = require('path');
13
13
  const net = require('net');
14
14
  const crypto = require('crypto');
15
15
  const { execFileSync, spawn } = require('child_process');
16
+ const { deriveBaseUrlFromCookies, deriveBaseUrlFromUrl } = require('../core/env-manager');
16
17
 
17
18
  function findBrowserExecutable() {
18
19
  if (process.env.OPENYIDA_CHROME_PATH && fs.existsSync(process.env.OPENYIDA_CHROME_PATH)) {
@@ -267,23 +268,19 @@ function createCdpClient(wsUrl) {
267
268
  }
268
269
 
269
270
  function deriveBaseUrl(cookies, fallbackUrl) {
270
- const yidaCookie = cookies.find((cookie) =>
271
- cookie.name === 'yida_user_cookie' &&
272
- cookie.domain &&
273
- cookie.domain.includes('aliwork.com'));
274
- if (yidaCookie) {
275
- return `https://${String(yidaCookie.domain).replace(/^\./, '')}`;
276
- }
277
-
278
- const csrfCookie = cookies.find((cookie) => cookie.name === 'tianshu_csrf_token' && cookie.domain);
279
- if (csrfCookie && csrfCookie.domain !== '.aliwork.com') {
280
- return `https://${String(csrfCookie.domain).replace(/^\./, '')}`;
281
- }
271
+ return deriveBaseUrlFromCookies(cookies, fallbackUrl);
272
+ }
282
273
 
274
+ async function getCurrentPageUrl(client, fallbackUrl) {
283
275
  try {
284
- return new URL(fallbackUrl).origin;
276
+ const result = await client.send('Runtime.evaluate', {
277
+ expression: 'window.location.href',
278
+ returnByValue: true,
279
+ });
280
+ const value = result && result.result && result.result.value;
281
+ return typeof value === 'string' && value ? value : fallbackUrl;
285
282
  } catch {
286
- return 'https://www.aliwork.com';
283
+ return fallbackUrl;
287
284
  }
288
285
  }
289
286
 
@@ -334,9 +331,11 @@ async function runCdpBrowserLogin(options = {}) {
334
331
  .catch(() => client.send('Storage.getCookies'));
335
332
  const cookies = Array.isArray(result.cookies) ? result.cookies : [];
336
333
  if (cookies.some((cookie) => cookie.name === 'tianshu_csrf_token' && cookie.value)) {
334
+ const currentPageUrl = await getCurrentPageUrl(client, target.url || loginUrl);
335
+ const fallbackBaseUrl = deriveBaseUrlFromUrl(loginUrl, currentPageUrl);
337
336
  return {
338
337
  cookies,
339
- base_url: deriveBaseUrl(cookies, loginUrl),
338
+ base_url: deriveBaseUrl(cookies, fallbackBaseUrl),
340
339
  };
341
340
  }
342
341
  await new Promise((resolve) => setTimeout(resolve, 2000));
package/lib/auth/login.js CHANGED
@@ -281,6 +281,10 @@ function interactiveLogin(options = {}) {
281
281
  return null;
282
282
  }
283
283
 
284
+ if (process.env.OPENYIDA_DISABLE_PLAYWRIGHT_LOGIN === '1') {
285
+ return null;
286
+ }
287
+
284
288
  const playwrightPath = getPlaywrightPath();
285
289
  if (!playwrightPath) {
286
290
  const { error: chalkError3 } = require('../core/chalk');
@@ -295,13 +299,14 @@ function playwrightInteractiveLogin(loginUrl, playwrightPath) {
295
299
  const { info: chalkInfo, label: chalkLabel2 } = require('../core/chalk');
296
300
  chalkInfo(t('login.browser_opening'));
297
301
  chalkLabel2('URL', loginUrl);
302
+ const envManagerPath = path.join(__dirname, '..', 'core', 'env-manager.js');
298
303
 
299
304
  // 通过子进程运行异步 playwright 逻辑,避免顶层 await 兼容性问题
300
305
  // 使用 require.resolve 获取的绝对路径,确保临时脚本能找到 playwright
301
306
  const scriptContent = `
302
307
  const playwright = require(${JSON.stringify(playwrightPath)});
303
308
  const { chromium } = playwright;
304
- const { URL } = require('url');
309
+ const { deriveBaseUrlFromCookies, deriveBaseUrlFromUrl } = require(${JSON.stringify(envManagerPath)});
305
310
 
306
311
  (async () => {
307
312
  // 优先使用本地已安装的 Chrome,避免下载 Playwright 内置 Chromium
@@ -339,19 +344,8 @@ const { URL } = require('url');
339
344
 
340
345
  const currentUrl = page.url();
341
346
  const cookies = await context.cookies();
342
- // 优先从 yida_user_cookie domain 提取 base_url,兼容专属域名
343
- const csrfCookie = cookies.find(c => c.name === 'tianshu_csrf_token');
344
- let baseUrl;
345
- if (csrfCookie && csrfCookie.domain && csrfCookie.domain !== '.aliwork.com') {
346
- baseUrl = 'https://' + csrfCookie.domain.replace(/^\\./, '');
347
- } else {
348
- try { baseUrl = new URL(currentUrl).origin; } catch { baseUrl = 'https://www.aliwork.com'; }
349
- }
350
- // 再用 yida_user_cookie 的 domain 覆盖,它是最可靠的来源
351
- const yidaCookie = cookies.find(c => c.name === 'yida_user_cookie');
352
- if (yidaCookie && yidaCookie.domain && yidaCookie.domain.includes('aliwork.com')) {
353
- baseUrl = 'https://' + yidaCookie.domain.replace(/^\\./, '');
354
- }
347
+ const fallbackBaseUrl = deriveBaseUrlFromUrl(${JSON.stringify(loginUrl)}, currentUrl);
348
+ const baseUrl = deriveBaseUrlFromCookies(cookies, fallbackBaseUrl);
355
349
  await browser.close();
356
350
 
357
351
  console.log(JSON.stringify({ cookies, base_url: baseUrl }));
package/lib/auth/org.js CHANGED
@@ -23,13 +23,11 @@
23
23
 
24
24
  const https = require('https');
25
25
  const http = require('http');
26
- const { extractInfoFromCookies } = require('../core/utils');
26
+ const { extractInfoFromCookies, resolveBaseUrl } = require('../core/utils');
27
27
  const { saveCookieCache } = require('./login');
28
28
  const { saveAuthConfig, loadAuthConfig } = require('./auth');
29
29
  const { t } = require('../core/i18n');
30
30
 
31
- const DEFAULT_BASE_URL = 'https://www.aliwork.com';
32
-
33
31
  // ── HTTP 请求工具 ─────────────────────────────────────
34
32
 
35
33
  /**
@@ -208,19 +206,21 @@ async function switchOrganization(targetCorpId, cookieData) {
208
206
  }
209
207
 
210
208
  try {
209
+ const baseUrl = resolveBaseUrl(cookieData);
210
+
211
211
  // Step 1: 发起切换请求
212
212
  step(1, t('org.step1'), false);
213
- const step1Url = `${DEFAULT_BASE_URL}/start.html?corpid=${targetCorpId}&switchCorp=true`;
213
+ const step1Url = `${baseUrl}/start.html?corpid=${targetCorpId}&switchCorp=true`;
214
214
  const step1Result = await httpGetWithCookies(step1Url, cookies, false);
215
215
 
216
216
  // Step 2: 确认切换
217
217
  step(2, t('org.step2'), false);
218
- const step2Url = `${DEFAULT_BASE_URL}/start.html?corpid=${targetCorpId}&`;
218
+ const step2Url = `${baseUrl}/start.html?corpid=${targetCorpId}&`;
219
219
  const step2Result = await httpGetWithCookies(step2Url, step1Result.cookies, false);
220
220
 
221
221
  // Step 3-4: 获取新 Cookie(跟随重定向)
222
222
  step(3, t('org.step3'), false);
223
- const step3Url = `https://www.aliwork.com/start.html?corpid=${targetCorpId}&`;
223
+ const step3Url = `${baseUrl}/start.html?corpid=${targetCorpId}&`;
224
224
  const step3Result = await httpGetWithCookies(step3Url, step2Result.cookies, false);
225
225
 
226
226
  // 处理重定向
@@ -235,7 +235,7 @@ async function switchOrganization(targetCorpId, cookieData) {
235
235
 
236
236
  // 构建完整 URL
237
237
  if (!currentUrl.startsWith('http')) {
238
- currentUrl = `https://www.aliwork.com${currentUrl}`;
238
+ currentUrl = new URL(currentUrl, baseUrl).toString();
239
239
  }
240
240
 
241
241
  const redirectResult = await httpGetWithCookies(currentUrl, finalCookies, false);
@@ -261,7 +261,7 @@ async function switchOrganization(targetCorpId, cookieData) {
261
261
  }
262
262
 
263
263
  // 保存新的 Cookie
264
- const newBaseUrl = 'https://www.aliwork.com';
264
+ const newBaseUrl = baseUrl;
265
265
  saveCookieCache(finalCookies, newBaseUrl);
266
266
 
267
267
  // 更新 auth 配置
@@ -24,7 +24,7 @@ const { saveCookieCache } = require('./login');
24
24
  const { t } = require('../core/i18n');
25
25
  const { warn } = require('../core/chalk');
26
26
 
27
- const { resolveLoginUrl, resolveEndpoint } = require('../core/env-manager');
27
+ const { resolveLoginUrl, resolveEndpoint, deriveBaseUrlFromUrl } = require('../core/env-manager');
28
28
 
29
29
  function shellQuote(value) {
30
30
  return `'${String(value).replace(/'/g, "'\\''")}'`;
@@ -511,16 +511,7 @@ async function resolveCorpSelection(corpList, options = {}) {
511
511
  }
512
512
 
513
513
  function deriveAliworkBaseUrl(fallbackBaseUrl, finalUrl) {
514
- try {
515
- const parsedUrl = new URL(finalUrl);
516
- if (parsedUrl.hostname.endsWith('aliwork.com')) {
517
- return parsedUrl.origin;
518
- }
519
- } catch {
520
- // ignore
521
- }
522
-
523
- return fallbackBaseUrl;
514
+ return deriveBaseUrlFromUrl(fallbackBaseUrl, finalUrl);
524
515
  }
525
516
 
526
517
  /**
@@ -530,7 +521,8 @@ function deriveAliworkBaseUrl(fallbackBaseUrl, finalUrl) {
530
521
  * @returns {Promise<{ cookieHeader: string, loginPageUrl: string }>}
531
522
  */
532
523
  async function fetchInitialSession(baseUrl, options = {}) {
533
- const loginPageUrl = options.loginUrl || resolveLoginUrl() || `${baseUrl}/workPlatform`;
524
+ const loginPageUrl = options.loginUrl ||
525
+ (options.baseUrl ? `${baseUrl}/workPlatform` : (resolveLoginUrl() || `${baseUrl}/workPlatform`));
534
526
  const response = await fetchGetFollowRedirects(loginPageUrl, {
535
527
  cookieHeader: options.cookieHeader || '',
536
528
  });
@@ -1443,5 +1435,6 @@ module.exports = {
1443
1435
  buildAgentQrResponseMarkdown,
1444
1436
  buildNeedQrScanResult,
1445
1437
  getTargetCorpId,
1438
+ deriveAliworkBaseUrl,
1446
1439
  },
1447
1440
  };
@@ -126,6 +126,10 @@ const COMMAND_GROUPS = [
126
126
  commands: [
127
127
  command('integration.create', ['integration', 'create'], 'integration create <appType> ...', 'help.cmd_integration'),
128
128
  command('dws', ['dws'], 'dws <command> [args]', 'help.cmd_dws'),
129
+ command('dingtalk-link', ['dingtalk-link'], 'dingtalk-link <url> [--target fullScreen] [--legacy-scheme] [--json]', 'help.cmd_dingtalk_link', {
130
+ requiresLogin: false,
131
+ output: 'text|json',
132
+ }),
129
133
  ],
130
134
  },
131
135
  {
@@ -286,8 +286,15 @@ class EnvironmentChecker {
286
286
  async checkNetwork() {
287
287
  try {
288
288
  const https = require('https');
289
+ const http = require('http');
290
+ const { loadCookieData } = require('./utils');
291
+ const { resolveEndpoint } = require('./env-manager');
292
+ const cookieData = loadCookieData(this.projectRoot);
293
+ const baseUrl = resolveEndpoint(cookieData, this.projectRoot);
294
+ const parsedUrl = new URL(baseUrl);
295
+ const requestModule = parsedUrl.protocol === 'https:' ? https : http;
289
296
  await new Promise((resolve, reject) => {
290
- const request = https.get('https://www.aliwork.com', { timeout: 5000 }, (response) => {
297
+ const request = requestModule.get(baseUrl, { timeout: 5000 }, (response) => {
291
298
  resolve(response.statusCode);
292
299
  });
293
300
  request.on('error', reject);
@@ -298,18 +305,18 @@ class EnvironmentChecker {
298
305
  });
299
306
  return {
300
307
  id: 'env-network',
301
- label: '网络连通性(aliwork.com)',
308
+ label: `网络连通性(${parsedUrl.hostname})`,
302
309
  passed: true,
303
310
  severity: Severity.INFO,
304
311
  fixType: null,
305
312
  };
306
- } catch {
313
+ } catch (err) {
307
314
  return {
308
315
  id: 'env-network',
309
316
  label: '网络连通性检测',
310
317
  passed: false,
311
318
  severity: Severity.WARNING,
312
- message: '无法连接 aliwork.com,请检查网络',
319
+ message: `无法连接当前宜搭地址,请检查网络或环境配置:${err.message}`,
313
320
  fixType: null,
314
321
  };
315
322
  }
@@ -31,6 +31,8 @@ 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 ALIBABA_INTERNAL_BASE_URL = 'https://yida-group.alibaba-inc.com';
35
+ const ALIBABA_INTERNAL_LOGIN_URL = `${ALIBABA_INTERNAL_BASE_URL}/workPlatform`;
34
36
  const ENVS_CONFIG_FILE = 'openyida-envs.json';
35
37
 
36
38
  /** 默认公有云环境配置 */
@@ -41,6 +43,142 @@ const DEFAULT_PUBLIC_ENV = {
41
43
  cookieFile: 'cookies-public.json',
42
44
  };
43
45
 
46
+ /** 阿里内网宜搭环境配置 */
47
+ const DEFAULT_ALIBABA_INTERNAL_ENV = {
48
+ baseUrl: ALIBABA_INTERNAL_BASE_URL,
49
+ loginUrl: ALIBABA_INTERNAL_LOGIN_URL,
50
+ description: '阿里内网宜搭',
51
+ cookieFile: 'cookies-alibaba.json',
52
+ };
53
+
54
+ const BUILTIN_ENVIRONMENTS = {
55
+ public: DEFAULT_PUBLIC_ENV,
56
+ alibaba: DEFAULT_ALIBABA_INTERNAL_ENV,
57
+ };
58
+
59
+ const SHARED_COOKIE_DOMAINS = new Set([
60
+ 'aliwork.com',
61
+ 'alibaba-inc.com',
62
+ ]);
63
+
64
+ const KNOWN_YIDA_HOSTS = new Set([
65
+ 'www.aliwork.com',
66
+ 'yida-group.alibaba-inc.com',
67
+ ]);
68
+
69
+ function cloneBuiltinEnvironments() {
70
+ return Object.fromEntries(
71
+ Object.entries(BUILTIN_ENVIRONMENTS).map(([name, envConfig]) => [name, { ...envConfig }])
72
+ );
73
+ }
74
+
75
+ function buildDefaultEnvsConfig() {
76
+ return {
77
+ current: 'public',
78
+ environments: cloneBuiltinEnvironments(),
79
+ };
80
+ }
81
+
82
+ function ensureBuiltinEnvironments(config) {
83
+ if (!config.environments) { config.environments = {}; }
84
+ for (const [envName, envConfig] of Object.entries(BUILTIN_ENVIRONMENTS)) {
85
+ if (!config.environments[envName]) {
86
+ config.environments[envName] = { ...envConfig };
87
+ }
88
+ }
89
+ return config;
90
+ }
91
+
92
+ function normalizeBaseUrl(value, fallback = null) {
93
+ if (!value) { return fallback; }
94
+ const trimmed = String(value).trim();
95
+ if (!trimmed) { return fallback; }
96
+ const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
97
+ ? trimmed
98
+ : `https://${trimmed}`;
99
+ try {
100
+ return new URL(withProtocol).origin.replace(/\/+$/, '');
101
+ } catch {
102
+ return fallback;
103
+ }
104
+ }
105
+
106
+ function normalizeHostname(value) {
107
+ if (!value) { return ''; }
108
+ const trimmed = String(value).trim().replace(/^\./, '').toLowerCase();
109
+ if (!trimmed) { return ''; }
110
+ try {
111
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
112
+ return new URL(trimmed).hostname.toLowerCase();
113
+ }
114
+ if (trimmed.includes('/')) {
115
+ return new URL(`https://${trimmed}`).hostname.toLowerCase();
116
+ }
117
+ } catch {
118
+ return '';
119
+ }
120
+ return trimmed;
121
+ }
122
+
123
+ function isSharedCookieDomain(hostname) {
124
+ return SHARED_COOKIE_DOMAINS.has(normalizeHostname(hostname));
125
+ }
126
+
127
+ function isYidaServiceHost(hostname) {
128
+ const host = normalizeHostname(hostname);
129
+ if (!host) { return false; }
130
+ if (KNOWN_YIDA_HOSTS.has(host)) { return true; }
131
+ if (host.endsWith('.aliwork.com') && host !== 'aliwork.com') { return true; }
132
+ if (host.endsWith('.alibaba-inc.com') && host !== 'alibaba-inc.com') {
133
+ return host.startsWith('yida-') || host.includes('.yida-') || host.includes('.yida.');
134
+ }
135
+ return false;
136
+ }
137
+
138
+ function cookieDomainToBaseUrl(domain, fallbackUrl) {
139
+ const host = normalizeHostname(domain);
140
+ if (!host || isSharedCookieDomain(host)) {
141
+ return null;
142
+ }
143
+
144
+ const fallbackOrigin = normalizeBaseUrl(fallbackUrl, null);
145
+ if (fallbackOrigin) {
146
+ const fallbackHost = normalizeHostname(fallbackOrigin);
147
+ if (host === fallbackHost) {
148
+ return fallbackOrigin;
149
+ }
150
+ }
151
+
152
+ if (isYidaServiceHost(host)) {
153
+ return `https://${host}`;
154
+ }
155
+
156
+ return null;
157
+ }
158
+
159
+ function deriveBaseUrlFromCookies(cookies = [], fallbackUrl = DEFAULT_BASE_URL) {
160
+ const fallbackOrigin = normalizeBaseUrl(fallbackUrl, DEFAULT_BASE_URL);
161
+ const cookieList = Array.isArray(cookies) ? cookies : [];
162
+ const preferredCookieNames = ['yida_user_cookie', 'tianshu_csrf_token'];
163
+
164
+ for (const cookieName of preferredCookieNames) {
165
+ const cookie = cookieList.find((item) => item && item.name === cookieName && item.domain);
166
+ const baseUrl = cookie ? cookieDomainToBaseUrl(cookie.domain, fallbackOrigin) : null;
167
+ if (baseUrl) { return baseUrl; }
168
+ }
169
+
170
+ return fallbackOrigin;
171
+ }
172
+
173
+ function deriveBaseUrlFromUrl(fallbackBaseUrl, candidateUrl) {
174
+ const fallbackOrigin = normalizeBaseUrl(fallbackBaseUrl, DEFAULT_BASE_URL);
175
+ const candidateOrigin = normalizeBaseUrl(candidateUrl, null);
176
+ if (!candidateOrigin) { return fallbackOrigin; }
177
+
178
+ const candidateHost = normalizeHostname(candidateOrigin);
179
+ return isYidaServiceHost(candidateHost) ? candidateOrigin : fallbackOrigin;
180
+ }
181
+
44
182
  // ── 配置文件读写 ──────────────────────────────────────
45
183
 
46
184
  /**
@@ -54,26 +192,16 @@ function loadEnvsConfig(projectRoot) {
54
192
  const configPath = path.join(root, '.cache', ENVS_CONFIG_FILE);
55
193
 
56
194
  if (!fs.existsSync(configPath)) {
57
- return {
58
- current: 'public',
59
- environments: { public: { ...DEFAULT_PUBLIC_ENV } },
60
- };
195
+ return buildDefaultEnvsConfig();
61
196
  }
62
197
 
63
198
  try {
64
199
  const raw = fs.readFileSync(configPath, 'utf-8').trim();
65
200
  const parsed = JSON.parse(raw);
66
- // 确保 public 环境始终存在
67
- if (!parsed.environments) { parsed.environments = {}; }
68
- if (!parsed.environments.public) {
69
- parsed.environments.public = { ...DEFAULT_PUBLIC_ENV };
70
- }
71
- return parsed;
201
+ // 确保内置环境始终存在,已有同名环境不覆盖,保证用户配置优先
202
+ return ensureBuiltinEnvironments(parsed);
72
203
  } catch {
73
- return {
74
- current: 'public',
75
- environments: { public: { ...DEFAULT_PUBLIC_ENV } },
76
- };
204
+ return buildDefaultEnvsConfig();
77
205
  }
78
206
  }
79
207
 
@@ -162,7 +290,7 @@ function migrateOldCookieFile(projectRoot) {
162
290
  function resolveEndpoint(cookieData, projectRoot) {
163
291
  // 优先级 1:环境变量强制指定
164
292
  if (process.env.OPENYIDA_ENDPOINT) {
165
- return process.env.OPENYIDA_ENDPOINT.replace(/\/+$/, '');
293
+ return normalizeBaseUrl(process.env.OPENYIDA_ENDPOINT, DEFAULT_BASE_URL);
166
294
  }
167
295
 
168
296
  // 优先级 2:当前激活环境配置
@@ -171,17 +299,17 @@ function resolveEndpoint(cookieData, projectRoot) {
171
299
  // 这样可以兼容:用户没有配置多环境时,仍从 Cookie 中提取专属域名
172
300
  const isDefaultPublic = envConfig.baseUrl === DEFAULT_BASE_URL;
173
301
  if (!isDefaultPublic && envConfig.baseUrl) {
174
- return envConfig.baseUrl.replace(/\/+$/, '');
302
+ return normalizeBaseUrl(envConfig.baseUrl, DEFAULT_BASE_URL);
175
303
  }
176
304
 
177
305
  // 优先级 3:从 Cookie 历史提取(兼容专属域名)
178
306
  if (cookieData && cookieData.base_url) {
179
- return cookieData.base_url.replace(/\/+$/, '');
307
+ return normalizeBaseUrl(cookieData.base_url, DEFAULT_BASE_URL);
180
308
  }
181
309
 
182
310
  // 优先级 4:环境配置(公有云默认)
183
311
  if (envConfig.baseUrl) {
184
- return envConfig.baseUrl.replace(/\/+$/, '');
312
+ return normalizeBaseUrl(envConfig.baseUrl, DEFAULT_BASE_URL);
185
313
  }
186
314
 
187
315
  return DEFAULT_BASE_URL;
@@ -207,7 +335,10 @@ function resolveLoginUrl(projectRoot) {
207
335
  module.exports = {
208
336
  DEFAULT_BASE_URL,
209
337
  DEFAULT_LOGIN_URL,
338
+ ALIBABA_INTERNAL_BASE_URL,
339
+ ALIBABA_INTERNAL_LOGIN_URL,
210
340
  DEFAULT_PUBLIC_ENV,
341
+ DEFAULT_ALIBABA_INTERNAL_ENV,
211
342
  loadEnvsConfig,
212
343
  saveEnvsConfig,
213
344
  getCurrentEnvConfig,
@@ -215,4 +346,9 @@ module.exports = {
215
346
  migrateOldCookieFile,
216
347
  resolveEndpoint,
217
348
  resolveLoginUrl,
349
+ normalizeBaseUrl,
350
+ normalizeHostname,
351
+ isYidaServiceHost,
352
+ deriveBaseUrlFromCookies,
353
+ deriveBaseUrlFromUrl,
218
354
  };
@@ -68,6 +68,7 @@ module.exports = {
68
68
  group_integration: 'التكامل & DingTalk',
69
69
  cmd_integration: 'إنشاء تدفق أتمتة التكامل',
70
70
  cmd_dws: 'DingTalk CLI (جهات الاتصال/التقويم/المهام/الموافقة إلخ)',
71
+ cmd_dingtalk_link: 'إنشاء روابط DingTalk AppLink / dingtalk:// القديمة',
71
72
  group_utility: 'الأدوات',
72
73
  cmd_commands: 'Output machine-readable command manifest',
73
74
  cmd_a2a: 'Start local read-only A2A adapter or print Agent Card',
@@ -68,6 +68,7 @@ module.exports = {
68
68
  group_integration: 'Integration & DingTalk',
69
69
  cmd_integration: 'Integrations-Automatisierungsflow erstellen',
70
70
  cmd_dws: 'DingTalk CLI (Kontakte/Kalender/Aufgaben/Genehmigung etc.)',
71
+ cmd_dingtalk_link: 'DingTalk AppLink / alte dingtalk:// Seitenlinks erzeugen',
71
72
  group_utility: 'Werkzeuge',
72
73
  cmd_commands: 'Maschinenlesbares Command Manifest ausgeben',
73
74
  cmd_a2a: 'Lokalen schreibgeschützten A2A-Adapter starten oder Agent Card ausgeben',
@@ -71,6 +71,7 @@ module.exports = {
71
71
  group_integration: 'Integration & DingTalk',
72
72
  cmd_integration: 'Create integration automation flow',
73
73
  cmd_dws: 'DingTalk CLI (contacts/calendar/todo/approval etc.)',
74
+ cmd_dingtalk_link: 'Generate DingTalk AppLink / legacy dingtalk:// page links',
74
75
  group_utility: 'Utility',
75
76
  cmd_commands: 'Output machine-readable command manifest',
76
77
  cmd_a2a: 'Start local read-only A2A adapter or print Agent Card',
@@ -68,6 +68,7 @@ module.exports = {
68
68
  group_integration: 'Integración & DingTalk',
69
69
  cmd_integration: 'Crear flujo de automatización',
70
70
  cmd_dws: 'DingTalk CLI (contactos/calendario/tareas/aprobación etc.)',
71
+ cmd_dingtalk_link: 'Generar enlaces DingTalk AppLink / dingtalk:// heredados',
71
72
  group_utility: 'Utilidades',
72
73
  cmd_commands: 'Emitir manifiesto de comandos legible por máquina',
73
74
  cmd_a2a: 'Iniciar adaptador A2A local de solo lectura o imprimir Agent Card',
@@ -68,6 +68,7 @@ module.exports = {
68
68
  group_integration: 'Intégration & DingTalk',
69
69
  cmd_integration: 'Créer un flux d\'automatisation',
70
70
  cmd_dws: 'DingTalk CLI (contacts/calendrier/tâches/approbation etc.)',
71
+ cmd_dingtalk_link: 'Générer des liens DingTalk AppLink / dingtalk:// historiques',
71
72
  group_utility: 'Utilitaires',
72
73
  cmd_commands: 'Afficher le manifeste des commandes lisible par machine',
73
74
  cmd_a2a: 'Démarrer l’adaptateur A2A local en lecture seule ou afficher l’Agent Card',
@@ -68,6 +68,7 @@ module.exports = {
68
68
  group_integration: 'एकीकरण & DingTalk',
69
69
  cmd_integration: 'एकीकरण स्वचालन फ्लो बनाएं',
70
70
  cmd_dws: 'DingTalk CLI (संपर्क/कैलेंडर/कार्य/अनुमोदन आदि)',
71
+ cmd_dingtalk_link: 'DingTalk AppLink / legacy dingtalk:// पेज लिंक बनाएं',
71
72
  group_utility: 'उपकरण',
72
73
  cmd_commands: 'Output machine-readable command manifest',
73
74
  cmd_a2a: 'Start local read-only A2A adapter or print Agent Card',
@@ -70,6 +70,7 @@ module.exports = {
70
70
  group_integration: '統合 & DingTalk',
71
71
  cmd_integration: '統合自動化フローを作成',
72
72
  cmd_dws: 'DingTalk CLI(連絡先/カレンダー/ToDo/承認等)',
73
+ cmd_dingtalk_link: 'DingTalk AppLink / 旧 dingtalk:// ページリンクを生成',
73
74
  group_utility: 'ユーティリティ',
74
75
  cmd_commands: '機械可読コマンド manifest を出力',
75
76
  cmd_a2a: 'ローカル読み取り専用 A2A Adapter を起動、または Agent Card を出力',
@@ -68,6 +68,7 @@ module.exports = {
68
68
  group_integration: '통합 & DingTalk',
69
69
  cmd_integration: '통합 자동화 플로우 생성',
70
70
  cmd_dws: 'DingTalk CLI (연락처/캘린더/할일/승인 등)',
71
+ cmd_dingtalk_link: 'DingTalk AppLink / 기존 dingtalk:// 페이지 링크 생성',
71
72
  group_utility: '유틸리티',
72
73
  cmd_commands: 'Output machine-readable command manifest',
73
74
  cmd_a2a: 'Start local read-only A2A adapter or print Agent Card',
@@ -68,6 +68,7 @@ module.exports = {
68
68
  group_integration: 'Integração & DingTalk',
69
69
  cmd_integration: 'Criar fluxo de automação',
70
70
  cmd_dws: 'DingTalk CLI (contatos/calendário/tarefas/aprovação etc.)',
71
+ cmd_dingtalk_link: 'Gerar links DingTalk AppLink / dingtalk:// legados',
71
72
  group_utility: 'Utilitários',
72
73
  cmd_commands: 'Emitir manifesto de comandos legível por máquina',
73
74
  cmd_a2a: 'Iniciar adaptador A2A local somente leitura ou imprimir Agent Card',
@@ -68,6 +68,7 @@ module.exports = {
68
68
  group_integration: 'Tích hợp & DingTalk',
69
69
  cmd_integration: 'Tạo luồng tự động hóa tích hợp',
70
70
  cmd_dws: 'DingTalk CLI (danh bạ/lịch/việc cần làm/phê duyệt v.v.)',
71
+ cmd_dingtalk_link: 'Tạo liên kết DingTalk AppLink / dingtalk:// cũ',
71
72
  group_utility: 'Tiện ích',
72
73
  cmd_commands: 'Output machine-readable command manifest',
73
74
  cmd_a2a: 'Start local read-only A2A adapter or print Agent Card',
@@ -70,6 +70,7 @@ module.exports = {
70
70
  group_integration: '整合 & 釘釘',
71
71
  cmd_integration: '建立整合自動化邏輯流',
72
72
  cmd_dws: '釘釘 CLI(通訊錄/日曆/待辦/審批等)',
73
+ cmd_dingtalk_link: '生成釘釘 AppLink / 相容 dingtalk:// 跳轉連結',
73
74
  group_utility: '工具',
74
75
  cmd_commands: '輸出機器可讀命令清單',
75
76
  cmd_a2a: '啟動本機唯讀 A2A Adapter 或輸出 Agent Card',
@@ -71,6 +71,7 @@ module.exports = {
71
71
  group_integration: '集成 & 钉钉',
72
72
  cmd_integration: '创建集成自动化逻辑流',
73
73
  cmd_dws: '钉钉 CLI(通讯录/日历/待办/审批等)',
74
+ cmd_dingtalk_link: '生成钉钉 AppLink / 兼容 dingtalk:// 跳转链接',
74
75
  group_utility: '工具',
75
76
  cmd_commands: '输出机器可读命令清单',
76
77
  cmd_a2a: '启动本地只读 A2A Adapter 或输出 Agent Card',
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+
3
+ const { warn } = require('../core/chalk');
4
+
5
+ const APPLINK_PAGE_LINK = 'https://applink.dingtalk.com/page/link';
6
+ const LEGACY_PAGE_LINK = 'dingtalk://dingtalkclient/page/link';
7
+ const DEFAULT_TARGET = 'fullScreen';
8
+
9
+ function parseArgs(args) {
10
+ const options = {
11
+ url: null,
12
+ target: DEFAULT_TARGET,
13
+ legacyScheme: false,
14
+ json: false,
15
+ help: false,
16
+ };
17
+
18
+ for (let i = 0; i < args.length; i += 1) {
19
+ const arg = args[i];
20
+ if (arg === '--help' || arg === '-h') {
21
+ options.help = true;
22
+ } else if (arg === '--url') {
23
+ options.url = args[++i];
24
+ } else if (arg === '--target') {
25
+ options.target = args[++i];
26
+ } else if (arg === '--legacy-scheme') {
27
+ options.legacyScheme = true;
28
+ } else if (arg === '--json') {
29
+ options.json = true;
30
+ } else if (!arg.startsWith('--') && !options.url) {
31
+ options.url = arg;
32
+ }
33
+ }
34
+
35
+ return options;
36
+ }
37
+
38
+ function isDingTalkPageLink(value) {
39
+ try {
40
+ const parsed = new URL(value);
41
+ if (parsed.protocol === 'dingtalk:' && parsed.hostname === 'dingtalkclient') {
42
+ return parsed.pathname === '/page/link';
43
+ }
44
+ return parsed.protocol === 'https:' && parsed.hostname === 'applink.dingtalk.com' && parsed.pathname === '/page/link';
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ function extractPageUrl(value) {
51
+ if (!isDingTalkPageLink(value)) {
52
+ return { pageUrl: value, target: null };
53
+ }
54
+
55
+ const parsed = new URL(value);
56
+ return {
57
+ pageUrl: parsed.searchParams.get('url') || '',
58
+ target: parsed.searchParams.get('target'),
59
+ };
60
+ }
61
+
62
+ function assertHttpUrl(value) {
63
+ let parsed;
64
+ try {
65
+ parsed = new URL(value);
66
+ } catch {
67
+ throw new Error('DingTalk page links require an absolute http(s) URL.');
68
+ }
69
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
70
+ throw new Error('DingTalk page links require an absolute http(s) URL.');
71
+ }
72
+ return parsed.toString();
73
+ }
74
+
75
+ function buildDingTalkPageLink(input) {
76
+ const options = {
77
+ url: input && input.url,
78
+ target: input && input.target,
79
+ legacyScheme: !!(input && input.legacyScheme),
80
+ };
81
+ const hasTargetOption = !!(input && Object.prototype.hasOwnProperty.call(input, 'target'));
82
+
83
+ if (!options.url) {
84
+ throw new Error('Missing URL. Usage: openyida dingtalk-link <url>');
85
+ }
86
+
87
+ const extracted = extractPageUrl(options.url);
88
+ const pageUrl = assertHttpUrl(extracted.pageUrl);
89
+ const target = hasTargetOption ? options.target : (extracted.target || DEFAULT_TARGET);
90
+ const base = options.legacyScheme ? LEGACY_PAGE_LINK : APPLINK_PAGE_LINK;
91
+ const link = new URL(base);
92
+ link.searchParams.set('url', pageUrl);
93
+ if (target) {
94
+ link.searchParams.set('target', target);
95
+ }
96
+ return link.toString();
97
+ }
98
+
99
+ function printHelp() {
100
+ console.log('Usage: openyida dingtalk-link <url> [--target fullScreen] [--legacy-scheme] [--json]');
101
+ console.log('');
102
+ console.log('Generate DingTalk AppLink URLs for opening web pages in DingTalk.');
103
+ console.log('AppLink is the default because dingtalk:// may be claimed by DingTalk variants such as dedicated DingTalk clients.');
104
+ console.log('');
105
+ console.log('Examples:');
106
+ console.log(' openyida dingtalk-link https://attend.dingtalk.com/attend/index.html');
107
+ console.log(' openyida dingtalk-link "dingtalk://dingtalkclient/page/link?url=https%3A%2F%2Fexample.com"');
108
+ console.log(' openyida dingtalk-link https://example.com --legacy-scheme');
109
+ }
110
+
111
+ async function run(args) {
112
+ const options = parseArgs(args);
113
+ if (options.help) {
114
+ printHelp();
115
+ return;
116
+ }
117
+
118
+ try {
119
+ const link = buildDingTalkPageLink(options);
120
+ if (options.json) {
121
+ console.log(JSON.stringify({
122
+ url: link,
123
+ kind: options.legacyScheme ? 'legacy-scheme' : 'applink',
124
+ target: options.target || null,
125
+ }, null, 2));
126
+ return;
127
+ }
128
+ console.log(link);
129
+ } catch (error) {
130
+ warn(error.message);
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ module.exports = {
136
+ APPLINK_PAGE_LINK,
137
+ LEGACY_PAGE_LINK,
138
+ buildDingTalkPageLink,
139
+ extractPageUrl,
140
+ run,
141
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.5.12",
3
+ "version": "2026.5.13-beta.0",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",