openyida 2026.9.4 → 2026.9.7-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.
Files changed (65) hide show
  1. package/README.md +7 -6
  2. package/bin/yida.js +1 -1
  3. package/lib/app/app-list.js +78 -53
  4. package/lib/app/canvas-compile.js +184 -4
  5. package/lib/app/display-page-readback.js +63 -5
  6. package/lib/app/get-schema.js +158 -5
  7. package/lib/app/page-linter.js +24 -0
  8. package/lib/app/publish.js +141 -6
  9. package/lib/app/services/canvas-page-schema-builder.js +175 -0
  10. package/lib/bridge/bridge.js +15 -3
  11. package/lib/connector/api.js +3 -1
  12. package/lib/connector/connector-add-action.js +2 -0
  13. package/lib/connector/connector-create-connection.js +119 -16
  14. package/lib/connector/connector-create.js +40 -32
  15. package/lib/connector/connector-detail.js +2 -1
  16. package/lib/connector/connector-gen-template.js +5 -17
  17. package/lib/connector/connector-list-connections.js +5 -4
  18. package/lib/connector/connector-test.js +48 -2
  19. package/lib/connector/contract.js +132 -10
  20. package/lib/connector/operation-normalizer.js +20 -4
  21. package/lib/connector/yida-system-token.js +280 -0
  22. package/lib/core/agent-capabilities.js +24 -2
  23. package/lib/core/command-manifest.js +8 -8
  24. package/lib/core/locales/en.js +52 -2
  25. package/lib/core/locales/zh.js +52 -2
  26. package/lib/core/redact.js +4 -3
  27. package/lib/core/utils.js +358 -0
  28. package/lib/integration/connector-presets.js +95 -9
  29. package/lib/integration/integration-connector-schema.js +79 -15
  30. package/lib/integration/integration-create.js +131 -5
  31. package/lib/integration/integration-readback.js +136 -0
  32. package/lib/process/configure-process.js +69 -16
  33. package/package.json +1 -1
  34. package/scripts/postinstall.js +1 -1
  35. package/yida-skills/SKILL.md +3 -1
  36. package/yida-skills/references/resource-context.md +1 -1
  37. package/yida-skills/skills/yida-app/workflow/step-1-resource-context.md +1 -1
  38. package/yida-skills/skills/yida-app/workflow/step-2-design.md +3 -1
  39. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +4 -3
  40. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +3 -1
  41. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +2 -0
  42. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +19 -3
  43. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +25 -22
  44. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-authoring-examples.md +19 -18
  45. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +41 -37
  46. package/yida-skills/skills/yida-canvas-custom-page/references/employeefield-verification.md +2 -2
  47. package/yida-skills/skills/yida-canvas-custom-page/references/native-components-bridge.md +1 -1
  48. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +3 -3
  49. package/yida-skills/skills/yida-canvas-data-binding/references/connector-binding.md +23 -0
  50. package/yida-skills/skills/yida-connector/SKILL.md +15 -8
  51. package/yida-skills/skills/yida-connector/examples/operations-search-formdata-v2.json +2 -2
  52. package/yida-skills/skills/yida-connector/references/connector-action-format.md +9 -0
  53. package/yida-skills/skills/yida-connector/templates/api-document-template.md +6 -19
  54. package/yida-skills/skills/yida-data-management/SKILL.md +8 -4
  55. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  56. package/yida-skills/skills/yida-design/workflow/output-design.md +2 -0
  57. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +4 -3
  58. package/yida-skills/skills/yida-dingtalk-openapi/SKILL.md +75 -0
  59. package/yida-skills/skills/yida-dingtalk-openapi/references/api-contract.md +98 -0
  60. package/yida-skills/skills/yida-dingtalk-openapi/references/connector-mapping.md +27 -0
  61. package/yida-skills/skills/yida-integration/SKILL.md +29 -0
  62. package/yida-skills/skills/yida-publish-page/SKILL.md +3 -2
  63. package/yida-skills/skills/yida-requirement-analysis/SKILL.md +2 -1
  64. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +3 -0
  65. package/yida-skills/skills-index.json +54 -0
