openyida 2026.7.7 → 2026.7.9

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 (88) hide show
  1. package/README.md +4 -2
  2. package/bin/yida.js +4 -4
  3. package/lib/app/canvas-compile.js +303 -0
  4. package/lib/app/publish.js +190 -39
  5. package/lib/auth/auth.js +11 -3
  6. package/lib/auth/login.js +34 -15
  7. package/lib/core/command-manifest.js +1 -1
  8. package/lib/core/env.js +19 -4
  9. package/lib/core/locales/ar.js +6 -2
  10. package/lib/core/locales/de.js +6 -2
  11. package/lib/core/locales/en.js +7 -3
  12. package/lib/core/locales/es.js +6 -2
  13. package/lib/core/locales/fr.js +6 -2
  14. package/lib/core/locales/hi.js +6 -2
  15. package/lib/core/locales/ja.js +7 -3
  16. package/lib/core/locales/ko.js +6 -2
  17. package/lib/core/locales/pt.js +6 -2
  18. package/lib/core/locales/vi.js +6 -2
  19. package/lib/core/locales/zh-HK.js +7 -3
  20. package/lib/core/locales/zh.js +7 -3
  21. package/lib/core/utils.js +195 -15
  22. package/lib/samples/yida-custom-page/custom-page-template.js +6 -6
  23. package/lib/samples/yida-custom-page/design-tokens.js +14 -12
  24. package/package.json +1 -1
  25. package/scripts/e2e-real/skill-coverage.js +4 -0
  26. package/scripts/validate-ci.sh +6 -0
  27. package/scripts/validate-package-size.js +12 -1
  28. package/yida-skills/SKILL.md +21 -6
  29. package/yida-skills/references/development-rules.md +13 -9
  30. package/yida-skills/references/field-and-url-reference.md +14 -1
  31. package/yida-skills/skills/large-file-write/SKILL.md +12 -10
  32. package/yida-skills/skills/large-file-write/references/write-patterns.md +13 -15
  33. package/yida-skills/skills/large-file-write/scripts/write.js +10 -10
  34. package/yida-skills/skills/yida-app/SKILL.md +15 -8
  35. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +115 -0
  36. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-authoring-examples.md +163 -0
  37. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-design-system.md +148 -0
  38. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +115 -0
  39. package/yida-skills/skills/yida-canvas-custom-page/references/dependencies-and-cdn.md +45 -0
  40. package/yida-skills/skills/yida-canvas-custom-page/references/employeefield-verification.md +68 -0
  41. package/yida-skills/skills/yida-canvas-upgrade/SKILL.md +99 -0
  42. package/yida-skills/skills/yida-canvas-upgrade/references/migration-examples.md +69 -0
  43. package/yida-skills/skills/yida-chart/SKILL.md +2 -2
  44. package/yida-skills/skills/yida-chart/references/echarts-bindding-guide.md +5 -6
  45. package/yida-skills/skills/yida-chart/references/examples.md +2 -2
  46. package/yida-skills/skills/yida-connector/SKILL.md +4 -0
  47. package/yida-skills/skills/yida-connector-safe-actions/SKILL.md +7 -4
  48. package/yida-skills/skills/yida-create-form-page/SKILL.md +11 -8
  49. package/yida-skills/skills/yida-create-process/SKILL.md +10 -4
  50. package/yida-skills/skills/yida-custom-page/SKILL.md +17 -6
  51. package/yida-skills/skills/yida-custom-page/examples/attachment-upload.js +1 -1
  52. package/yida-skills/skills/yida-custom-page/references/coding-guide.md +3 -3
  53. package/yida-skills/skills/yida-custom-page/references/component-jsx-guide.md +6 -3
  54. package/yida-skills/skills/yida-custom-page/references/design-system.md +30 -13
  55. package/yida-skills/skills/yida-data-management/SKILL.md +14 -11
  56. package/yida-skills/skills/yida-data-source-connectors/SKILL.md +3 -1
  57. package/yida-skills/skills/yida-flash-note-to-prd/references/examples.md +3 -2
  58. package/yida-skills/skills/yida-formula/references/examples.md +4 -4
  59. package/yida-skills/skills/yida-formula-evaluate/SKILL.md +4 -2
  60. package/yida-skills/skills/yida-get-schema/SKILL.md +14 -5
  61. package/yida-skills/skills/yida-i18n/SKILL.md +4 -2
  62. package/yida-skills/skills/yida-integration/SKILL.md +4 -1
  63. package/yida-skills/skills/yida-integration/references/examples.md +2 -2
  64. package/yida-skills/skills/yida-nav-shell/SKILL.md +119 -0
  65. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +302 -0
  66. package/yida-skills/skills/yida-openyida-publish-guard/SKILL.md +2 -2
  67. package/yida-skills/skills/yida-page-config/SKILL.md +1 -0
  68. package/yida-skills/skills/yida-page-uiux/SKILL.md +68 -0
  69. package/yida-skills/skills/yida-page-uiux/references/scenes/dashboard.md +128 -0
  70. package/yida-skills/skills/yida-page-uiux/references/scenes/detail.md +79 -0
  71. package/yida-skills/skills/yida-page-uiux/references/scenes/landing.md +101 -0
  72. package/yida-skills/skills/yida-page-uiux/references/scenes/list.md +78 -0
  73. package/yida-skills/skills/yida-page-uiux/references/scenes/workbench.md +74 -0
  74. package/yida-skills/skills/yida-page-uiux/references/visual-decision-engine.md +151 -0
  75. package/yida-skills/skills/yida-page-uiux/workflow/output-decision-block.md +36 -0
  76. package/yida-skills/skills/yida-page-uiux/workflow/step-0-nav-shape.md +39 -0
  77. package/yida-skills/skills/yida-page-uiux/workflow/step-1-page-type.md +25 -0
  78. package/yida-skills/skills/yida-page-uiux/workflow/step-2-intent-decode.md +25 -0
  79. package/yida-skills/skills/yida-page-uiux/workflow/step-3-scene-routing.md +29 -0
  80. package/yida-skills/skills/yida-page-uiux/workflow/step-4-visual-decision.md +32 -0
  81. package/yida-skills/skills/yida-page-uiux/workflow/step-5-icon-and-assets.md +36 -0
  82. package/yida-skills/skills/yida-page-uiux/workflow/step-6-deai-selfcheck.md +37 -0
  83. package/yida-skills/skills/yida-process-rule/SKILL.md +4 -1
  84. package/yida-skills/skills/yida-process-rule/references/examples.md +12 -12
  85. package/yida-skills/skills/yida-publish-page/SKILL.md +7 -3
  86. package/yida-skills/skills/yida-report/SKILL.md +5 -2
  87. package/yida-skills/skills/yida-report/references/examples.md +4 -4
  88. package/yida-skills/skills/yida-table-form/references/examples.md +2 -2
