openyida 2026.5.21 → 2026.5.25

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 (51) hide show
  1. package/README.md +5 -1
  2. package/bin/yida.js +7 -1
  3. package/lib/app/app-list.js +20 -1
  4. package/lib/app/check-page.js +2 -2
  5. package/lib/app/compile.js +3 -2
  6. package/lib/app/externalize-form.js +642 -0
  7. package/lib/app/import-app.js +39 -11
  8. package/lib/app/page-compat.js +258 -2
  9. package/lib/app/page-compiler.js +4 -1
  10. package/lib/app/page-linter.js +271 -0
  11. package/lib/app/publish.js +3 -2
  12. package/lib/auth/cdp-browser-login.js +7 -3
  13. package/lib/auth/login.js +2 -3
  14. package/lib/core/command-manifest.js +3 -0
  15. package/lib/core/copy.js +50 -8
  16. package/lib/core/env-manager.js +24 -16
  17. package/lib/core/locales/ar.js +7 -0
  18. package/lib/core/locales/de.js +7 -0
  19. package/lib/core/locales/en.js +7 -0
  20. package/lib/core/locales/es.js +7 -0
  21. package/lib/core/locales/fr.js +7 -0
  22. package/lib/core/locales/hi.js +7 -0
  23. package/lib/core/locales/ja.js +7 -0
  24. package/lib/core/locales/ko.js +7 -0
  25. package/lib/core/locales/pt.js +7 -0
  26. package/lib/core/locales/vi.js +7 -0
  27. package/lib/core/locales/zh-HK.js +7 -0
  28. package/lib/core/locales/zh.js +7 -0
  29. package/lib/core/utils.js +2 -2
  30. package/lib/process/configure-process.js +552 -20
  31. package/package.json +1 -1
  32. package/project/pages/src/demo-agent-chatbox.oyd.jsx +78 -3
  33. package/scripts/e2e-real/full-runner.js +257 -8
  34. package/scripts/e2e-real/skill-coverage.js +2 -2
  35. package/yida-skills/SKILL.md +1 -1
  36. package/yida-skills/skills/yida-chart/SKILL.md +1 -1
  37. package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
  38. package/yida-skills/skills/yida-custom-page/SKILL.md +7 -2
  39. package/yida-skills/skills/yida-custom-page/examples/attachment-upload.js +14 -12
  40. package/yida-skills/skills/yida-custom-page/references/attachment-upload-guide.md +3 -1
  41. package/yida-skills/skills/yida-custom-page/references/coding-guide.md +4 -0
  42. package/yida-skills/skills/yida-custom-page/references/component-jsx-guide.md +31 -22
  43. package/yida-skills/skills/yida-dashboard/SKILL.md +10 -9
  44. package/yida-skills/skills/yida-dashboard/references/interaction-patterns.md +2 -0
  45. package/yida-skills/skills/yida-dashboard/references/pitfalls.md +13 -4
  46. package/yida-skills/skills/yida-dashboard/references/structure-and-layout.md +1 -1
  47. package/yida-skills/skills/yida-ppt-slider/SKILL.md +47 -37
  48. package/yida-skills/skills/yida-ppt-slider/references/examples.md +5 -4
  49. package/yida-skills/skills/yida-process-rule/SKILL.md +93 -3
  50. package/yida-skills/skills/yida-process-rule/references/official-component-nodes.md +93 -0
  51. package/yida-skills/skills/yida-publish-page/SKILL.md +6 -4
@@ -2,9 +2,42 @@
2
2
 
3
3
  const { t } = require('../core/i18n');
4
4
  const { warn, fail, hint, success } = require('../core/chalk');
5
+ const Babel = require('@babel/standalone');
5
6
 
6
7
  const THEN_CALLBACK_LINE_LIMIT = 50;
7
8
  const CALLBACK_SCAN_LINE_LIMIT = 80;
9
+ const parser = Babel.packages.parser;
10
+ const traverse = Babel.packages.traverse.default || Babel.packages.traverse;
11
+
12
+ const PARSER_OPTIONS = {
13
+ sourceType: 'module',
14
+ plugins: [
15
+ 'jsx',
16
+ 'objectRestSpread',
17
+ 'classProperties',
18
+ 'optionalChaining',
19
+ 'nullishCoalescingOperator',
20
+ ],
21
+ };
22
+
23
+ const EVENT_NAME_ALIASES = {
24
+ onclick: 'onClick',
25
+ onchange: 'onChange',
26
+ oninput: 'onChange',
27
+ onsubmit: 'onSubmit',
28
+ onkeydown: 'onKeyDown',
29
+ onkeyup: 'onKeyUp',
30
+ onkeypress: 'onKeyPress',
31
+ onfocus: 'onFocus',
32
+ onblur: 'onBlur',
33
+ onmouseenter: 'onMouseEnter',
34
+ onmouseleave: 'onMouseLeave',
35
+ onmousedown: 'onMouseDown',
36
+ onmouseup: 'onMouseUp',
37
+ onmousemove: 'onMouseMove',
38
+ oncompositionstart: 'onCompositionStart',
39
+ oncompositionend: 'onCompositionEnd',
40
+ };
8
41
 