package/README.md CHANGED
@@ -168,8 +168,9 @@ For a user-facing list of supported features and matching CLI commands, see [Ope
168
168
  ```bash
169
169
  openyida create-app --name "CRM" --desc "Customer management"
170
170
  openyida sample yida-design app-theme --output .cache/openyida/crm/app-theme.css
171
+ openyida create-app --name "CRM" --desc "Customer management" --theme-file .cache/openyida/crm/app-theme.css --nav-theme light --logo-source appIcon --layout l_shape
172
+ openyida app-list --type managed --page 1 --size 16
171
173
  openyida update-app APP_XXX --theme-file .cache/openyida/crm/app-theme.css --nav-theme light --logo-source appIcon --layout l_shape
172
- openyida app-list --size 20
173
174
  openyida corp-efficiency
174
175
  openyida create-form create APP_XXX "Customer" .cache/openyida/forms/customer-fields.json
175
176
  openyida create-form update APP_XXX FORM_XXX .cache/openyida/forms/customer-changes.json
@@ -388,7 +389,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
388
389
 
389
390
  | Command | Description |
390
391
  |---------|-------------|
391
- | `openyida app-list [--size N]` | List my Yida apps |
392
+ | `openyida app-list [--type managed\|created] [--page N] [--size N]` | Page through apps I manage or created |
392
393
  | `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency overview and detail reports |
393
394
  | `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a Yida app |
394
395
  | `openyida design-plan init <requirement-brief.json> [--theme-id <id>] [--output-dir <dir>] [--json]` | Initialize a plan draft from confirmed requirements |
@@ -484,9 +485,9 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
484
485
  | `openyida connector update-action --connector-id <id> --action <operationId> --query-json JSON --confirm` | Safely update action query defaults |
485
486
  | `openyida connector list-actions <id>` | List actions |
486
487
  | `openyida connector delete-action <id> <operation-id>` | Delete an action |
487
- | `openyida connector test --connector-id <id> --action <actionId> [--path-json JSON] [--query-json JSON] [--header-json JSON] [--body-json JSON] [--account-id <id>]` | Test an action |
488
+ | `openyida connector test --connector-id <id> --action <actionId> [--path-json JSON] [--query-json JSON] [--header-json JSON] [--body-json JSON] [--account-id <id>] [--system-token-app <appType>]` | Test an action |
488
489
  | `openyida connector list-connections <id>` | List auth connections |
489
- | `openyida connector create-connection <id> <name>` | Create an auth connection |
490
+ | `openyida connector create-connection <id> <name> [--interactive]` | Create an auth connection |
490
491
  | `openyida connector smart-create --curl "..."` | Generate a redacted action draft from cURL (no remote create) |
491
492
  | `openyida connector parse-api [options]` | Parse API information |
492
493
  | `openyida connector gen-template [output]` | Generate API document template |
@@ -495,7 +496,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
495
496
 
496
497
  | Command | Description |
497
498
  |---------|-------------|
498
- | `openyida integration create <appType> ... [--spec file.json]` | Create integration automation flow |
499
+ | `openyida integration create <appType> ... [--spec file.json] [--connector-system-token-app <appType>]` | Create integration automation flow |
499
500
  | `openyida integration update <appType> <formUuid> <processCode> --spec <desired-spec.json> [--publish]` | Probe integration update capability (currently blocked without full readback) |
500
501
  | `openyida integration list <appType> [--flow-types 1,2,3,5,6] [--form-uuid <uuid>] [--status y\|n] [--json]` | List integration automation flows |
501
502
  | `openyida integration enable <appType> <formUuid> <processCode>` | Enable integration automation flow |
@@ -568,7 +569,7 @@ The CLI package ships the core UI languages `zh` and `en` by default. Other CLI
568
569
 
569
570
  #### Forms and Pages
570
571
 
571
- Form field definitions can include `alias` or `componentAlias` to populate Yida designer component aliases, stored as `pages[0].componentAlias.items`. Yida runtime resolves these aliases in page JS, so `this.$('phone')` can be used instead of `this.$('textField_xxx')`; OpenYida form rules, validations, and `openyida data ... --resolve-aliases` JSON inputs also accept aliases as field references. For server-side DingTalk OpenAPI calls, use `GET /v2.0/yida/forms/component/alias/{appType}/{formUuid}` to read the `{ fieldId, alias }` mapping, then translate aliases before sending form data/search JSON. That endpoint requires `systemToken`, `userId`, an access token, and the Yida form data read permission; grant that permission in DingTalk developer console API permissions and publish the DingTalk app. Yida app code and app secret are available under app settings > deployment/maintenance.
572
+ Form field definitions can include `alias` or `componentAlias` to populate Yida designer component aliases, stored as `pages[0].componentAlias.items`. Yida runtime resolves these aliases in page JS, so `this.$('phone')` can be used instead of `this.$('textField_xxx')`; OpenYida form rules, validations, and `openyida data ... --resolve-aliases` JSON inputs also accept aliases as field references. For server-side DingTalk OpenAPI calls, use `GET /v2.0/yida/forms/component/alias/{appType}/{formUuid}` to read the `{ fieldId, alias }` mapping, then translate aliases before sending form data/search JSON. That endpoint requires `systemToken`, `userId`, an access token, and the Yida form data read permission. Use `connector test --system-token-app <appType>` for a transient test or `integration create --connector-system-token-app <appType>` for a server-side automation; OpenYida resolves `systemToken` internally and never prints it or stores it in page source and Action defaults.
572
573
 
573
574
  `openyida publish` preserves existing custom page data sources by default. Before saving the new compiled JSX Schema, it reads the current page Schema and merges the Page-level `dataSource` with the built-in `urlParams` and `timestamp` sources, so manually configured data sources are not deleted during republish.
574
575
 
package/bin/yida.js CHANGED
@@ -1119,7 +1119,7 @@ async function main() {
1119
1119
  delete-action <connector-id> <operation-id> 删除执行动作
1120
1120
  test --connector-id <id> --action <actionId> [结构化 JSON 参数] 测试执行动作
1121
1121
  list-connections <connector-id> 列出鉴权账号
1122
- create-connection <connector-id> <name> 创建鉴权账号
1122
+ create-connection <connector-id> <name> [--interactive] 创建鉴权账号;钉钉凭据可在本机隐藏输入
1123
1123
  smart-create --curl "curl命令" 生成脱敏连接器动作草稿(不创建远端资源)
1124
1124
  parse-api [选项] 解析接口信息
1125
1125
  gen-template [输出路径] 生成接口文档模板
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * app-list.js - 查询我的应用列表
3
3
  *
4
- * 用法:openyida app-list [--page <页码>] [--size <每页数量>]
4
+ * 用法:openyida app-list [--type managed|created] [--page <页码>] [--size <每页数量>]
5
5
  *
6
6
  * 输出字段:
7
7
  * appName - 应用名称(zh_CN)
@@ -12,61 +12,65 @@
12
12
  'use strict';
13
13
 
14
14
  const { createAuthRef, createYidaClient } = require('../core/yida-client');
15
- const { throwCommandError } = require('../core/command-errors');
15
+ const { t } = require('../core/i18n');
16
+ const { throwCommandError, throwUsage } = require('../core/command-errors');
16
17
 
17
18
  const API_PATH = '/query/app/getAppList.json';
19
+ const DEFAULT_PAGE_SIZE = 16;
20
+ const DEFAULT_PAGE_INDEX = 1;
21
+ const DEFAULT_TYPE = 'managed';
22
+ const APP_TYPES = Object.freeze({
23
+ managed: true,
24
+ created: false,
25
+ });
18
26
 
19
27
  /**
20
28
  * 拉取单页应用列表
21
29
  */
22
- function fetchAppListPage(auth, pageIndex, pageSize) {
30
+ function fetchAppListPage(auth, pageIndex, pageSize, isAdmin) {
23
31
  return createYidaClient({ authRef: auth }).get(API_PATH, (ref) => ({
24
32
  _api: 'nattyFetch',
25
33
  _mock: 'false',
26
34
  pageIndex,
27
35
  pageSize,
28
36
  creator: ref.userId,
37
+ isAdmin,
29
38
  _stamp: Date.now(),
30
39
  }));
31
40
  }
32
41
 
33
- /**
34
- * 拉取全量应用列表(自动翻页)
35
- */
36
- async function fetchAllApps(auth, pageSize) {
37
- const firstResult = await fetchAppListPage(auth, 1, pageSize);
38
-
39
- // 如果首次请求就提示需要登录,直接返回原始结果让外层自动刷新 token 后重试。
40
- if (firstResult.__needLogin) {
41
- return firstResult;
42
+ function parsePositiveInteger(value, optionName) {
43
+ if (!/^\d+$/.test(String(value || '')) || Number(value) < 1) {
44
+ throwUsage(t('app_list.invalid_positive_integer', optionName, value || ''));
42
45
  }
46
+ return Number(value);
47
+ }
43
48
 
44
- if (!firstResult.success) {
45
- throw new Error(firstResult.errorMsg || '查询应用列表失败');
46
- }
47
-
48
- const { data: firstPageData, totalCount } = firstResult.content;
49
- const allApps = [...firstPageData];
50
-
51
- const totalPages = Math.ceil(totalCount / pageSize);
52
- const remainingFetches = [];
53
-
54
- for (let pageIndex = 2; pageIndex <= totalPages; pageIndex++) {
55
- remainingFetches.push(fetchAppListPage(auth, pageIndex, pageSize));
56
- }
49
+ function parseArgs(args = []) {
50
+ const options = {
51
+ type: DEFAULT_TYPE,
52
+ pageIndex: DEFAULT_PAGE_INDEX,
53
+ pageSize: DEFAULT_PAGE_SIZE,
54
+ };
57
55
 
58
- const remainingResults = await Promise.all(remainingFetches);
59
- for (const result of remainingResults) {
60
- // 如果后续翻页也出现登录态问题,同样返回标记对象
61
- if (result.__needLogin) {
62
- return result;
63
- }
64
- if (result.success && result.content && result.content.data) {
65
- allApps.push(...result.content.data);
56
+ for (let index = 0; index < args.length; index++) {
57
+ const arg = args[index];
58
+ if (arg === '--type') {
59
+ const type = args[++index];
60
+ if (!Object.prototype.hasOwnProperty.call(APP_TYPES, type)) {
61
+ throwUsage(t('app_list.invalid_type', type || '', Object.keys(APP_TYPES).join(', ')));
62
+ }
63
+ options.type = type;
64
+ } else if (arg === '--page') {
65
+ options.pageIndex = parsePositiveInteger(args[++index], '--page');
66
+ } else if (arg === '--size') {
67
+ options.pageSize = parsePositiveInteger(args[++index], '--size');
68
+ } else {
69
+ throwUsage(t('app_list.invalid_argument', arg));
66
70
  }
67
71
  }
68
72
 
69
- return allApps;
73
+ return options;
70
74
  }
71
75
 
72
76
  /**
@@ -86,11 +90,13 @@ function hasHelpFlag(args) {
86
90
 
87
91
  function printUsage() {
88
92
  process.stderr.write([
89
- 'Usage: openyida app-list [--size N]',
93
+ t('app_list.usage'),
90
94
  '',
91
- 'Options:',
92
- ' --size N Page size used when fetching apps, default: 20',
93
- ' --help, -h Show this help',
95
+ t('app_list.options'),
96
+ t('app_list.option_type'),
97
+ t('app_list.option_page'),
98
+ t('app_list.option_size'),
99
+ t('app_list.option_help'),
94
100
  '',
95
101
  ].join('\n'));
96
102
  }
@@ -105,41 +111,60 @@ async function run(args) {
105
111
  return;
106
112
  }
107
113
 
108
- const pageSizeIndex = args.indexOf('--size');
109
- const pageSize = pageSizeIndex !== -1 && args[pageSizeIndex + 1]
110
- ? parseInt(args[pageSizeIndex + 1], 10)
111
- : 20;
114
+ const options = parseArgs(args);
112
115
 
113
116
  const authRef = createAuthRef();
114
117
 
115
- let apps;
118
+ let result;
116
119
  try {
117
- apps = await fetchAllApps(authRef, pageSize);
120
+ result = await fetchAppListPage(
121
+ authRef,
122
+ options.pageIndex,
123
+ options.pageSize,
124
+ APP_TYPES[options.type]
125
+ );
118
126
  } catch (err) {
119
- throwCommandError(`查询应用列表失败:${err.message}`);
127
+ throwCommandError(t('app_list.query_failed', err.message));
120
128
  }
121
129
 
122
130
  // 如果重试后仍然返回登录失效标记,说明登录态确实不可用
123
- if (apps && apps.__needLogin) {
124
- throwCommandError('登录态已失效,请重新登录', { code: 'CLI_AUTH_REQUIRED' });
131
+ if (result && result.__needLogin) {
132
+ throwCommandError(t('app_list.auth_required'), { code: 'CLI_AUTH_REQUIRED' });
125
133
  }
126
134
 
127
- if (!apps || apps.length === 0) {
128
- console.log('暂无应用');
129
- return;
135
+ if (!result || !result.success) {
136
+ throwCommandError(t('app_list.query_failed', (result && result.errorMsg) || t('app_list.unknown_error')));
130
137
  }
131
138
 
139
+ const content = result.content || {};
140
+ const apps = Array.isArray(content.data) ? content.data : [];
141
+ const totalCount = Number.isFinite(Number(content.totalCount)) ? Number(content.totalCount) : apps.length;
142
+ const totalPages = Math.max(1, Math.ceil(totalCount / options.pageSize));
143
+ const scopeLabel = t(`app_list.scope_${options.type}`);
132
144
  const formattedApps = apps.map(formatApp);
133
145
 
134
146
  // stdout 输出 JSON,方便 AI 工具解析
135
147
  console.log(JSON.stringify(formattedApps, null, 2));
136
148
 
137
149
  // stderr 输出人类可读摘要
138
- const { c, success: chalkSuccess, listItem } = require('../core/chalk');
139
- chalkSuccess(`共找到 ${c.cyan}${formattedApps.length}${c.reset} 个应用`);
150
+ const { c, success: chalkSuccess, hint, listItem } = require('../core/chalk');
151
+ chalkSuccess(t(
152
+ 'app_list.found',
153
+ scopeLabel,
154
+ options.pageIndex,
155
+ totalPages,
156
+ formattedApps.length,
157
+ totalCount
158
+ ));
140
159
  for (const app of formattedApps) {
141
160
  listItem(`${c.bold}${app.appName}${c.reset} ${c.dim}[${app.appType}]${c.reset} ${c.cyan}${app.systemLink}${c.reset}`);
142
161
  }
162
+ if (options.pageIndex < totalPages) {
163
+ hint(t(
164
+ 'app_list.next_page',
165
+ `openyida app-list --type ${options.type} --page ${options.pageIndex + 1} --size ${options.pageSize}`
166
+ ));
167
+ }
143
168
  }
144
169
 
145
- module.exports = { run };
170
+ module.exports = { parseArgs, run };
@@ -24,12 +24,13 @@
24
24
  * 3) 第三方依赖以 `window.<别名>` 形式引用(antd→window.antd、react→window.React …),
25
25
  * 这些 UMD 依赖由画布运行时依据 importedModules 白名单按需注入。
26
26
  *
27
- * 因此本地编译 = Babel 把 JSX/TS 转成 ES5 → 把 import 改写成 window 别名引用、
28
- * 把 export default 改写成画布入口 `YidaComp` → 正则抽出依赖包名。
27
+ * 因此本地编译 = Babel 把 JSX/TS 转成 JS → 把 import 改写成 window 别名引用、
28
+ * 把 export default 改写成画布入口 `YidaComp` → UglifyJS 压缩 → 正则抽出依赖包名。
29
29
  */
30
30
 
31
31
  const Babel = require('@babel/standalone');
32
32
  const globals = require('globals');
33
+ const UglifyJS = require('uglify-js');
33
34
  const {
34
35
  assertNoEmojiInArtifactName,
35
36
  assertNoEmojiInText,
@@ -75,6 +76,7 @@ const MODULE_ALIAS_MAP = {
75
76
 
76
77
  const MANUAL_DEPENDENCY_GLOBALS = Object.freeze({
77
78
  React: 'react',
79
+ ReactDOM: 'react-dom',
78
80
  antd: 'antd',
79
81
  icons: '@ant-design/icons',
80
82
  ahooks: 'ahooks',
@@ -88,6 +90,12 @@ const MANUAL_DEPENDENCY_GLOBALS = Object.freeze({
88
90
  YidaMarkdown: 'yida-plugin-markdown',
89
91
  });
90
92
 
93
+ const DEPENDENCY_RUNTIME_ALIASES = Object.freeze(Object.assign(
94
+ {},
95
+ Object.fromEntries(Object.entries(MODULE_ALIAS_MAP).map(([packageName, alias]) => [alias, packageName])),
96
+ { LucideReact: 'lucide-react' }
97
+ ));
98
+
91
99
  const BARE_DEPENDENCY_GLOBALS = Object.freeze({
92
100
  antd: {
93
101
  packageName: 'antd',
@@ -209,6 +217,141 @@ function assertNoManualDependencyGlobals(source, options = {}) {
209
217
  );
210
218
  }
211
219
 
220
+ function findSelfReferentialDependencyBindingIssues(source) {
221
+ const issues = [];
222
+ const dependencyNames = new Set(Object.keys(MANUAL_DEPENDENCY_GLOBALS));
223
+
224
+ const guardPlugin = () => ({
225
+ name: 'yida-self-referential-dependency-binding-guard',
226
+ visitor: {
227
+ VariableDeclarator(path) {
228
+ if (!path.get('id').isIdentifier() || !path.node.init) {
229
+ return;
230
+ }
231
+ const name = path.node.id.name;
232
+ if (!dependencyNames.has(name)) {
233
+ return;
234
+ }
235
+ const binding = path.scope.getBinding(name);
236
+ let selfReference = null;
237
+ const initPath = path.get('init');
238
+ if (
239
+ initPath.isIdentifier({ name })
240
+ && initPath.scope.getBinding(name) === binding
241
+ ) {
242
+ selfReference = initPath.node;
243
+ }
244
+ initPath.traverse({
245
+ ReferencedIdentifier(referencePath) {
246
+ if (
247
+ !selfReference
248
+ && referencePath.node.name === name
249
+ && referencePath.scope.getBinding(name) === binding
250
+ ) {
251
+ selfReference = referencePath.node;
252
+ }
253
+ },
254
+ });
255
+ if (selfReference) {
256
+ issues.push({
257
+ name,
258
+ packageName: MANUAL_DEPENDENCY_GLOBALS[name],
259
+ line: selfReference.loc && selfReference.loc.start ? selfReference.loc.start.line : 0,
260
+ column: selfReference.loc && selfReference.loc.start ? selfReference.loc.start.column : 0,
261
+ });
262
+ }
263
+ },
264
+ },
265
+ });
266
+
267
+ Babel.transform(source, {
268
+ filename: 'canvas.tsx',
269
+ presets: [
270
+ ['typescript', { allExtensions: true, isTSX: true, allowDeclareFields: true }],
271
+ ['react', { runtime: 'classic' }],
272
+ ],
273
+ plugins: [guardPlugin],
274
+ sourceType: 'module',
275
+ compact: false,
276
+ babelrc: false,
277
+ configFile: false,
278
+ });
279
+ return issues;
280
+ }
281
+
282
+ function assertNoSelfReferentialDependencyBindings(source, options = {}) {
283
+ const issues = findSelfReferentialDependencyBindingIssues(source);
284
+ if (issues.length === 0) {
285
+ return;
286
+ }
287
+ const firstIssue = issues[0];
288
+ throw new CliError(
289
+ `Code Canvas 依赖绑定 ${firstIssue.name} 不能引用自身。请从 "${firstIssue.packageName}" 使用标准 import 引入。`,
290
+ {
291
+ code: 'OPENYIDA_CANVAS_SELF_REFERENTIAL_DEPENDENCY_BINDING',
292
+ details: {
293
+ stage: 'canvas_compile',
294
+ sourcePath: options.sourcePath || '',
295
+ line: firstIssue.line,
296
+ column: firstIssue.column,
297
+ globalName: firstIssue.name,
298
+ packageName: firstIssue.packageName,
299
+ },
300
+ }
301
+ );
302
+ }
303
+
304
+ function findDependencyManifestIssues(runtimeCode, importedModules) {
305
+ const importedBaseNames = new Set((importedModules || []).map(packageBaseName));
306
+ const observedAliases = new Set();
307
+ const guardPlugin = ({ types: types }) => ({
308
+ name: 'yida-runtime-dependency-manifest-guard',
309
+ visitor: {
310
+ MemberExpression(path) {
311
+ const node = path.node;
312
+ if (!types.isIdentifier(node.object, { name: 'window' })) {
313
+ return;
314
+ }
315
+ const alias = node.computed && types.isStringLiteral(node.property)
316
+ ? node.property.value
317
+ : (!node.computed && types.isIdentifier(node.property) ? node.property.name : '');
318
+ if (DEPENDENCY_RUNTIME_ALIASES[alias]) {
319
+ observedAliases.add(alias);
320
+ }
321
+ },
322
+ },
323
+ });
324
+ Babel.transform(runtimeCode, {
325
+ filename: 'canvas-runtime.js',
326
+ plugins: [guardPlugin],
327
+ sourceType: 'script',
328
+ compact: false,
329
+ babelrc: false,
330
+ configFile: false,
331
+ });
332
+ return Array.from(observedAliases)
333
+ .filter(alias => !importedBaseNames.has(packageBaseName(DEPENDENCY_RUNTIME_ALIASES[alias])))
334
+ .map(alias => ({ alias, packageName: DEPENDENCY_RUNTIME_ALIASES[alias] }));
335
+ }
336
+
337
+ function assertDependencyManifestConsistent(runtimeCode, importedModules, options = {}) {
338
+ const issues = findDependencyManifestIssues(runtimeCode, importedModules);
339
+ if (issues.length === 0) {
340
+ return;
341
+ }
342
+ throw new CliError(
343
+ `Code Canvas 运行时代码依赖 ${issues[0].alias},但 importedModules 缺少 "${issues[0].packageName}"。`,
344
+ {
345
+ code: 'OPENYIDA_CANVAS_DEPENDENCY_MANIFEST_MISMATCH',
346
+ details: {
347
+ stage: 'canvas_compile',
348
+ sourcePath: options.sourcePath || '',
349
+ issues,
350
+ },
351
+ }
352
+ );
353
+ }
354
+
212
355
  function findBareDependencyGlobalIssues(source) {
213
356
  const issues = [];
214
357
  const names = new Set(Object.keys(BARE_DEPENDENCY_GLOBALS));
@@ -735,6 +878,36 @@ function assertCanvasRuntimeParseable(runtimeCode, options = {}) {
735
878
  }
736
879
  }
737
880
 
881
+ /**
882
+ * 压缩画布运行态代码,并显式保留装配器读取的 YidaComp 入口名。
883
+ */
884
+ function minifyCanvasRuntime(runtimeCode, options = {}) {
885
+ let result;
886
+ try {
887
+ result = UglifyJS.minify(runtimeCode, {
888
+ compress: true,
889
+ mangle: {
890
+ reserved: ['YidaComp'],
891
+ },
892
+ });
893
+ } catch (error) {
894
+ result = { error };
895
+ }
896
+ if (!result || result.error || typeof result.code !== 'string' || !result.code) {
897
+ const detail = result && result.error && result.error.message
898
+ ? result.error.message
899
+ : '压缩结果为空';
900
+ throw new CliError(`Code Canvas runtimeCode 压缩失败: ${detail}`, {
901
+ code: 'OPENYIDA_CANVAS_MINIFY_FAILED',
902
+ details: {
903
+ stage: 'canvas_minify',
904
+ sourcePath: options.sourcePath || '',
905
+ },
906
+ });
907
+ }
908
+ return result.code;
909
+ }
910
+
738
911
  /**
739
912
  * 本地编译 Code Canvas 源码。
740
913
  * @param {string} source 原始 React/JSX/TSX 源码
@@ -790,6 +963,7 @@ function compileCanvasLocal(source, options = {}) {
790
963
  });
791
964
  }
792
965
  assertNoUnavailableCanvasInstanceApis(source, options);
966
+ assertNoSelfReferentialDependencyBindings(source, options);
793
967
  assertNoBareDependencyGlobals(source, options);
794
968
  assertNoManualDependencyGlobals(source, options);
795
969
  assertInlineCssStructure(source, options);
@@ -841,12 +1015,14 @@ function compileCanvasLocal(source, options = {}) {
841
1015
  configFile: false,
842
1016
  });
843
1017
 
844
- const runtimeCode = stage2.code || '';
845
- assertNoEmojiInText(runtimeCode, {
1018
+ const unminifiedRuntimeCode = stage2.code || '';
1019
+ assertNoEmojiInText(unminifiedRuntimeCode, {
846
1020
  artifact: options.sourcePath ? options.sourcePath + ' runtime' : 'Code Canvas runtime',
847
1021
  code: 'OPENYIDA_CANVAS_SOURCE_EMOJI_FORBIDDEN',
848
1022
  });
1023
+ const runtimeCode = minifyCanvasRuntime(unminifiedRuntimeCode, options);
849
1024
  assertCanvasRuntimeParseable(runtimeCode, options);
1025
+ assertDependencyManifestConsistent(runtimeCode, importedModules, options);
850
1026
  return {
851
1027
  runtimeCode,
852
1028
  importedModules: JSON.stringify(importedModules),
@@ -912,6 +1088,10 @@ module.exports = {
912
1088
  resolveWindowAlias,
913
1089
  shouldAllowUnsupportedBareImports,
914
1090
  assertCanvasRuntimeParseable,
1091
+ minifyCanvasRuntime,
915
1092
  findBareDependencyGlobalIssues,
1093
+ findSelfReferentialDependencyBindingIssues,
1094
+ findDependencyManifestIssues,
1095
+ assertDependencyManifestConsistent,
916
1096
  MODULE_ALIAS_MAP,
917
1097
  };
@@ -23,10 +23,30 @@ function fingerprint(value) {
23
23
  return crypto.createHash('sha256').update(normalized).digest('hex');
24
24
  }
25
25
 
26
+ function rawFingerprint(value) {
27
+ if (typeof value !== 'string' || !value) {
28
+ return '';
29
+ }
30
+ return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
31
+ }
32
+
33
+ function positiveSize(value) {
34
+ const size = Number(value);
35
+ return Number.isFinite(size) && size > 0 ? size : 0;
36
+ }
37
+
38
+ function sha256(value) {
39
+ const digest = String(value || '').toLowerCase();
40
+ return /^[a-f0-9]{64}$/.test(digest) ? digest : '';
41
+ }
42
+
26
43
  function createEmptyDisplayInfo() {
27
44
  return {
28
45
  hasYidaCodeCanvas: false,
46
+ hasCodeBundle: false,
29
47
  hasNativeJsx: false,
48
+ codeBundleCount: 0,
49
+ bundleIds: [],
30
50
  runtimeCodeBytes: 0,
31
51
  sourceCodeBytes: 0,
32
52
  compiledCodeBytes: 0,
@@ -34,6 +54,8 @@ function createEmptyDisplayInfo() {
34
54
  componentCount: 0,
35
55
  canvasRuntimeCode: '',
36
56
  canvasSourceCode: '',
57
+ canvasRuntimeSha256: '',
58
+ canvasSourceSha256: '',
37
59
  nativeCompiledCode: '',
38
60
  nativeSourceCode: '',
39
61
  };
@@ -79,12 +101,37 @@ function traverseDisplayNodes(node, info) {
79
101
 
80
102
  if (node.componentName === 'YidaCodeCanvas') {
81
103
  const props = node.props || {};
104
+ const codeBundle = props.codeBundle && typeof props.codeBundle === 'object'
105
+ ? props.codeBundle
106
+ : null;
82
107
  info.hasYidaCodeCanvas = true;
83
108
  info.componentCount++;
84
- info.canvasRuntimeCode = props.runtimeCode || '';
85
- info.canvasSourceCode = props.code || '';
109
+ if (typeof props.runtimeCode === 'string' && props.runtimeCode) {
110
+ info.canvasRuntimeCode = props.runtimeCode;
111
+ }
112
+ if (typeof props.code === 'string' && props.code) {
113
+ info.canvasSourceCode = props.code;
114
+ }
86
115
  info.runtimeCodeBytes += codeBytes(props.runtimeCode);
87
116
  info.sourceCodeBytes += codeBytes(props.code);
117
+ if (codeBundle) {
118
+ const bundleId = String(codeBundle.bundleId || '');
119
+ const runtime = codeBundle.runtime && typeof codeBundle.runtime === 'object'
120
+ ? codeBundle.runtime
121
+ : {};
122
+ const source = codeBundle.source && typeof codeBundle.source === 'object'
123
+ ? codeBundle.source
124
+ : {};
125
+ info.hasCodeBundle = true;
126
+ info.codeBundleCount++;
127
+ if (bundleId && !info.bundleIds.includes(bundleId)) {
128
+ info.bundleIds.push(bundleId);
129
+ }
130
+ info.runtimeCodeBytes += positiveSize(runtime.size);
131
+ info.sourceCodeBytes += positiveSize(source.size);
132
+ info.canvasRuntimeSha256 = sha256(runtime.sha256) || info.canvasRuntimeSha256;
133
+ info.canvasSourceSha256 = sha256(source.sha256) || info.canvasSourceSha256;
134
+ }
88
135
  addImportedModules(info.importedModules, props.importedModules);
89
136
  } else if (node.componentName === 'Jsx') {
90
137
  info.hasNativeJsx = true;
@@ -133,7 +180,9 @@ function hasExpectedDisplayComponent(info, publishMode) {
133
180
  return false;
134
181
  }
135
182
  if (publishMode === 'canvas') {
136
- return info.hasYidaCodeCanvas && info.runtimeCodeBytes > 0;
183
+ return info.hasYidaCodeCanvas && (
184
+ info.runtimeCodeBytes > 0 || !!info.canvasRuntimeSha256
185
+ );
137
186
  }
138
187
  return info.hasNativeJsx && info.compiledCodeBytes > 0;
139
188
  }
@@ -144,7 +193,10 @@ function summarizeDisplayPublishInfo(info) {
144
193
  }
145
194
  return {
146
195
  hasYidaCodeCanvas: info.hasYidaCodeCanvas,
196
+ hasCodeBundle: info.hasCodeBundle,
147
197
  hasNativeJsx: info.hasNativeJsx,
198
+ codeBundleCount: info.codeBundleCount,
199
+ bundleIds: info.bundleIds.slice(),
148
200
  runtimeCodeBytes: info.runtimeCodeBytes,
149
201
  sourceCodeBytes: info.sourceCodeBytes,
150
202
  compiledCodeBytes: info.compiledCodeBytes,
@@ -159,8 +211,13 @@ function verifyPublishedContentMatch(readbackSchema, expectedSchemaContent, publ
159
211
  const displayComponentPresent = hasExpectedDisplayComponent(readbackInfo, publishMode);
160
212
  const readbackArtifact = getPublishArtifact(readbackInfo, publishMode);
161
213
  const expectedArtifact = getPublishArtifact(expectedInfo, publishMode);
162
- const readbackFingerprint = fingerprint(readbackArtifact);
163
- const expectedFingerprint = fingerprint(expectedArtifact);
214
+ const codeBundleReadback = publishMode === 'canvas' && readbackInfo && readbackInfo.hasCodeBundle;
215
+ const readbackFingerprint = codeBundleReadback
216
+ ? readbackInfo.canvasRuntimeSha256
217
+ : fingerprint(readbackArtifact);
218
+ const expectedFingerprint = codeBundleReadback
219
+ ? rawFingerprint(expectedArtifact)
220
+ : fingerprint(expectedArtifact);
164
221
 
165
222
  return {
166
223
  readbackInfo,
@@ -180,6 +237,7 @@ function verifyPublishedContentMatch(readbackSchema, expectedSchemaContent, publ
180
237
  module.exports = {
181
238
  extractDisplayPublishInfo,
182
239
  fingerprint,
240
+ rawFingerprint,
183
241
  hasExpectedDisplayComponent,
184
242
  parseSchemaContent,
185
243
  summarizeDisplayPublishInfo,