@@ -250,7 +250,7 @@ openyida - 宜搭命令行工具
250
250
  generate_page_example: '示例: openyida generate-page product-homepage --brand-name OpenKuma --brand-initials OK --output pages/src/home.oyd.jsx --compile',
251
251
  build_page_usage: '用法: openyida build-page <源文件路径> [--output pages/build/page.yida.jsx|--write] [--json]',
252
252
  build_page_example: '示例: openyida build-page pages/src/dashboard.oyd.jsx --output pages/build/dashboard.yida.jsx',
253
- publish_usage: '用法: openyida publish <源文件路径> <appType> <formUuid> [--health-check]',
253
+ publish_usage: '用法: openyida publish <源文件路径> <appType> <formUuid> [--health-check] [--canvas]',
254
254
  publish_example: '示例: openyida publish pages/src/home.oyd.jsx APP_XXX FORM-XXX --health-check',
255
255
  formula_usage: '用法: openyida formula evaluate <公式或文件> [--schema schema.json] [--json] [--strict]',
256
256
  formula_example: '示例: openyida formula evaluate \'IF(GT(#{numberField_total}, 100), "高", "低")\' --schema .cache/schema.json',
@@ -1066,7 +1066,7 @@ openyida - 宜搭命令行工具
1066
1066
  lint_array_callback_function: '.{0}(function ...) 会导致回调内 this 丢失,请改为 .{0}((item) => ...)',
1067
1067
  lint_foreach_callback_function: '.forEach(function ...) 可能导致回调内 this 丢失,建议改为 .forEach((item) => ...)',
1068
1068
  lint_controlled_input: 'input 使用了 value 受控模式,宜搭页面应使用 defaultValue + onChange 写入 _customState',
1069
- lint_native_select_ui: '检测到原生 <select>,样式不一致且移动端兼容性差。请改用宜搭内置的 SelectField 组件(this.$(fieldId) 或 <SelectField>),或用 div+button+onClick 实现自定义下拉',
1069
+ lint_native_select_ui: '检测到原生 <select>,样式不一致且移动端兼容性差。普通自定义页请用 div+button+onClick 实现自定义下拉;只有 Code Canvas 场景在确认依赖映射可用后,才使用宜搭字段组件。',
1070
1070
  lint_iframe_self_navigation: '宜搭自定义页面运行在 iframe 中,跳转宜搭页面时请使用 target="_top"/target="_blank" 或 window.top.location,避免页面套娃',
1071
1071
  lint_page_size_limit: 'pageSize={0} 超过宜搭接口上限 100,请改为 100 或更小',
1072
1072
  lint_page_size_recommend: 'pageSize={0} 偏大,推荐使用 50(性能最佳,最大 100),优先选择 10/20/50',
@@ -1113,6 +1113,10 @@ openyida - 宜搭命令行工具
1113
1113
  building_schema: '[4/4] 构建 Schema...',
1114
1114
  formula_prefix_fixed: ' 🔧 已自动修正 {0} 处子表公式前缀引用',
1115
1115
  schema_built: ' ✅ Schema 构建完成!',
1116
+ step_canvas_compile: '\n📦 Step 1: 编译 Code Canvas 源码\n',
1117
+ canvas_compiling: ' 🎨 本地编译 Code Canvas 源码...',
1118
+ canvas_compile_done: ' ✅ Code Canvas 编译完成!',
1119
+ canvas_compile_failed: ' ❌ Code Canvas 编译失败:{0}',
1116
1120
  step_login: '\n🔑 Step 2: 读取登录态',
1117
1121
  step_publish: '\n📤 Step 3: 发布 Schema\n',
1118
1122
  resend_save_csrf: ' 🔄 重新发送 saveFormSchema 请求(csrf_token 已刷新)...',
@@ -1142,7 +1146,7 @@ openyida - 宜搭命令行工具
1142
1146
  exception: '\n❌ 发布异常: {0}',
1143
1147
  error: '\n❌ 发布异常: {0}',
1144
1148
  source_not_found: '❌ 源文件不存在:{0}',
1145
- usage: '用法: openyida publish <源文件路径> <appType> <formUuid> [--health-check]',
1149
+ usage: '用法: openyida publish <源文件路径> <appType> <formUuid> [--health-check] [--canvas]',
1146
1150
  example: '示例:openyida publish pages/src/xxx.js APP_XXX FORM-XXX --health-check',
1147
1151
  },
1148
1152
 
package/lib/core/utils.js CHANGED
@@ -264,6 +264,43 @@ function isInjectedAuthMode(env = process.env) {
264
264
  return ['1', 'true', 'yes', 'on'].includes(authEnabled);
265
265
  }
266
266
 