9
42
  function isInCommentOrString(line, matchIndex) {
10
43
  const beforeMatch = line.substring(0, matchIndex);
@@ -64,6 +97,9 @@ function pushIssue(list, line, rule, message, disableMap) {
64
97
  if (isRuleDisabled(disableMap, line, rule)) {
65
98
  return;
66
99
  }
100
+ if (list.some(issue => issue.line === line && issue.rule === rule && issue.message === message)) {
101
+ return;
102
+ }
67
103
  list.push({ line, rule, message });
68
104
  }
69
105
 
@@ -244,6 +280,240 @@ function detectEchartsRichLabelFormatter(sourceCode, warnings, disableMap) {
244
280
  }
245
281
  }
246
282
 
283
+ function getNodeLine(node) {
284
+ return node && node.loc && node.loc.start && node.loc.start.line ? node.loc.start.line : 1;
285
+ }
286
+
287
+ function getJsxElementName(nameNode) {
288
+ if (!nameNode) {
289
+ return '';
290
+ }
291
+ if (nameNode.type === 'JSXIdentifier') {
292
+ return nameNode.name;
293
+ }
294
+ if (nameNode.type === 'JSXMemberExpression') {
295
+ return getJsxElementName(nameNode.property);
296
+ }
297
+ return '';
298
+ }
299
+
300
+ function getJsxAttributeName(nameNode) {
301
+ if (!nameNode || nameNode.type !== 'JSXIdentifier') {
302
+ return '';
303
+ }
304
+ return nameNode.name;
305
+ }
306
+
307
+ function hasJsxAttribute(attrs, name) {
308
+ return attrs.some(attr => getJsxAttributeName(attr.name) === name);
309
+ }
310
+
311
+ function getStaticStringAttribute(attrs, name) {
312
+ const attr = attrs.find(item => getJsxAttributeName(item.name) === name);
313
+ if (!attr || !attr.value) {
314
+ return '';
315
+ }
316
+ if (attr.value.type === 'StringLiteral') {
317
+ return attr.value.value;
318
+ }
319
+ return '';
320
+ }
321
+
322
+ function isThisOrSelfMember(expression) {
323
+ return !!(
324
+ expression &&
325
+ expression.type === 'MemberExpression' &&
326
+ (
327
+ expression.object.type === 'ThisExpression' ||
328
+ (expression.object.type === 'Identifier' && expression.object.name === 'self')
329
+ )
330
+ );
331
+ }
332
+
333
+ function isBindThisCall(expression) {
334
+ return !!(
335
+ expression &&
336
+ expression.type === 'CallExpression' &&
337
+ expression.callee &&
338
+ expression.callee.type === 'MemberExpression' &&
339
+ expression.callee.property &&
340
+ expression.callee.property.type === 'Identifier' &&
341
+ expression.callee.property.name === 'bind' &&
342
+ expression.arguments &&
343
+ expression.arguments[0] &&
344
+ expression.arguments[0].type === 'ThisExpression'
345
+ );
346
+ }
347
+
348
+ function expressionIsBareHandlerReference(expression) {
349
+ if (!expression) {
350
+ return false;
351
+ }
352
+ if (isThisOrSelfMember(expression)) {
353
+ return true;
354
+ }
355
+ if (expression.type === 'Identifier') {
356
+ return /^handle[A-Z]|^on[A-Z]/.test(expression.name);
357
+ }
358
+ return false;
359
+ }
360
+
361
+ function blockContainsBareHandlerReference(blockStatement) {
362
+ return blockStatement.body.some((statement) => {
363
+ return statement.type === 'ExpressionStatement' &&
364
+ expressionIsBareHandlerReference(statement.expression);
365
+ });
366
+ }
367
+
368
+ function isEventAttribute(name) {
369
+ return /^on[A-Z]/.test(name || '');
370
+ }
371
+
372
+ function isLowercaseEventAttribute(name) {
373
+ return /^on[a-z]/.test(name || '');
374
+ }
375
+
376
+ function isInteractiveButton(attrs) {
377
+ if (hasJsxAttribute(attrs, 'disabled') || hasJsxAttribute(attrs, 'aria-disabled')) {
378
+ return true;
379
+ }
380
+
381
+ const buttonType = getStaticStringAttribute(attrs, 'type').toLowerCase();
382
+ if (buttonType === 'submit') {
383
+ return true;
384
+ }
385
+
386
+ return attrs.some((attr) => {
387
+ const attrName = getJsxAttributeName(attr.name);
388
+ return attrName === 'onClick' || attrName === 'onMouseDown' || attrName === 'onKeyDown';
389
+ });
390
+ }
391
+
392
+ function getCanonicalEventName(name) {
393
+ if (EVENT_NAME_ALIASES[name]) {
394
+ return EVENT_NAME_ALIASES[name];
395
+ }
396
+ if (!name || name.length <= 2) {
397
+ return name;
398
+ }
399
+ return 'on' + name.charAt(2).toUpperCase() + name.slice(3);
400
+ }
401
+
402
+ function collectAstLintIssues(sourceCode, errors, warnings, disableMap) {
403
+ let ast;
404
+ try {
405
+ ast = parser.parse(sourceCode, PARSER_OPTIONS);
406
+ } catch {
407
+ return;
408
+ }
409
+
410
+ traverse(ast, {
411
+ ExportNamedDeclaration(pathRef) {
412
+ const declaration = pathRef.node.declaration;
413
+ if (!declaration || declaration.type !== 'FunctionDeclaration' || !declaration.id) {
414
+ return;
415
+ }
416
+
417
+ const name = declaration.id.name;
418
+ if (/^didmount$/i.test(name) && name !== 'didMount') {
419
+ pushIssue(errors, getNodeLine(declaration), 'lifecycle-case', t('publish.lint_lifecycle_case', name, 'didMount'), disableMap);
420
+ }
421
+ if (/^didunmount$/i.test(name) && name !== 'didUnmount') {
422
+ pushIssue(errors, getNodeLine(declaration), 'lifecycle-case', t('publish.lint_lifecycle_case', name, 'didUnmount'), disableMap);
423
+ }
424
+ if (name === 'componentDidMount' || name === 'componentWillUnmount') {
425
+ const expected = name === 'componentDidMount' ? 'didMount' : 'didUnmount';
426
+ pushIssue(errors, getNodeLine(declaration), 'react-lifecycle-method', t('publish.lint_react_lifecycle_method', name, expected), disableMap);
427
+ }
428
+ },
429
+ ClassMethod(pathRef) {
430
+ const key = pathRef.node.key;
431
+ const name = key && key.type === 'Identifier' ? key.name : '';
432
+ if (name === 'componentDidMount' || name === 'componentWillUnmount') {
433
+ const expected = name === 'componentDidMount' ? 'didMount' : 'didUnmount';
434
+ pushIssue(errors, getNodeLine(pathRef.node), 'react-lifecycle-method', t('publish.lint_react_lifecycle_method', name, expected), disableMap);
435
+ }
436
+ },
437
+ ObjectMethod(pathRef) {
438
+ const key = pathRef.node.key;
439
+ const name = key && key.type === 'Identifier' ? key.name : '';
440
+ if (name === 'componentDidMount' || name === 'componentWillUnmount') {
441
+ const expected = name === 'componentDidMount' ? 'didMount' : 'didUnmount';
442
+ pushIssue(errors, getNodeLine(pathRef.node), 'react-lifecycle-method', t('publish.lint_react_lifecycle_method', name, expected), disableMap);
443
+ }
444
+ },
445
+ ObjectProperty(pathRef) {
446
+ if (pathRef.node.computed) {
447
+ pushIssue(errors, getNodeLine(pathRef.node), 'computed-property', t('publish.lint_computed_property'), disableMap);
448
+ }
449
+ },
450
+ JSXOpeningElement(pathRef) {
451
+ const elementName = getJsxElementName(pathRef.node.name);
452
+ const attrs = pathRef.node.attributes || [];
453
+
454
+ if (elementName === 'input' && attrs.some(attr => getJsxAttributeName(attr.name) === 'value')) {
455
+ pushIssue(errors, getNodeLine(pathRef.node), 'controlled-input', t('publish.lint_controlled_input'), disableMap);
456
+ }
457
+
458
+ if (elementName === 'select') {
459
+ pushIssue(warnings, getNodeLine(pathRef.node), 'native-select-ui', t('publish.lint_native_select_ui'), disableMap);
460
+ }
461
+
462
+ if (elementName === 'button' && !isInteractiveButton(attrs)) {
463
+ pushIssue(errors, getNodeLine(pathRef.node), 'button-missing-handler', t('publish.lint_button_missing_handler'), disableMap);
464
+ }
465
+
466
+ attrs.forEach((attr) => {
467
+ const attrName = getJsxAttributeName(attr.name);
468
+ if (!attrName) {
469
+ return;
470
+ }
471
+
472
+ if (isLowercaseEventAttribute(attrName)) {
473
+ pushIssue(errors, getNodeLine(attr), 'event-lowercase', t('publish.lint_event_lowercase', attrName, getCanonicalEventName(attrName)), disableMap);
474
+ return;
475
+ }
476
+
477
+ if (!isEventAttribute(attrName)) {
478
+ return;
479
+ }
480
+
481
+ if (!attr.value || attr.value.type !== 'JSXExpressionContainer') {
482
+ pushIssue(errors, getNodeLine(attr), 'event-call-result', t('publish.lint_event_call_result'), disableMap);
483
+ return;
484
+ }
485
+
486
+ const expression = attr.value.expression;
487
+
488
+ if (isThisOrSelfMember(expression)) {
489
+ pushIssue(errors, getNodeLine(attr), 'event-direct-method', t('publish.lint_event_direct_method'), disableMap);
490
+ return;
491
+ }
492
+
493
+ if (isBindThisCall(expression)) {
494
+ pushIssue(errors, getNodeLine(attr), 'event-bind-this', t('publish.lint_event_bind_this'), disableMap);
495
+ return;
496
+ }
497
+
498
+ if (expression && expression.type === 'CallExpression') {
499
+ pushIssue(errors, getNodeLine(attr), 'event-call-result', t('publish.lint_event_call_result'), disableMap);
500
+ return;
501
+ }
502
+
503
+ if (expression && expression.type === 'ArrowFunctionExpression') {
504
+ if (expressionIsBareHandlerReference(expression.body)) {
505
+ pushIssue(errors, getNodeLine(attr), 'event-noop-arrow', t('publish.lint_event_noop_arrow'), disableMap);
506
+ return;
507
+ }
508
+ if (expression.body && expression.body.type === 'BlockStatement' && blockContainsBareHandlerReference(expression.body)) {
509
+ pushIssue(errors, getNodeLine(attr), 'event-noop-arrow', t('publish.lint_event_noop_arrow'), disableMap);
510
+ }
511
+ }
512
+ });
513
+ },
514
+ });
515
+ }
516
+
247
517
  function lintYidaSource(sourceCode, filePath) {
248
518
  const errors = [];
249
519
  const warnings = [];
@@ -346,6 +616,7 @@ function lintYidaSource(sourceCode, filePath) {
346
616
 
347
617
  detectYidaCallsWithoutCatch(sourceCode, warnings, disableMap);
348
618
  detectEchartsRichLabelFormatter(sourceCode, warnings, disableMap);
619
+ collectAstLintIssues(sourceCode, errors, warnings, disableMap);
349
620
 
350
621
  return { errors, warnings };
351
622
  }
@@ -34,7 +34,7 @@ const { t } = require('../core/i18n');
34
34
  const { banner, step, label, success, fail, warn, info, error, result, usage, hint } = require('../core/chalk');
35
35
  const { compileSource } = require('./page-compiler');
36
36
  const { runLintCheck } = require('./page-linter');
37
- const { buildPageFile, isAuthoringPath } = require('./page-compat');
37
+ const { buildPageFile, shouldBuildPageSource } = require('./page-compat');
38
38
  const { fetchFormPageList } = require('./form-navigation');
39
39
  const { parseOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
40
40
 
@@ -868,7 +868,8 @@ async function main(argv) {
868
868
  error(t('publish.source_not_found', sourcePath));
869
869
  }
870
870
 
871
- if (compat || isAuthoringPath(sourcePath)) {
871
+ const initialSourceCode = fs.readFileSync(sourcePath, 'utf-8');
872
+ if (shouldBuildPageSource(initialSourceCode, sourcePath, { modern: compat })) {
872
873
  step(0, t('build_page.step'));
873
874
  const buildResult = buildPageFile(sourcePath, { modern: compat });
874
875
  if (!buildResult.ok) {
@@ -13,7 +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
+ const { deriveBaseUrlFromCookies, deriveBaseUrlFromLoginState } = require('../core/env-manager');
17
17
 
18
18
  function findBrowserExecutable() {
19
19
  if (process.env.OPENYIDA_CHROME_PATH && fs.existsSync(process.env.OPENYIDA_CHROME_PATH)) {
@@ -284,6 +284,10 @@ async function getCurrentPageUrl(client, fallbackUrl) {
284
284
  }
285
285
  }
286
286
 
287
+ function deriveBaseUrlFromBrowserState(cookies, loginUrl, currentPageUrl) {
288
+ return deriveBaseUrlFromLoginState(cookies, loginUrl, currentPageUrl);
289
+ }
290
+
287
291
  async function runCdpBrowserLogin(options = {}) {
288
292
  const browserPath = options.browserPath || findBrowserExecutable();
289
293
  if (!browserPath) {
@@ -332,10 +336,9 @@ async function runCdpBrowserLogin(options = {}) {
332
336
  const cookies = Array.isArray(result.cookies) ? result.cookies : [];
333
337
  if (cookies.some((cookie) => cookie.name === 'tianshu_csrf_token' && cookie.value)) {
334
338
  const currentPageUrl = await getCurrentPageUrl(client, target.url || loginUrl);
335
- const fallbackBaseUrl = deriveBaseUrlFromUrl(loginUrl, currentPageUrl);
336
339
  return {
337
340
  cookies,
338
- base_url: deriveBaseUrl(cookies, fallbackBaseUrl),
341
+ base_url: deriveBaseUrlFromBrowserState(cookies, loginUrl, currentPageUrl),
339
342
  };
340
343
  }
341
344
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -381,6 +384,7 @@ module.exports = {
381
384
  cdpBrowserLogin,
382
385
  createCdpClient,
383
386
  deriveBaseUrl,
387
+ deriveBaseUrlFromBrowserState,
384
388
  findBrowserExecutable,
385
389
  runCdpBrowserLogin,
386
390
  };
package/lib/auth/login.js CHANGED
@@ -311,7 +311,7 @@ function playwrightInteractiveLogin(loginUrl, playwrightPath) {
311
311
  const scriptContent = `
312
312
  const playwright = require(${JSON.stringify(playwrightPath)});
313
313
  const { chromium } = playwright;
314
- const { deriveBaseUrlFromCookies, deriveBaseUrlFromUrl } = require(${JSON.stringify(envManagerPath)});
314
+ const { deriveBaseUrlFromLoginState } = require(${JSON.stringify(envManagerPath)});
315
315
 
316
316
  (async () => {
317
317
  // 优先使用本地已安装的 Chrome,避免下载 Playwright 内置 Chromium
@@ -349,8 +349,7 @@ const { deriveBaseUrlFromCookies, deriveBaseUrlFromUrl } = require(${JSON.string
349
349
 
350
350
  const currentUrl = page.url();
351
351
  const cookies = await context.cookies();
352
- const fallbackBaseUrl = deriveBaseUrlFromUrl(${JSON.stringify(loginUrl)}, currentUrl);
353
- const baseUrl = deriveBaseUrlFromCookies(cookies, fallbackBaseUrl);
352
+ const baseUrl = deriveBaseUrlFromLoginState(cookies, ${JSON.stringify(loginUrl)}, currentUrl);
354
353
  await browser.close();
355
354
 
356
355
  console.log(JSON.stringify({ cookies, base_url: baseUrl }));
@@ -110,6 +110,9 @@ const COMMAND_GROUPS = [
110
110
  command('verify-short-url', ['verify-short-url'], 'verify-short-url <appType> ...', 'help.cmd_verify_url'),
111
111
  command('save-share-config', ['save-share-config'], 'save-share-config <appType> ...', 'help.cmd_save_share'),
112
112
  command('get-page-config', ['get-page-config'], 'get-page-config <appType> <formUuid>', 'help.cmd_get_page_config'),
113
+ command('externalize-form', ['externalize-form'], 'externalize-form <appType> <formUuid> [--schema-file file]', 'help.cmd_externalize_form', {
114
+ output: 'json|markdown',
115
+ }),
113
116
  ],
114
117
  },
115
118
  {
package/lib/core/copy.js CHANGED
@@ -81,6 +81,30 @@ function mergeCopyDir(sourceDir, destDir) {
81
81
  return copiedCount;
82
82
  }
83
83
 
84
+ function resolveExistingPath(targetPath) {
85
+ try {
86
+ return fs.realpathSync(targetPath);
87
+ } catch {
88
+ return path.resolve(targetPath);
89
+ }
90
+ }
91
+
92
+ function isSameDirectory(a, b) {
93
+ const pathA = resolveExistingPath(a);
94
+ const pathB = resolveExistingPath(b);
95
+ if (process.platform === 'win32') {
96
+ return pathA.toLowerCase() === pathB.toLowerCase();
97
+ }
98
+ return pathA === pathB;
99
+ }
100
+
101
+ function clearDirectoryContents(dir) {
102
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
103
+ for (const entry of entries) {
104
+ fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true });
105
+ }
106
+ }
107
+
84
108
  /**
85
109
  * 强制复制目录:先清空目标目录,再完整复制。
86
110
  * @returns {number} 复制的文件数量
@@ -89,7 +113,11 @@ function forceCopyDir(sourceDir, destDir) {
89
113
  if (!fs.existsSync(sourceDir)) {return 0;}
90
114
 
91
115
  if (fs.existsSync(destDir)) {
92
- fs.rmSync(destDir, { recursive: true, force: true });
116
+ if (isSameDirectory(destDir, process.cwd())) {
117
+ clearDirectoryContents(destDir);
118
+ } else {
119
+ fs.rmSync(destDir, { recursive: true, force: true });
120
+ }
93
121
  console.log(t('copy.cleared', destDir));
94
122
  }
95
123
 
@@ -182,9 +210,11 @@ function createSymlink(sourceDir, destLink) {
182
210
  * @param {string|null} activeToolName
183
211
  * @param {string|null} activeProjectRoot
184
212
  * @param {Array} envResults
213
+ * @param {object} [options]
214
+ * @param {boolean} [options.allowCurrentDir=false] - 未检测到活跃 AI 工具时,是否允许使用当前目录
185
215
  * @returns {string} 目标根目录路径
186
216
  */
187
- function resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults) {
217
+ function resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults, options = {}) {
188
218
  const activeResult = envResults.find((r) => r.displayName === activeToolName);
189
219
  const isWukong = activeResult && activeResult.dirName === '.real';
190
220
 
@@ -201,6 +231,10 @@ function resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults) {
201
231
  return process.cwd();
202
232
  }
203
233
 
234
+ if (options.allowCurrentDir) {
235
+ return process.cwd();
236
+ }
237
+
204
238
  // 未检测到活跃工具
205
239
  warn(t('copy.no_ai_tool'));
206
240
  envResults.forEach((r) => {
@@ -223,13 +257,13 @@ function copyItem(label, sourceDir, destDir, isForce) {
223
257
 
224
258
  /**
225
259
  * 执行 copy 命令主逻辑。
260
+ * @param {string[]} [args=process.argv.slice(3)] 命令参数
226
261
  */
227
- function run() {
262
+ function run(args = process.argv.slice(3)) {
228
263
  const { c, sep, banner, info, success, hint, label, fail: chalkFail, listItem } = require('./chalk');
229
264
 
230
265
  banner(t('copy.title'), { stderr: false });
231
266
 
232
- const args = process.argv.slice(3);
233
267
  const isForce = args.includes('--force');
234
268
  const wantsSkills = args.includes('-skills');
235
269
  const wantsProject = args.includes('-project');
@@ -250,7 +284,9 @@ function run() {
250
284
  const { activeToolName, activeProjectRoot, results: envResults } = detectEnvironment();
251
285
  const activeEnvResult = envResults.find((r) => r.isActive);
252
286
  const isWukong = activeEnvResult && activeEnvResult.dirName === '.real';
253
- const destBase = resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults);
287
+ const destBase = resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults, {
288
+ allowCurrentDir: isForce,
289
+ });
254
290
  label('Target', destBase, { stderr: false });
255
291
  if (isForce) {
256
292
  warn(t('copy.force_mode'), false);
@@ -345,8 +381,8 @@ function run() {
345
381
  }
346
382
 
347
383
  // 4. 打印汇总
348
- const copyCount = results.filter(r => r.type === 'copy').reduce((sum, r) => sum + r.count, 0);
349
- const linkCount = results.filter(r => r.type === 'symlink').length;
384
+ const copyCount = results.filter((r) => r.type === 'copy').reduce((sum, r) => sum + r.count, 0);
385
+ const linkCount = results.filter((r) => r.type === 'symlink').length;
350
386
  console.log('');
351
387
  console.log(` ${sep()}`);
352
388
  success(t('copy.done'), false);
@@ -369,4 +405,10 @@ function run() {
369
405
  console.log(` ${sep()}\n`);
370
406
  }
371
407
 
372
- module.exports = { run };
408
+ module.exports = {
409
+ run,
410
+ _internal: {
411
+ forceCopyDir,
412
+ resolveDestBaseFromEnv,
413
+ },
414
+ };
@@ -8,9 +8,9 @@
8
8
  *
9
9
  * 优先级(高 → 低):
10
10
  * 1. 环境变量 OPENYIDA_ENDPOINT
11
- * 2. 环境变量 OPENYIDA_ENV 指定的环境配置
12
- * 3. 当前激活的环境配置(openyida-envs.json current 字段)
13
- * 4. cookieData.base_url(历史兼容)
11
+ * 2. cookieData.base_url(登录后实际跳转域名)
12
+ * 3. 环境变量 OPENYIDA_ENV 指定的环境配置
13
+ * 4. 当前激活的环境配置(openyida-envs.json current 字段)
14
14
  * 5. 默认公有云 https://www.aliwork.com
15
15
  *
16
16
  * 导出函数:
@@ -21,6 +21,7 @@
21
21
  * migrateOldCookieFile() - 迁移旧版 cookies.json → cookies-public.json
22
22
  * resolveEndpoint() - 解析最终 baseUrl(含完整优先级)
23
23
  * resolveLoginUrl() - 解析最终登录 URL
24
+ * deriveBaseUrlFromLoginState() - 从 Cookie + 浏览器当前 URL 解析实际 baseUrl
24
25
  */
25
26
 
26
27
  'use strict';
@@ -329,6 +330,20 @@ function deriveBaseUrlFromCookies(cookies = [], fallbackUrl = DEFAULT_BASE_URL)
329
330
  return fallbackOrigin;
330
331
  }
331
332
 
333
+ function deriveBaseUrlFromLoginState(cookies = [], loginUrl = DEFAULT_LOGIN_URL, currentUrl = null) {
334
+ const fallbackBaseUrl = deriveBaseUrlFromUrl(loginUrl, currentUrl);
335
+ const cookieBaseUrl = deriveBaseUrlFromCookies(cookies, fallbackBaseUrl);
336
+ const currentOrigin = normalizeBaseUrl(currentUrl, null);
337
+ const currentHost = normalizeHostname(currentOrigin);
338
+ const loginHost = normalizeHostname(normalizeBaseUrl(loginUrl, null));
339
+
340
+ if (currentOrigin && isYidaServiceHost(currentHost) && currentHost !== loginHost) {
341
+ return currentOrigin;
342
+ }
343
+
344
+ return cookieBaseUrl;
345
+ }
346
+
332
347
  function deriveBaseUrlFromUrl(fallbackBaseUrl, candidateUrl) {
333
348
  let fallbackOrigin = normalizeBaseUrl(fallbackBaseUrl, DEFAULT_BASE_URL);
334
349
  const fallbackHost = normalizeHostname(fallbackOrigin);
@@ -470,8 +485,8 @@ function migrateOldCookieFile(projectRoot) {
470
485
  /**
471
486
  * 解析最终的 baseUrl,按优先级:
472
487
  * 1. OPENYIDA_ENDPOINT 环境变量
473
- * 2. 当前激活环境配置的 baseUrl
474
- * 3. cookieData.base_url(历史兼容)
488
+ * 2. cookieData.base_url(登录后实际跳转域名)
489
+ * 3. 当前激活环境配置的 baseUrl
475
490
  * 4. 默认公有云
476
491
  * @param {object} [cookieData]
477
492
  * @param {string} [projectRoot]
@@ -483,21 +498,13 @@ function resolveEndpoint(cookieData, projectRoot) {
483
498
  return normalizeBaseUrl(process.env.OPENYIDA_ENDPOINT, DEFAULT_BASE_URL);
484
499
  }
485
500
 
486
- // 优先级 2:当前激活环境配置
487
- const { config: envConfig } = getCurrentEnvConfig(projectRoot);
488
- // 只有当环境配置不是默认公有云,或者没有 cookieData 时才使用环境配置
489
- // 这样可以兼容:用户没有配置多环境时,仍从 Cookie 中提取专属域名
490
- const isDefaultPublic = envConfig.baseUrl === DEFAULT_BASE_URL;
491
- if (!isDefaultPublic && envConfig.baseUrl) {
492
- return normalizeBaseUrl(envConfig.baseUrl, DEFAULT_BASE_URL);
493
- }
494
-
495
- // 优先级 3:从 Cookie 历史提取(兼容专属域名)
501
+ // 优先级 2:登录缓存中记录的实际服务域名
496
502
  if (cookieData && cookieData.base_url) {
497
503
  return normalizeBaseUrl(cookieData.base_url, DEFAULT_BASE_URL);
498
504
  }
499
505
 
500
- // 优先级 4:环境配置(公有云默认)
506
+ // 优先级 3:当前激活环境配置
507
+ const { config: envConfig } = getCurrentEnvConfig(projectRoot);
501
508
  if (envConfig.baseUrl) {
502
509
  return normalizeBaseUrl(envConfig.baseUrl, DEFAULT_BASE_URL);
503
510
  }
@@ -560,5 +567,6 @@ module.exports = {
560
567
  inferLoginUrlForBaseUrl,
561
568
  deriveBaseUrlFromDingtalkOAuthUrl,
562
569
  deriveBaseUrlFromCookies,
570
+ deriveBaseUrlFromLoginState,
563
571
  deriveBaseUrlFromUrl,
564
572
  };
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'التحقق من الرابط المختصر',
54
54
  cmd_save_share: 'حفظ إعدادات الوصول العام / المشاركة',
55
55
  cmd_get_page_config: 'استعلام إعدادات الوصول العام للصفحة',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'التقارير',
57
58
  cmd_create_report: 'إنشاء تقرير Yida',
58
59
  cmd_append_chart: 'إضافة رسم بياني إلى تقرير موجود',
@@ -451,6 +452,12 @@ module.exports = {
451
452
  lint_jsx_extension: 'This .js file contains JSX; prefer .jsx for source files. The compiled output is still .js',
452
453
  lint_event_direct_method: 'Event binding passes this.xxx directly and will lose this; use (e) => { this.xxx(e); }',
453
454
  lint_event_bind_this: 'Event binding uses .bind(this); use (e) => { this.xxx(e); } instead',
455
+ lint_lifecycle_case: 'Detected lifecycle export {0}; Yida only recognizes {1} (case-sensitive)',
456
+ lint_react_lifecycle_method: 'Detected React lifecycle {0}; export {1} for Yida custom pages',
457
+ lint_event_lowercase: 'Detected lowercase event prop {0}; React/Yida will not bind it. Use {1}',
458
+ lint_event_call_result: 'Event binding invokes a function or is not an expression handler; use (e) => { self.xxx(e); }',
459
+ lint_event_noop_arrow: 'Event arrow function references a handler without calling it; use (e) => { self.xxx(e); }',
460
+ lint_button_missing_handler: 'Visible <button> has no onClick/onMouseDown/onKeyDown or disabled state; bind an event handler or use span/div for static labels',
454
461
  lint_array_callback_function: '.{0}(function ...) may lose this in callbacks; use .{0}((item) => ...) instead',
455
462
  lint_foreach_callback_function: '.forEach(function ...) may lose this in callbacks; prefer .forEach((item) => ...)',
456
463
  lint_controlled_input: 'input uses controlled value mode. Yida pages should use defaultValue + onChange to update _customState',
@@ -53,6 +53,7 @@ module.exports = {
53
53
  cmd_verify_url: 'Kurz-URL überprüfen',
54
54
  cmd_save_share: 'Öffentlichen Zugang / Freigabe speichern',
55
55
  cmd_get_page_config: 'Öffentliche Zugangskonfiguration abfragen',
56
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
56
57
  group_report: 'Berichte',
57
58
  cmd_create_report: 'Yida-Bericht erstellen',
58
59
  cmd_append_chart: 'Diagramm zu bestehendem Bericht hinzufügen',
@@ -451,6 +452,12 @@ module.exports = {
451
452
  lint_jsx_extension: 'This .js file contains JSX; prefer .jsx for source files. The compiled output is still .js',
452
453
  lint_event_direct_method: 'Event binding passes this.xxx directly and will lose this; use (e) => { this.xxx(e); }',
453
454
  lint_event_bind_this: 'Event binding uses .bind(this); use (e) => { this.xxx(e); } instead',
455
+ lint_lifecycle_case: 'Detected lifecycle export {0}; Yida only recognizes {1} (case-sensitive)',
456
+ lint_react_lifecycle_method: 'Detected React lifecycle {0}; export {1} for Yida custom pages',
457
+ lint_event_lowercase: 'Detected lowercase event prop {0}; React/Yida will not bind it. Use {1}',
458
+ lint_event_call_result: 'Event binding invokes a function or is not an expression handler; use (e) => { self.xxx(e); }',
459
+ lint_event_noop_arrow: 'Event arrow function references a handler without calling it; use (e) => { self.xxx(e); }',
460
+ lint_button_missing_handler: 'Visible <button> has no onClick/onMouseDown/onKeyDown or disabled state; bind an event handler or use span/div for static labels',
454
461
  lint_array_callback_function: '.{0}(function ...) may lose this in callbacks; use .{0}((item) => ...) instead',
455
462
  lint_foreach_callback_function: '.forEach(function ...) may lose this in callbacks; prefer .forEach((item) => ...)',
456
463
  lint_controlled_input: 'input uses controlled value mode. Yida pages should use defaultValue + onChange to update _customState',
@@ -56,6 +56,7 @@ module.exports = {
56
56
  cmd_verify_url: 'Verify short URL',
57
57
  cmd_save_share: 'Save public access / share config',
58
58
  cmd_get_page_config: 'Query page public access config',
59
+ cmd_externalize_form: 'Plan external access-safe mirror fields',
59
60
  group_report: 'Reports',
60
61
  cmd_create_report: 'Create a Yida report',
61
62
  cmd_append_chart: 'Append chart to existing report',
@@ -949,6 +950,12 @@ Examples:
949
950
  lint_jsx_extension: 'This .js file contains JSX; prefer .jsx for source files. The compiled output is still .js',
950
951
  lint_event_direct_method: 'Event binding passes this.xxx directly and will lose this; use (e) => { this.xxx(e); }',
951
952
  lint_event_bind_this: 'Event binding uses .bind(this); use (e) => { this.xxx(e); } instead',
953
+ lint_lifecycle_case: 'Detected lifecycle export {0}; Yida only recognizes {1} (case-sensitive)',
954
+ lint_react_lifecycle_method: 'Detected React lifecycle {0}; export {1} for Yida custom pages',
955
+ lint_event_lowercase: 'Detected lowercase event prop {0}; React/Yida will not bind it. Use {1}',
956
+ lint_event_call_result: 'Event binding invokes a function or is not an expression handler; use (e) => { self.xxx(e); }',
957
+ lint_event_noop_arrow: 'Event arrow function references a handler without calling it; use (e) => { self.xxx(e); }',
958
+ lint_button_missing_handler: 'Visible <button> has no onClick/onMouseDown/onKeyDown or disabled state; bind an event handler or use span/div for static labels',
952
959
  lint_array_callback_function: '.{0}(function ...) may lose this in callbacks; use .{0}((item) => ...) instead',
953
960
  lint_foreach_callback_function: '.forEach(function ...) may lose this in callbacks; prefer .forEach((item) => ...)',
954
961
  lint_controlled_input: 'input uses controlled value mode. Yida pages should use defaultValue + onChange to update _customState',