267
+ function hasEnvCookieAuth(env = process.env) {
268
+ return !!String(env.OPENYIDA_COOKIE_B64 || '').trim();
269
+ }
270
+
271
+ function isEnvAuthMode(env = process.env) {
272
+ return isInjectedAuthMode(env) || hasEnvCookieAuth(env);
273
+ }
274
+
275
+ let lastEnvAuthError = null;
276
+
277
+ function getLastEnvAuthError() {
278
+ return lastEnvAuthError;
279
+ }
280
+
281
+ function setLastEnvAuthError(code, failureReason, message) {
282
+ lastEnvAuthError = {
283
+ code,
284
+ failure_reason: failureReason,
285
+ message,
286
+ authMode: 'env',
287
+ };
288
+ return lastEnvAuthError;
289
+ }
290
+
291
+ function normalizeEnvBaseUrl(baseUrl, fallbackBaseUrl) {
292
+ const raw = String(baseUrl || fallbackBaseUrl || 'https://www.aliwork.com').trim();
293
+ try {
294
+ const parsed = new URL(raw);
295
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
296
+ return null;
297
+ }
298
+ return raw.replace(/\/+$/, '');
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+
267
304
  /**
268
305
  * 查找项目根目录(project 工作区)。
269
306
  *
@@ -354,6 +391,123 @@ function extractInfoFromCookies(cookies) {
354
391
  return { csrfToken, corpId, userId };
355
392
  }
356
393
 
394
+ function parseCookieHeader(rawCookieHeader, options = {}) {
395
+ const raw = String(rawCookieHeader || '').trim();
396
+ if (!raw) {
397
+ return [];
398
+ }
399
+ let domain = '';
400
+ if (options.baseUrl) {
401
+ try {
402
+ domain = new URL(options.baseUrl).hostname;
403
+ } catch {
404
+ domain = '';
405
+ }
406
+ }
407
+ return raw
408
+ .split(';')
409
+ .map((part) => {
410
+ const trimmed = part.trim();
411
+ const equalsIndex = trimmed.indexOf('=');
412
+ if (equalsIndex <= 0) {
413
+ return null;
414
+ }
415
+ const name = trimmed.slice(0, equalsIndex).trim();
416
+ const value = trimmed.slice(equalsIndex + 1).trim();
417
+ if (!name) {
418
+ return null;
419
+ }
420
+ const cookie = { name, value };
421
+ if (domain) {
422
+ cookie.domain = domain;
423
+ }
424
+ return cookie;
425
+ })
426
+ .filter(Boolean);
427
+ }
428
+
429
+ function decodeCookieHeaderFromBase64(encodedCookie) {
430
+ const normalized = String(encodedCookie || '').trim().replace(/\s+/g, '');
431
+ if (!normalized) {
432
+ return null;
433
+ }
434
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
435
+ return null;
436
+ }
437
+ try {
438
+ const decoded = Buffer.from(normalized, 'base64').toString('utf8').trim();
439
+ return decoded && decoded.includes('=') ? decoded : null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+
445
+ function loadEnvCookieData(defaultBaseUrl, env = process.env) {
446
+ lastEnvAuthError = null;
447
+ const encodedCookie = String(env.OPENYIDA_COOKIE_B64 || '').trim();
448
+ const envAuthEnabled = isInjectedAuthMode(env);
449
+ if (!encodedCookie) {
450
+ if (envAuthEnabled) {
451
+ setLastEnvAuthError(
452
+ 'not_logged_in',
453
+ 'env_cookie_missing',
454
+ 'OPENYIDA_COOKIE_B64 is required when YIDA_AUTH_ENABLED=true'
455
+ );
456
+ }
457
+ return null;
458
+ }
459
+
460
+ const baseUrl = normalizeEnvBaseUrl(env.OPENYIDA_BASE_URL, defaultBaseUrl);
461
+ if (!baseUrl) {
462
+ setLastEnvAuthError(
463
+ 'not_logged_in',
464
+ 'base_url_invalid',
465
+ 'OPENYIDA_BASE_URL is invalid'
466
+ );
467
+ return null;
468
+ }
469
+
470
+ const rawCookieHeader = decodeCookieHeaderFromBase64(encodedCookie);
471
+ if (!rawCookieHeader) {
472
+ setLastEnvAuthError(
473
+ 'not_logged_in',
474
+ 'env_cookie_decode_failed',
475
+ 'OPENYIDA_COOKIE_B64 is not a valid base64 encoded Cookie header'
476
+ );
477
+ return null;
478
+ }
479
+
480
+ const cookies = parseCookieHeader(rawCookieHeader, { baseUrl });
481
+ if (cookies.length === 0) {
482
+ setLastEnvAuthError(
483
+ 'not_logged_in',
484
+ 'env_cookie_parse_failed',
485
+ 'OPENYIDA_COOKIE_B64 decoded to an empty or invalid Cookie header'
486
+ );
487
+ return null;
488
+ }
489
+
490
+ const { csrfToken, corpId, userId } = extractInfoFromCookies(cookies);
491
+ if (!csrfToken) {
492
+ setLastEnvAuthError(
493
+ 'csrf_missing',
494
+ 'csrf_token_missing',
495
+ 'Env Cookie header does not contain tianshu_csrf_token'
496
+ );
497
+ return null;
498
+ }
499
+
500
+ return {
501
+ cookies,
502
+ csrf_token: csrfToken,
503
+ corp_id: corpId,
504
+ user_id: userId,
505
+ base_url: baseUrl,
506
+ auth_source: 'env',
507
+ auth_mode: 'env',
508
+ };
509
+ }
510
+
357
511
  // ── 登录态缓存读取 ────────────────────────────────────
358
512
 
359
513
  /**
@@ -368,6 +522,14 @@ function loadCookieData(projectRoot, defaultBaseUrl) {
368
522
  const root = projectRoot || findProjectRoot();
369
523
  const fallbackBaseUrl = defaultBaseUrl || 'https://www.aliwork.com';
370
524
 
525
+ const envCookieData = loadEnvCookieData(fallbackBaseUrl);
526
+ if (envCookieData) {
527
+ return envCookieData;
528
+ }
529
+ if (lastEnvAuthError && isEnvAuthMode()) {
530
+ return null;
531
+ }
532
+
371
533
  // 尝试迁移旧版 cookies.json(仅在首次使用多环境功能时执行一次)
372
534
  const { migrateOldCookieFile, getCookieFilePath } = require('./env-manager');
373
535
  migrateOldCookieFile(root);
@@ -428,21 +590,22 @@ function loadCookieData(projectRoot, defaultBaseUrl) {
428
590
  * @returns {object} loginResult
429
591
  */
430
592
  function triggerLogin(options = {}) {
431
- if (isInjectedAuthMode()) {
593
+ if (isEnvAuthMode()) {
432
594
  const cookieData = loadCookieData();
433
595
  if (cookieData && cookieData.cookies && cookieData.cookies.length > 0 && cookieData.csrf_token) {
434
596
  return cookieData;
435
597
  }
436
- const reason = cookieData && cookieData.cookies
437
- ? 'csrf_token_missing'
438
- : 'cookie_cache_missing';
598
+ const envAuthError = getLastEnvAuthError();
599
+ const reason = envAuthError
600
+ ? envAuthError.failure_reason
601
+ : (cookieData && cookieData.cookies ? 'csrf_token_missing' : 'env_cookie_missing');
439
602
  const { CliError } = require('./cli-error');
440
603
  throw new CliError(
441
- `not_logged_in: injected cookie is unavailable (${reason}). Refresh the host page so it can inject a fresh Yida cookie cache.`,
604
+ `not_logged_in: env cookie is unavailable (${reason}). Refresh the host page so it can inject a fresh Yida cookie.`,
442
605
  {
443
606
  code: 'INJECTED_AUTH_REQUIRED',
444
607
  details: {
445
- authMode: 'injected',
608
+ authMode: 'env',
446
609
  failure_reason: reason,
447
610
  },
448
611
  }
@@ -499,10 +662,16 @@ function refreshCsrfToken() {
499
662
  * @returns {boolean}
500
663
  */
501
664
  function isLoginExpired(responseJson) {
665
+ if (!responseJson) {return false;}
666
+ const status = String(responseJson.status || '').toLowerCase();
667
+ const errorCode = String(responseJson.errorCode || responseJson.code || '').toLowerCase();
668
+ const errorMsg = String(responseJson.errorMsg || responseJson.message || '').toLowerCase();
669
+ if (status === 'not_logged_in' || errorCode === 'not_logged_in' || errorMsg.includes('not_logged_in')) {
670
+ return true;
671
+ }
502
672
  return (
503
- responseJson &&
504
673
  responseJson.success === false &&
505
- (responseJson.errorCode === '307' || responseJson.errorCode === '302')
674
+ ['307', '302', '401', '403'].includes(errorCode)
506
675
  );
507
676
  }
508
677
 
@@ -523,6 +692,10 @@ function isHttpRedirectStatus(statusCode) {
523
692
  return [301, 302, 303, 307, 308].includes(Number(statusCode));
524
693
  }
525
694
 
695
+ function isHttpAuthStatus(statusCode) {
696
+ return [401, 403].includes(Number(statusCode));
697
+ }
698
+
526
699
  // ── base_url 解析 ─────────────────────────────────────
527
700
 
528
701
  /**
@@ -612,7 +785,7 @@ function httpPost(baseUrl, requestPath, postData, cookies, optionsOverride = {})
612
785
  if (!optionsOverride.silentStatus) {
613
786
  warn(t('common.http_status', res.statusCode));
614
787
  }
615
- if (isHttpRedirectStatus(res.statusCode)) {
788
+ if (isHttpRedirectStatus(res.statusCode) || isHttpAuthStatus(res.statusCode)) {
616
789
  resolve({
617
790
  __needLogin: true,
618
791
  __httpStatus: res.statusCode,
@@ -694,7 +867,7 @@ function httpPostJson(baseUrl, requestPath, payload, cookies, optionsOverride =
694
867
  if (!optionsOverride.silentStatus) {
695
868
  warn(t('common.http_status', res.statusCode));
696
869
  }
697
- if (isHttpRedirectStatus(res.statusCode)) {
870
+ if (isHttpRedirectStatus(res.statusCode) || isHttpAuthStatus(res.statusCode)) {
698
871
  resolve({
699
872
  __needLogin: true,
700
873
  __httpStatus: res.statusCode,
@@ -784,7 +957,7 @@ function httpGet(baseUrl, requestPath, queryParams, cookies, optionsOverride = {
784
957
  if (!optionsOverride.silentStatus) {
785
958
  warn(t('common.http_status', res.statusCode));
786
959
  }
787
- if (isHttpRedirectStatus(res.statusCode)) {
960
+ if (isHttpRedirectStatus(res.statusCode) || isHttpAuthStatus(res.statusCode)) {
788
961
  resolve({
789
962
  __needLogin: true,
790
963
  __httpStatus: res.statusCode,
@@ -830,14 +1003,16 @@ function httpGet(baseUrl, requestPath, queryParams, cookies, optionsOverride = {
830
1003
  */
831
1004
  async function requestWithAutoLogin(requestFn, authRef) {
832
1005
  let result = await requestFn(authRef);
1006
+ const usingEnvAuth = isEnvAuthMode()
1007
+ || !!(authRef && authRef.cookieData && authRef.cookieData.auth_source === 'env');
833
1008
 
834
1009
  if (result && result.__csrfExpired) {
835
- if (isInjectedAuthMode()) {
1010
+ if (usingEnvAuth) {
836
1011
  return {
837
1012
  success: false,
838
1013
  __needLogin: true,
839
1014
  errorCode: 'INJECTED_AUTH_REQUIRED',
840
- errorMsg: 'not_logged_in: injected cookie csrf_token expired. Refresh the host page so it can inject a fresh Yida cookie cache.',
1015
+ errorMsg: 'not_logged_in: env cookie csrf_token expired. Refresh the host page so it can inject a fresh Yida cookie.',
841
1016
  };
842
1017
  }
843
1018
  const refreshedData = refreshCsrfToken();
@@ -854,12 +1029,12 @@ async function requestWithAutoLogin(requestFn, authRef) {
854
1029
  }
855
1030
 
856
1031
  if (result && result.__needLogin) {
857
- if (isInjectedAuthMode()) {
1032
+ if (usingEnvAuth) {
858
1033
  return {
859
1034
  success: false,
860
1035
  __needLogin: true,
861
1036
  errorCode: 'INJECTED_AUTH_REQUIRED',
862
- errorMsg: 'not_logged_in: injected cookie missing or expired. Refresh the host page so it can inject a fresh Yida cookie cache.',
1037
+ errorMsg: 'not_logged_in: env cookie missing or expired. Refresh the host page so it can inject a fresh Yida cookie.',
863
1038
  };
864
1039
  }
865
1040
  const newCookieData = triggerLogin({ force: true });
@@ -886,6 +1061,9 @@ module.exports = {
886
1061
  hasDesktopEnvironment,
887
1062
  findProjectRoot,
888
1063
  extractInfoFromCookies,
1064
+ parseCookieHeader,
1065
+ loadEnvCookieData,
1066
+ getLastEnvAuthError,
889
1067
  loadCookieData,
890
1068
  triggerLogin,
891
1069
  refreshCsrfToken,
@@ -901,4 +1079,6 @@ module.exports = {
901
1079
  getNodeExecutable,
902
1080
  resolveWukongWorkspaceRoot,
903
1081
  isInjectedAuthMode,
1082
+ hasEnvCookieAuth,
1083
+ isEnvAuthMode,
904
1084
  };
@@ -165,7 +165,7 @@ export function injectTailwindSource() {
165
165
  '@import "tailwindcss/theme";',
166
166
  '@import "tailwindcss/preflight";',
167
167
  '@import "tailwindcss/utilities";',
168
- '@theme { --color-brand: #2F6FED; }',
168
+ '@theme { --color-brand: var(--color-brand1-6, #2F6FED); }',
169
169
  ].join('\n');
170
170
  document.head.appendChild(style);
171
171
  }
@@ -183,11 +183,11 @@ export function injectTailwindFallback() {
183
183
  style.innerHTML = [
184
184
  '.oyd-btn,.oyd-select-trigger,.oyd-select-option{appearance:none;-webkit-appearance:none;font-family:inherit;}',
185
185
  '.oyd-btn{height:36px;border-radius:6px;border:1px solid #D0D5DD;background:#fff;padding:0 12px;font-size:14px;cursor:pointer;}',
186
- '.oyd-btn-primary{background:#2F6FED;border-color:#2F6FED;color:#fff;}',
186
+ '.oyd-btn-primary{background:var(--color-brand1-6,#2F6FED);border-color:var(--color-brand1-6,#2F6FED);color:#fff;}',
187
187
  '.oyd-select-trigger{height:38px;border-radius:6px;border:1px solid #D0D5DD;background:#fff;padding:0 12px;font-size:14px;text-align:left;box-shadow:0 6px 14px rgba(15,23,42,.06);}',
188
188
  '.oyd-select-menu{position:absolute;z-index:30;margin-top:6px;width:100%;padding:6px;border:1px solid #E4E7EC;border-radius:10px;background:#fff;box-shadow:0 16px 32px rgba(16,24,40,.14);}',
189
189
  '.oyd-select-option{width:100%;min-height:36px;border:0;border-radius:8px;background:#fff;padding:0 10px;text-align:left;font-size:14px;cursor:pointer;}',
190
- '.oyd-select-option-active{background:#EFF6FF;color:#1D4ED8;font-weight:600;}',
190
+ '.oyd-select-option-active{background:var(--color-brand1-1,#EFF6FF);color:var(--color-brand1-6,#1D4ED8);font-weight:600;}',
191
191
  ].join('');
192
192
  document.head.appendChild(style);
193
193
  }
@@ -351,7 +351,7 @@ var styles = {
351
351
  marginBottom: '16px'
352
352
  },
353
353
  button: {
354
- backgroundColor: '#1677FF',
354
+ backgroundColor: 'var(--color-brand1-6, #1677FF)',
355
355
  color: '#fff',
356
356
  border: 'none',
357
357
  borderRadius: '4px',
@@ -424,10 +424,10 @@ var styles = {
424
424
  minHeight: 36,
425
425
  padding: '8px 12px',
426
426
  textAlign: 'left',
427
- background: '#EFF6FF',
427
+ background: 'var(--color-brand1-1, #EFF6FF)',
428
428
  border: 0,
429
429
  borderRadius: 8,
430
- color: '#1D4ED8',
430
+ color: 'var(--color-brand1-6, #1D4ED8)',
431
431
  appearance: 'none',
432
432
  WebkitAppearance: 'none',
433
433
  fontFamily: 'inherit',
@@ -2,23 +2,25 @@
2
2
 
3
3
  // ── 色彩系统 ─────────────────────────────────────────
4
4
  var colors = {
5
- // 主色(使用宜搭平台 CSS 变量)
6
- primary: 'var(--color-brand1-6)',
7
- primaryHover: 'var(--color-brand1-1)',
8
- hover: 'var(--color-brand1-9)',
9
- active: 'var(--color-brand1-9)',
10
- disabled: 'var(--color-brand1-8)',
11
- primaryLight: 'var(--color-brand1-2)',
12
-
13
- // 语义色
5
+ // 主色(跟随宜搭平台品牌变量,自动适配 App 主题;色阶 1 最浅、6 主色、10 最深)
6
+ primary: 'var(--color-brand1-6)', // 主色:主按钮、链接、选中态高亮
7
+ primaryHover: 'var(--color-brand1-5)', // 主色 hover:填充按钮/链接 hover,比主色亮一档
8
+ primaryActive: 'var(--color-brand1-7)', // 主色按下:填充按钮 active,比主色深一档
9
+ hover: 'var(--color-brand1-1)', // 通用浅色 hover 底:列表行、菜单项 hover
10
+ active: 'var(--color-brand1-2)', // 通用浅色激活/按下底
11
+ disabled: 'var(--color-brand1-3)', // 禁用态:浅、去饱和
12
+ primaryLight: 'var(--color-brand1-2)', // 主色浅背景:选中行底、标签高亮底
13
+
14
+ // 语义色(固定,不随主题变——保证成功/警告/错误语义稳定)
14
15
  success: '#52C41A',
15
16
  successLight: '#F6FFED',
16
17
  warning: '#FAAD14',
17
18
  warningLight: '#FFFBE6',
18
19
  error: '#FF4D4F',
19
20
  errorLight: '#FFF2F0',
20
- info: '#1677FF',
21
- infoLight: '#E6F4FF',
21
+ // info = 品牌信息色,跟随主题(不再固定蓝)
22
+ info: 'var(--color-brand1-6)',
23
+ infoLight: 'var(--color-brand1-1)',
22
24
 
23
25
  // 中性色
24
26
  text: '#1D2129',
@@ -168,7 +170,7 @@ var styles = {
168
170
  success: { color: '#52C41A', bg: '#F6FFED', border: '#B7EB8F' },
169
171
  warning: { color: '#FAAD14', bg: '#FFFBE6', border: '#FFE58F' },
170
172
  error: { color: '#FF4D4F', bg: '#FFF2F0', border: '#FFCCC7' },
171
- info: { color: '#1677FF', bg: '#E6F4FF', border: '#91CAFF' },
173
+ info: { color: 'var(--color-brand1-6)', bg: 'var(--color-brand1-1)', border: 'var(--color-brand1-3)' },
172
174
  default: { color: '#4E5969', bg: '#F2F3F5', border: '#E5E6EB' },
173
175
  };
174
176
  var c = colorMap[type] || colorMap.default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.7.7",
3
+ "version": "2026.7.9",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -15,6 +15,8 @@ const SKILL_COVERAGE = {
15
15
  'yida-app-permission': { level: 'offline-unit', tests: ['tests/app-permission.test.js'], reason: 'app admin mutations affect real application access; shared real E2E only validates safe read paths' },
16
16
  'yida-basic-info': { level: 'offline-unit', tests: ['tests/basic-info.test.js'], reason: 'basic-info reads org admin metadata and can update domains; unit coverage avoids mutating shared real org settings' },
17
17
  'yida-business-rule': { level: 'opt-in', reason: 'business association rules mutate form event configuration; validate in a dedicated real-form/UI stage before adding to deterministic shared E2E' },
18
+ 'yida-canvas-custom-page': { level: 'offline-unit', tests: ['skill metadata and packaging validation'], reason: 'Code Canvas authoring skill has no dedicated CLI command; runtimeCode/importedModules are produced by the platform compile service and a YidaCodeCanvas schema cannot be published via openyida, so shared real E2E validates skill metadata and routing rather than mutating a real page' },
19
+ 'yida-canvas-upgrade': { level: 'offline-unit', tests: ['skill metadata and packaging validation'], reason: 'native-to-Code-Canvas migration skill emits source/report artifacts but has no CLI publish path for YidaCodeCanvas; validated via skill metadata and routing until a Canvas publish stage exists' },
18
20
  'yida-chart': { level: 'real-e2e', stages: ['report', 'dashboard'], tests: ['report chart config generation'] },
19
21
  'yida-connector': { level: 'offline', stages: ['connector-local'], commands: ['connector gen-template', 'connector parse-api'] },
20
22
  'yida-connector-safe-actions': { level: 'offline', stages: ['connector-local'], commands: ['connector parse-api', 'connector test --action <operationId>'], reason: 'skill documents conservative HTTP connector action generation and repair workflow; shared E2E should validate local parsing without mutating tenant connectors' },
@@ -43,8 +45,10 @@ const SKILL_COVERAGE = {
43
45
  'yida-login': { level: 'real-e2e', stages: ['auth'], commands: ['login --check-only --json'] },
44
46
  'yida-logout': { level: 'offline-unit', tests: ['login/auth unit coverage'], reason: 'real logout would destroy the shared E2E session' },
45
47
  'yida-nav-group': { level: 'offline-unit', tests: ['tests/nav-group.test.js'], reason: 'navigation grouping mutates app sidebar order; unit coverage validates payloads and tree operations until a dedicated cleanup-safe nav stage exists' },
48
+ 'yida-nav-shell': { level: 'offline-unit', tests: ['skill metadata and packaging validation'], reason: 'page-internal JSX navigation shell is an authoring pattern, not a standalone CLI command; validate skill packaging and use custom-page/page-uiux generation fixtures for runtime page coverage' },
46
49
  'yida-openyida-publish-guard': { level: 'offline-unit', tests: ['skill metadata and packaging validation'], reason: 'publish guard is an agent workflow safety rule for comparing live schema before publish; real page publish behavior remains covered by yida-publish-page E2E stages' },
47
50
  'yida-page-config': { level: 'real-e2e', stages: ['share'], commands: ['get-page-config', 'verify-short-url', 'save-share-config'] },
51
+ 'yida-page-uiux': { level: 'offline-unit', tests: ['skill metadata and packaging validation', 'routing eval scenarios'], reason: 'visual direction skill emits a decision block before JSX authoring and should be validated through routing/packaging rather than mutating Yida resources' },
48
52
  'yida-ppt': { level: 'deprecated', reason: 'skill is deprecated in favor of yida-ppt-slider' },
49
53
  'yida-ppt-slider': { level: 'offline-unit', reason: 'presentation-style custom page skill should be validated by page generation/check-page fixtures' },
50
54
  'yida-process-rule': { level: 'opt-in-real-e2e', stages: ['process'], commands: ['configure-process'], reason: 'process stage publishes workflow rules on the disposable E2E form and is excluded from default full E2E unless explicitly requested' },
@@ -3,6 +3,12 @@ set -euo pipefail
3
3
 
4
4
  cd "$(dirname "$0")/.."
5
5
 
6
+ if [ -z "${npm_config_cache:-}" ]; then
7
+ npm_config_cache="$(mktemp -d "${TMPDIR:-/tmp}/openyida-npm-cache-XXXXXX")"
8
+ export npm_config_cache
9
+ trap 'rm -rf "$npm_config_cache"' EXIT
10
+ fi
11
+
6
12
  echo "=== Step 1: Install dependencies ==="
7
13
  npm ci --ignore-scripts
8
14
 
@@ -3,10 +3,13 @@
3
3
  'use strict';
4
4
 
5
5
  const { spawnSync } = require('child_process');
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
6
9
 
7
10
  const MAX_TARBALL_BYTES = 1536 * 1024;
8
11
  const MAX_UNPACKED_BYTES = 4608 * 1024;
9
- const MAX_ENTRY_COUNT = 330;
12
+ const MAX_ENTRY_COUNT = 350;
10
13
  const MAX_SINGLE_FILE_BYTES = 512 * 1024;
11
14
 
12
15
  function formatBytes(bytes) {
@@ -26,10 +29,18 @@ function fail(message) {
26
29
  }
27
30
 
28
31
  function runNpmPackDryRun() {
32
+ const shouldCreateCache = !process.env.OPENYIDA_NPM_CACHE;
33
+ const npmCache = shouldCreateCache
34
+ ? fs.mkdtempSync(path.join(os.tmpdir(), 'openyida-npm-cache-'))
35
+ : process.env.OPENYIDA_NPM_CACHE;
29
36
  const result = spawnSync('npm', ['pack', '--dry-run', '--json'], {
30
37
  encoding: 'utf8',
38
+ env: { ...process.env, npm_config_cache: npmCache, NPM_CONFIG_CACHE: npmCache },
31
39
  stdio: 'pipe',
32
40
  });
41
+ if (shouldCreateCache) {
42
+ fs.rmSync(npmCache, { recursive: true, force: true });
43
+ }
33
44
 
34
45
  if (result.status !== 0) {
35
46
  process.stderr.write(result.stderr || result.stdout);
@@ -54,7 +54,8 @@ description: >
54
54
 
55
55
  [Step 4](按需)创建/更新表单 → openyida create-form → 获得 formUuid(表单)
56
56
 
57
- [Step 5] 编写自定义页面代码 → yida-custom-page 规范 pages/src/<项目名>.oyd.jsx
57
+ [Step 5] 编写自定义页面代码 → 先按「自定义页面选路」定链路(**默认 Code Canvas yida-canvas-custom-page**,含开放 API 读数据页;仅强依赖原生实例数据桥的页回退 native yida-custom-page)
58
+ ↓ (首次生成面向用户的页必做:先用 yida-page-uiux 产出「视觉方向决策块」,避免统一灰白圆角的 AI 味模板脸,再交所选链路落地)
58
59
 
59
60
  [Step 6] 发布页面 → openyida publish
60
61
 
@@ -69,6 +70,8 @@ description: >
69
70
  [Step 10] 输出访问链接,用系统浏览器打开
70
71
  ```
71
72
 
73
+ > **Step 5 先定视觉方向(首次生成面向用户页必做)**:写 JSX 前先用 `yida-page-uiux` 锁定视觉方向(页面类型判定 → 意图解码 → 差异化决策 → 去 AI 味自检),产出「视觉方向决策块」,再交所选链路(默认 `yida-canvas-custom-page`,回退 `yida-custom-page`)按 `design-system.md` token 落地。跳过此步会直接套用统一灰白圆角模板,生成有 AI 味的平庸页面。详见 `skills/yida-page-uiux/SKILL.md`。
74
+ >
72
75
  > **Step 7 导航整理(含看板/多页面时必做)**:首次生成完整应用后,必须基于业务信息架构重排导航,不能保留创建时的默认顺序。默认原则:面向决策者的**总览/驾驶舱看板作为应用门面靠前**,数据录入/明细表单在其后;同级多个专题看板按业务优先级排,不要把所有页面无脑堆最前。详见 `skills/yida-nav-group/SKILL.md`。
73
76
  >
74
77
  > **Step 8 灌入示例数据(有表单时默认执行)**:新建应用的表单默认无数据,看板会空。导航整理完成后,默认向核心表单灌入 **2-3 条**覆盖关键维度(如不同活动/渠道/日期)的示例记录,让看板首屏可展示真实聚合效果。`DateField`/`CascadeDateField` 必须用 13 位毫秒时间戳;灌后执行 `openyida data query` 抽查至少 1 条,确认字段值非空。详见 `skills/yida-data-management/SKILL.md`。
@@ -78,12 +81,19 @@ description: >
78
81
  ## 技能路由(单一 / 增量任务)
79
82
 
80
83
  > 选定 **1 个**最匹配的项执行。表**按业务域分组**,每组内既可能是 skill 也可能是 CLI:
81
- > - 行名为 `yida-xxx` / `sls-log-workbench` / `large-file-write` 的是 **skill** → 先读它的 `skills/<技能名>/SKILL.md` 再执行;
84
+ > - 行名为 `yida-xxx` / `sls-log-workbench` / `large-file-write` 的是 **skill** → 先读对应子技能文档再执行;
82
85
  > - 行名为 `openyida xxx` 并标 **`CLI`** 的**无 SKILL.md** → 识别到诉求直接执行命令、**不要当 skill 去 read**。
83
86
  >
84
87
  > 按分组 +「何时选择」内联区别对号入座即可。
85
88
 
86
- > ⚠️ **同类易错先分清**:改字段结构→`create-form-page`|只读 Schema→`get-schema`|改数据记录→`data-management`|详情页美化→`form-detail`;字段实时校验→`formula`|提交后编排→`integration`|跨表高级函数→`business-rule`;从零建流程→`create-process`|改已有流程→`process-rule`;权限按层级:组织→`corp-manager`/应用→`app-permission`/表单→`form-permission`/页面分享→`page-config`。
89
+ > ⚠️ **同类易错先分清**:改字段结构→`create-form-page`|只读 Schema→`get-schema`|改数据记录→`data-management`|详情页美化→`form-detail`;自定义页视觉方向/去AI味→`page-uiux`(定方向)|token/组件实现→`custom-page`(design-system);加导航先分清→平台左侧菜单分组/排序→`nav-group`|页面隐藏应用导航后页面内自绘导航壳→`nav-shell`(必须隐藏原导航,并让导航项 URL 带 `isRenderNav=false` 等参数);字段实时校验→`formula`|提交后编排→`integration`|跨表高级函数→`business-rule`;从零建流程→`create-process`|改已有流程→`process-rule`;权限按层级:组织→`corp-manager`/应用→`app-permission`/表单→`form-permission`/页面分享→`page-config`;**自定义页面选路见下方专表**。
90
+
91
+ > 🧭 **自定义页面选路(默认 Code Canvas,按顺序命中即停)**:
92
+ > 1. **默认 → Code Canvas** `yida-canvas-custom-page`:现代 React 交互 / hooks 状态 / 可视化 / AI 生成 / 需崩溃隔离,**以及只需通过开放 API(HTTP)读取宜搭数据的页面**(Canvas 自写 fetch 即可拿数据);
93
+ > 2. 仅当页面强依赖**原生实例数据桥**——表单内字段双向绑定 `this.$(fieldId)`、`this.utils.yida.*`、`dataSourceMap`、提交流程 / 设计器数据源深度耦合,且用开放 API 重写代价过高 → 回退 **native** `yida-custom-page`;
94
+ > 3. 已有普通 `.oyd.jsx` 要迁到 Canvas → `yida-canvas-upgrade`。
95
+ >
96
+ > 依据(源码核实):Canvas 代码在宿主页真实 `window` 中 `new Function` 执行,但物料只透传 `code/runtimeCode/importedModules/pageType`,**无 `this` 上下文、无 `dataSourceMap`**,`this.utils.yida.*` 不可用;宿主 window 全局(`__yida_plugin_runtime__` 插件系统、`__VcDeepYidaUtils__`)均非表单数据桥,也无 `window.Deep` 字段组件。Canvas 读宜搭数据 = 在 `YidaComp` 内自写 HTTP 调开放 API。只有需要免费 `this` 实例桥的页才留 native。
87
97
 
88
98
  | 分组 | 技能 | 何时选择(关键区别已内联) |
89
99
  |------|------|--------------------------|
@@ -94,7 +104,11 @@ description: >
94
104
  | **页面与表单** | `yida-create-page` | 创建空白自定义页面拿 formUuid,后续写 JSX |
95
105
  | | `yida-create-form-page` | 创建/更新表单、增删改**字段结构**(普通表单,无审批) |
96
106
  | | `yida-create-process` | 从零建**带审批**流程表单(表单还不存在,一步到位) |
97
- | | `yida-custom-page` | 编写自定义页面 JSX 逻辑与 UI |
107
+ | | `yida-page-uiux` | 写自定义页面 UI 前先定视觉方向、去 AI 味(工作台/看板/列表/详情/官网落地页;产出决策块,不写代码;canvas / native 两链路通用) |
108
+ | | `yida-canvas-custom-page` | **自定义页面默认链路**:现代 React 交互 / hooks / 可视化 / AI 页,含只需开放 API HTTP 读数据的页(Code Canvas,`runtimeCode` + `importedModules`,真 React18 + 崩溃隔离) |
109
+ | | `yida-custom-page` | 回退项:仅当强依赖原生实例数据桥(`this.$(fieldId)` 双向绑定 / `this.utils.yida.*` / `dataSourceMap` / 提交流程深度耦合)时才用(native `.oyd.jsx` 链路) |
110
+ | | `yida-canvas-upgrade` | 将已有普通 `.oyd.jsx` / `Jsx` 页面升级迁移到 Code Canvas / `YidaCodeCanvas` 链路 |
111
+ | | `yida-nav-shell` | 自定义页**隐藏应用导航**(`isRenderNav=false`,沉浸/门户/大屏/分享)后,页面内用 JSX 自绘侧边/顶部/浮动/标签导航壳;发布后要配置隐藏原导航,跨页导航项要拼完整 URL 并合并 `isRenderNav=false` / `corpid` / 业务参数(**区别** `yida-nav-group` 平台左侧菜单分组:那是真实导航树,本项是页面内自建导航) |
98
112
  | | `yida-publish-page` | JSX 写完后编译并发布 |
99
113
  | | `yida-openyida-publish-guard` | 发布已有自定义页面前检查线上设计器状态,避免本地旧源码覆盖用户在线改动 |
100
114
  | | `yida-table-form` | Excel 式表格批量录入提交 |
@@ -119,7 +133,7 @@ description: >
119
133
  | | `yida-app-permission` | **单应用级**权限(应用管理员/开发成员) |
120
134
  | | `yida-form-permission` | **单表单级**权限(权限组/数据范围) |
121
135
  | | `yida-page-config` | **页面级**:公开访问 / 组织内分享 |
122
- | **应用配置与平台** | `yida-nav-group` | 应用左侧菜单分组/排序 |
136
+ | **应用配置与平台** | `yida-nav-group` | 应用**左侧菜单**分组/排序(真实导航树;页面内自绘导航壳见 `yida-nav-shell`) |
123
137
  | | `yida-form-detail` | 只注 **CSS** 美化详情页,不改字段 |
124
138
  | | `yida-density` | 列表/表格信息密度选择 |
125
139
  | | `yida-i18n` | 应用多语言 / 国际化 |
@@ -143,6 +157,7 @@ description: >
143
157
  1. **先读后执行**:执行任何子技能前,必须先读取其 SKILL.md,不凭记忆猜参数格式。
144
158
  2. **corpId 一致性检查**:创建页面前对比 prd 与 `.cache/cookies.json` 的 corpId,不一致必须询问用户(重新登录 or 当前组织新建)。
145
159
  3. **发布前本地校验**:自定义页面发布前跑 `openyida check-page` + `openyida compile`;JSON 配置写盘后先解析校验,再调用平台命令。
160
+ 4. **命令输入文件禁止 shell 写入**:当 OpenYida 命令需要 JSON/YAML/CSV/config/script 文件参数时,先使用当前 agent 运行时提供的结构化文件写入工具(如 create_file / Write / file edit tool)创建文件,再把路径传给命令;禁止用 shell heredoc、`cat`/`echo`/`printf`/`tee` 加输出重定向,或把命令 stdout 重定向成业务文件。
146
161
 
147
162
  ### 重要规则(IMPORTANT,影响质量/性能/可维护性)
148
163
 
@@ -153,7 +168,7 @@ description: >
153
168
  5. **数据性能优先**:统计聚合用 `yida-report` 服务端聚合,不在前端拉全量后自行聚合。
154
169
  6. **避免无效重试**:失败先查登录态/组织/参数/字段 ID,无修改不连续重试超 1 次。
155
170
  7. **配置分两处存**:业务语义 → `prd/<项目名>.md`;Schema ID → `.cache/<项目名>-schema.json`(prd 不记 ID)。
156
- 8. **临时文件入 `.cache/`**:cookies/schema/配置/脚本一律放 `.cache/openyida/`,不写仓库根目录。
171
+ 8. **临时文件入 project `.cache/`**:OpenYida 业务中间文件写入 `<projectRoot>/.cache/openyida/<项目名或任务名>/`;Schema ID 映射仍写 `<projectRoot>/.cache/<项目名>-schema.json`。从 workspace 根执行命令时使用 `project/.cache/...`,从 project 工作目录内执行时使用 `.cache/...`;不要写仓库根目录或系统临时目录。
157
172
  9. **报表美化先问方案**:用户说"优化/美化报表"时先问选原生报表(`yida-report`)还是 ECharts(`yida-chart`)。
158
173
  10. **按 schema 证据选技能**:先看 `formType`、组件树、`dataSource.online`;`receipt/process/report` 分别落到表单/流程/报表技能。
159
174
  11. **官方示例范式优先**:蒸馏官方示例时先理解脱敏 schema 承载方式,不凭截图/标题/视觉判断。