openxiangda 1.0.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 (121) hide show
  1. package/README.md +58 -0
  2. package/bin/openxiangda.js +11 -0
  3. package/lib/cli.js +2423 -0
  4. package/lib/config.js +121 -0
  5. package/lib/http.js +47 -0
  6. package/lib/skills.js +371 -0
  7. package/lib/utils.js +87 -0
  8. package/lib/workspace-init.js +139 -0
  9. package/openxiangda-skills/SKILL.md +128 -0
  10. package/openxiangda-skills/references/architecture-patterns.md +242 -0
  11. package/openxiangda-skills/references/automation-v3.md +129 -0
  12. package/openxiangda-skills/references/component-guide.md +198 -0
  13. package/openxiangda-skills/references/forms/component-registry.md +53 -0
  14. package/openxiangda-skills/references/forms/form-schema.md +109 -0
  15. package/openxiangda-skills/references/forms/layout-and-rules.md +24 -0
  16. package/openxiangda-skills/references/openxiangda-api.md +466 -0
  17. package/openxiangda-skills/references/pages/page-sdk.md +13 -0
  18. package/openxiangda-skills/references/pages/publish-flow.md +36 -0
  19. package/openxiangda-skills/references/pages/workspace-structure.md +38 -0
  20. package/openxiangda-skills/references/permissions-settings.md +147 -0
  21. package/openxiangda-skills/references/platform-data-model.md +305 -0
  22. package/openxiangda-skills/references/style-system.md +492 -0
  23. package/openxiangda-skills/references/troubleshooting.md +246 -0
  24. package/openxiangda-skills/references/workflow-v3.md +105 -0
  25. package/openxiangda-skills/references/workspace-state.md +45 -0
  26. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +64 -0
  27. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +143 -0
  28. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +76 -0
  29. package/openxiangda-skills/skills/openxiangda-inspect/SKILL.md +40 -0
  30. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +62 -0
  31. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +95 -0
  32. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +97 -0
  33. package/package.json +126 -0
  34. package/packages/sdk/bin/lowcode-workspace.mjs +4 -0
  35. package/packages/sdk/dist/build/index.cjs +33 -0
  36. package/packages/sdk/dist/build/index.cjs.map +1 -0
  37. package/packages/sdk/dist/build/index.d.mts +40 -0
  38. package/packages/sdk/dist/build/index.d.ts +40 -0
  39. package/packages/sdk/dist/build/index.mjs +8 -0
  40. package/packages/sdk/dist/build/index.mjs.map +1 -0
  41. package/packages/sdk/dist/components/index.cjs +18700 -0
  42. package/packages/sdk/dist/components/index.cjs.map +1 -0
  43. package/packages/sdk/dist/components/index.d.mts +2094 -0
  44. package/packages/sdk/dist/components/index.d.ts +2094 -0
  45. package/packages/sdk/dist/components/index.mjs +18649 -0
  46. package/packages/sdk/dist/components/index.mjs.map +1 -0
  47. package/packages/sdk/dist/runtime/index.cjs +1469 -0
  48. package/packages/sdk/dist/runtime/index.cjs.map +1 -0
  49. package/packages/sdk/dist/runtime/index.d.mts +831 -0
  50. package/packages/sdk/dist/runtime/index.d.ts +831 -0
  51. package/packages/sdk/dist/runtime/index.mjs +1420 -0
  52. package/packages/sdk/dist/runtime/index.mjs.map +1 -0
  53. package/packages/sdk/dist/styles/antd-theme.cjs +60 -0
  54. package/packages/sdk/dist/styles/antd-theme.cjs.map +1 -0
  55. package/packages/sdk/dist/styles/antd-theme.d.mts +5 -0
  56. package/packages/sdk/dist/styles/antd-theme.d.ts +5 -0
  57. package/packages/sdk/dist/styles/antd-theme.mjs +35 -0
  58. package/packages/sdk/dist/styles/antd-theme.mjs.map +1 -0
  59. package/packages/sdk/dist/styles/tailwind-preset.cjs +2641 -0
  60. package/packages/sdk/dist/styles/tailwind-preset.cjs.map +1 -0
  61. package/packages/sdk/dist/styles/tailwind-preset.d.mts +75 -0
  62. package/packages/sdk/dist/styles/tailwind-preset.d.ts +75 -0
  63. package/packages/sdk/dist/styles/tailwind-preset.mjs +2618 -0
  64. package/packages/sdk/dist/styles/tailwind-preset.mjs.map +1 -0
  65. package/packages/sdk/dist/styles/tokens.css +73 -0
  66. package/packages/sdk/src/build-source/README.md +9 -0
  67. package/packages/sdk/src/build-source/bin/lowcode-workspace.mjs +7 -0
  68. package/packages/sdk/src/build-source/package.json +34 -0
  69. package/packages/sdk/src/build-source/scripts/build-forms.mjs +824 -0
  70. package/packages/sdk/src/build-source/scripts/build-forms.runtime-entry.test.ts +18 -0
  71. package/packages/sdk/src/build-source/scripts/build-pages.mjs +793 -0
  72. package/packages/sdk/src/build-source/scripts/build-workspace.mjs +64 -0
  73. package/packages/sdk/src/build-source/scripts/publish-all.mjs +127 -0
  74. package/packages/sdk/src/build-source/scripts/publish-oss.mjs +149 -0
  75. package/packages/sdk/src/build-source/scripts/register-bundle.mjs +1 -0
  76. package/packages/sdk/src/build-source/scripts/register.mjs +329 -0
  77. package/packages/sdk/src/build-source/scripts/sync-schema.mjs +301 -0
  78. package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +639 -0
  79. package/packages/sdk/src/build-source/scripts/utils/form-api.test.ts +244 -0
  80. package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.mjs +57 -0
  81. package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.test.ts +135 -0
  82. package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +210 -0
  83. package/packages/sdk/src/build-source/scripts/utils/load-config.mjs +257 -0
  84. package/packages/sdk/src/build-source/scripts/utils/load-config.test.ts +44 -0
  85. package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -0
  86. package/packages/sdk/src/build-source/scripts/utils/namespace-css.mjs +61 -0
  87. package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -0
  88. package/packages/sdk/src/build-source/scripts/utils/pages.mjs +80 -0
  89. package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -0
  90. package/packages/sdk/src/build-source/scripts/utils/register-payload.mjs +89 -0
  91. package/packages/sdk/src/build-source/scripts/utils/register-payload.test.ts +76 -0
  92. package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.mjs +44 -0
  93. package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.test.ts +54 -0
  94. package/packages/sdk/src/build-source/scripts/utils/schema-transform.mjs +130 -0
  95. package/packages/sdk/src/build-source/scripts/utils/schema-transform.test.ts +141 -0
  96. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +227 -0
  97. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.test.ts +187 -0
  98. package/packages/sdk/src/build-source/src/cli.mjs +679 -0
  99. package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +34 -0
  100. package/templates/sy-lowcode-app-workspace/examples/forms/customer/page.tsx +1 -0
  101. package/templates/sy-lowcode-app-workspace/examples/forms/customer/schema.ts +35 -0
  102. package/templates/sy-lowcode-app-workspace/index.html +12 -0
  103. package/templates/sy-lowcode-app-workspace/package.json +49 -0
  104. package/templates/sy-lowcode-app-workspace/postcss.config.cjs +6 -0
  105. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +100 -0
  106. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +26 -0
  107. package/templates/sy-lowcode-app-workspace/src/forms/.gitkeep +1 -0
  108. package/templates/sy-lowcode-app-workspace/src/forms/README.md +48 -0
  109. package/templates/sy-lowcode-app-workspace/src/index.css +28 -0
  110. package/templates/sy-lowcode-app-workspace/src/js-code-nodes/.gitkeep +1 -0
  111. package/templates/sy-lowcode-app-workspace/src/js-code-nodes/types.d.ts +3 -0
  112. package/templates/sy-lowcode-app-workspace/src/main.tsx +36 -0
  113. package/templates/sy-lowcode-app-workspace/src/pages/.gitkeep +1 -0
  114. package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +128 -0
  115. package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +31 -0
  116. package/templates/sy-lowcode-app-workspace/tailwind.config.cjs +30 -0
  117. package/templates/sy-lowcode-app-workspace/tsconfig.app.json +24 -0
  118. package/templates/sy-lowcode-app-workspace/tsconfig.js-code-nodes.json +15 -0
  119. package/templates/sy-lowcode-app-workspace/tsconfig.json +7 -0
  120. package/templates/sy-lowcode-app-workspace/tsconfig.node.json +10 -0
  121. package/templates/sy-lowcode-app-workspace/vite.config.ts +32 -0
@@ -0,0 +1,639 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getApiBaseUrl } from "./load-config.mjs";
4
+
5
+ function readExportedStringConstant(filePath, exportName) {
6
+ if (!filePath || !fs.existsSync(filePath)) return "";
7
+ const content = fs.readFileSync(filePath, "utf-8");
8
+ const escaped = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
+ const match = content.match(
10
+ new RegExp(`export\\s+const\\s+${escaped}\\s*=\\s*(['"])(.*?)\\1`),
11
+ );
12
+ return match?.[2]?.trim() || "";
13
+ }
14
+
15
+ function resolveRelativeImportPath(schemaPath, source) {
16
+ if (!source.startsWith(".")) return "";
17
+ const basePath = path.resolve(path.dirname(schemaPath), source);
18
+ const candidates = [
19
+ basePath,
20
+ `${basePath}.ts`,
21
+ `${basePath}.tsx`,
22
+ `${basePath}.js`,
23
+ `${basePath}.mjs`,
24
+ path.join(basePath, "index.ts"),
25
+ path.join(basePath, "index.tsx"),
26
+ path.join(basePath, "index.js"),
27
+ ];
28
+ return candidates.find((candidate) => fs.existsSync(candidate)) || "";
29
+ }
30
+
31
+ function findImportedStringConstantSource(content, schemaPath, identifier) {
32
+ if (!schemaPath || !identifier) return "";
33
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
34
+ let match;
35
+ while ((match = importRegex.exec(content))) {
36
+ const specifiers = match[1]
37
+ .split(",")
38
+ .map((item) => item.trim())
39
+ .filter(Boolean);
40
+ const sourcePath = resolveRelativeImportPath(schemaPath, match[2]);
41
+ if (!sourcePath) continue;
42
+
43
+ for (const specifier of specifiers) {
44
+ const aliasMatch = specifier.match(
45
+ /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/,
46
+ );
47
+ if (!aliasMatch) continue;
48
+ const exportedName = aliasMatch[1];
49
+ const localName = aliasMatch[2] || exportedName;
50
+ if (localName !== identifier) continue;
51
+ return { sourcePath, exportedName };
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function readImportedStringConstant(content, schemaPath, identifier) {
58
+ const imported = findImportedStringConstantSource(
59
+ content,
60
+ schemaPath,
61
+ identifier,
62
+ );
63
+ if (!imported) return "";
64
+ return readExportedStringConstant(imported.sourcePath, imported.exportedName);
65
+ }
66
+
67
+ function readPropIdentifier(content, propName) {
68
+ const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
69
+ const identifierMatch = content.match(
70
+ new RegExp(`${escaped}\\s*:\\s*([A-Za-z_$][\\w$]*)`),
71
+ );
72
+ return identifierMatch?.[1] || "";
73
+ }
74
+
75
+ function writeStringConstant(filePath, identifier, value) {
76
+ const content = fs.readFileSync(filePath, "utf-8");
77
+ const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
78
+ const nextContent = content.replace(
79
+ new RegExp(`((?:export\\s+)?const\\s+${escaped}\\s*=\\s*)([^;]+);`),
80
+ (_match, prefix, initializer) => {
81
+ const quote = initializer.trim().startsWith("'") ? "'" : '"';
82
+ return `${prefix}${quote}${value}${quote};`;
83
+ },
84
+ );
85
+ if (nextContent === content) return false;
86
+ fs.writeFileSync(filePath, nextContent, "utf-8");
87
+ return true;
88
+ }
89
+
90
+ export function writeSchemaStringProp(schemaPath, propName, value) {
91
+ const content = fs.readFileSync(schemaPath, "utf-8");
92
+ const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
93
+ const literalRegex = new RegExp(
94
+ `(${escaped}\\s*:\\s*)(['"])([^'"]*)\\2`,
95
+ );
96
+ const literalNextContent = content.replace(
97
+ literalRegex,
98
+ (_match, prefix, quote) => `${prefix}${quote}${value}${quote}`,
99
+ );
100
+ if (literalNextContent !== content) {
101
+ fs.writeFileSync(schemaPath, literalNextContent, "utf-8");
102
+ return;
103
+ }
104
+
105
+ const identifier = readPropIdentifier(content, propName);
106
+ if (!identifier) {
107
+ throw new Error(`schema.ts 中未找到 ${propName} 字段,无法回写`);
108
+ }
109
+
110
+ if (writeStringConstant(schemaPath, identifier, value)) {
111
+ return;
112
+ }
113
+
114
+ const imported = findImportedStringConstantSource(
115
+ content,
116
+ schemaPath,
117
+ identifier,
118
+ );
119
+ if (
120
+ imported &&
121
+ writeStringConstant(imported.sourcePath, imported.exportedName, value)
122
+ ) {
123
+ return;
124
+ }
125
+
126
+ throw new Error(`schema.ts 中未找到可回写的 ${propName} 字段,无法回写`);
127
+ }
128
+
129
+ export function writeSchemaFormUuid(schemaPath, formUuid) {
130
+ writeSchemaStringProp(schemaPath, "formUuid", formUuid);
131
+ }
132
+
133
+ export function writeSchemaAppType(schemaPath, appType) {
134
+ writeSchemaStringProp(schemaPath, "appType", appType);
135
+ }
136
+
137
+ function resolveTargetAppType(config, meta) {
138
+ if (isOpenXiangdaMode(config)) {
139
+ return config.appType;
140
+ }
141
+ return meta.appType || config.appType;
142
+ }
143
+
144
+ function isStaleOpenXiangdaBinding(config, meta, targetAppType) {
145
+ return Boolean(
146
+ isOpenXiangdaMode(config) &&
147
+ meta.formUuid &&
148
+ meta.appType &&
149
+ meta.appType !== targetAppType,
150
+ );
151
+ }
152
+
153
+ function syncSchemaAppTypeIfNeeded(schemaPath, meta, targetAppType) {
154
+ if (!targetAppType || meta.appType === targetAppType) return;
155
+ if (!meta.appType && !meta.content.includes("appType")) return;
156
+ writeSchemaAppType(schemaPath, targetAppType);
157
+ }
158
+
159
+ function syncCreatedFormMeta(schemaPath, meta, targetAppType, formUuid) {
160
+ writeSchemaFormUuid(schemaPath, formUuid);
161
+ syncSchemaAppTypeIfNeeded(schemaPath, meta, targetAppType);
162
+ }
163
+
164
+ function readStringProp(content, propName, schemaPath) {
165
+ const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
+ const literalMatch = content.match(
167
+ new RegExp(`${escaped}\\s*:\\s*(['"])(.*?)\\1`),
168
+ );
169
+ if (literalMatch) return literalMatch[2]?.trim() || "";
170
+
171
+ const identifier = readPropIdentifier(content, propName);
172
+ if (!identifier) return "";
173
+
174
+ const localConstMatch = content.match(
175
+ new RegExp(`(?:export\\s+)?const\\s+${identifier}\\s*=\\s*(['"])(.*?)\\1`),
176
+ );
177
+ if (localConstMatch) return localConstMatch[2]?.trim() || "";
178
+
179
+ return readImportedStringConstant(content, schemaPath, identifier);
180
+ }
181
+
182
+ function hasObjectProp(content, propName) {
183
+ const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
184
+ return new RegExp(`${escaped}\\s*:`).test(content);
185
+ }
186
+
187
+ export function readSchemaMeta(schemaPath) {
188
+ const content = fs.readFileSync(schemaPath, "utf-8");
189
+
190
+ return {
191
+ content,
192
+ hasFormUuidProp: hasObjectProp(content, "formUuid"),
193
+ formUuid: readStringProp(content, "formUuid", schemaPath),
194
+ appType: readStringProp(content, "appType", schemaPath),
195
+ title: readStringProp(content, "title", schemaPath),
196
+ formType: readStringProp(content, "formType", schemaPath) || "receipt",
197
+ relateUuid: readStringProp(content, "relateUuid", schemaPath),
198
+ };
199
+ }
200
+
201
+ export function assertRequiredOpenApiConfig(config) {
202
+ const missing = [];
203
+ if (!config.appType) missing.push("APP_TYPE");
204
+ if (!config.platformUrl) missing.push("APP_PLATFORM_URL");
205
+
206
+ if (isOpenXiangdaMode(config)) {
207
+ if (!config.openXiangdaAccessToken) {
208
+ missing.push("OPENXIANGDA_ACCESS_TOKEN");
209
+ }
210
+ } else {
211
+ if (!config.appKey) missing.push("APP_KEY");
212
+ if (!config.appSecret) missing.push("APP_SECRET");
213
+ if (!config.userId) missing.push("APP_USER_ID");
214
+ }
215
+
216
+ if (missing.length > 0) {
217
+ throw new Error(`缺少必要配置: ${missing.join(", ")}`);
218
+ }
219
+ }
220
+
221
+ export function isOpenXiangdaMode(config) {
222
+ return Boolean(config.openXiangdaAccessToken);
223
+ }
224
+
225
+ function getAuthHeaders(config, accessToken) {
226
+ if (isOpenXiangdaMode(config)) {
227
+ return {
228
+ Authorization: `Bearer ${accessToken || config.openXiangdaAccessToken}`,
229
+ };
230
+ }
231
+ return {
232
+ "x-acs-dingtalk-access-token": accessToken,
233
+ };
234
+ }
235
+
236
+ export async function getOpenApiAccessToken(config) {
237
+ assertRequiredOpenApiConfig(config);
238
+ if (isOpenXiangdaMode(config)) {
239
+ return config.openXiangdaAccessToken;
240
+ }
241
+
242
+ const apiBase = getApiBaseUrl(config);
243
+ const res = await fetch(`${apiBase}/dingtalk-api/v1.0/oauth2/accessToken`, {
244
+ method: "POST",
245
+ headers: {
246
+ "Content-Type": "application/json",
247
+ },
248
+ body: JSON.stringify({
249
+ appKey: config.appKey,
250
+ appSecret: config.appSecret,
251
+ }),
252
+ });
253
+
254
+ let body = null;
255
+ try {
256
+ body = await res.json();
257
+ } catch {
258
+ body = null;
259
+ }
260
+
261
+ if (!res.ok || !body?.accessToken) {
262
+ const message = body?.message || res.statusText || "unknown error";
263
+ throw new Error(`获取 accessToken 失败: HTTP ${res.status} ${message}`);
264
+ }
265
+
266
+ return body.accessToken;
267
+ }
268
+
269
+ export async function createOpenApiForm(config, accessToken, formOptions) {
270
+ assertRequiredOpenApiConfig(config);
271
+
272
+ const apiBase = getApiBaseUrl(config);
273
+ const targetAppType = formOptions.appType || config.appType;
274
+ const payload = {
275
+ name: formOptions.name,
276
+ formType: formOptions.formType || "receipt",
277
+ relateUuid: formOptions.relateUuid || "",
278
+ };
279
+ const builderVersion =
280
+ formOptions.builderVersion || config.defaults?.formBuilderVersion || "2.0";
281
+ if (
282
+ builderVersion === "2.0" &&
283
+ ["receipt", "process"].includes(String(payload.formType || ""))
284
+ ) {
285
+ payload.builderVersion = "2.0";
286
+ }
287
+ if (formOptions.formUuid) {
288
+ payload.formUuid = formOptions.formUuid;
289
+ }
290
+
291
+ if (isOpenXiangdaMode(config)) {
292
+ payload.createMenu = formOptions.createMenu !== false;
293
+ payload.menuParentId = config.menu?.parentId || "";
294
+ payload.menuIcon = config.menu?.icon || "";
295
+ } else {
296
+ payload.userId = config.userId;
297
+ payload.appType = targetAppType;
298
+ }
299
+
300
+ const url = isOpenXiangdaMode(config)
301
+ ? `${apiBase}/openxiangda-api/v1/apps/${encodeURIComponent(targetAppType)}/forms`
302
+ : `${apiBase}/dingtalk-api/v1.0/forms/createForm`;
303
+
304
+ const res = await fetch(url, {
305
+ method: "POST",
306
+ headers: {
307
+ "Content-Type": "application/json",
308
+ ...getAuthHeaders(config, accessToken),
309
+ },
310
+ body: JSON.stringify(payload),
311
+ });
312
+
313
+ let body = null;
314
+ try {
315
+ body = await res.json();
316
+ } catch {
317
+ body = null;
318
+ }
319
+
320
+ if (!res.ok || body?.code !== 200) {
321
+ const message = body?.message || res.statusText || "unknown error";
322
+ throw new Error(`创建表单失败: HTTP ${res.status} ${message}`);
323
+ }
324
+
325
+ const data = body?.data?.form || body?.data || body;
326
+ const formUuid = data?.formUuid;
327
+ if (!formUuid) {
328
+ throw new Error("创建表单成功但响应中没有 formUuid");
329
+ }
330
+
331
+ return {
332
+ ...data,
333
+ formUuid,
334
+ };
335
+ }
336
+
337
+ export async function listOpenApiForms(config, accessToken, appType) {
338
+ assertRequiredOpenApiConfig(config);
339
+
340
+ const apiBase = getApiBaseUrl(config);
341
+ const targetAppType = appType || config.appType;
342
+ const url = isOpenXiangdaMode(config)
343
+ ? `${apiBase}/openxiangda-api/v1/apps/${encodeURIComponent(targetAppType)}/forms`
344
+ : `${apiBase}/dingtalk-api/v1.0/forms/getFormList`;
345
+ const res = await fetch(url, {
346
+ method: isOpenXiangdaMode(config) ? "GET" : "POST",
347
+ headers: {
348
+ "Content-Type": "application/json",
349
+ ...getAuthHeaders(config, accessToken),
350
+ },
351
+ body: isOpenXiangdaMode(config)
352
+ ? undefined
353
+ : JSON.stringify({
354
+ userId: config.userId,
355
+ appType: targetAppType,
356
+ }),
357
+ });
358
+
359
+ let body = null;
360
+ try {
361
+ body = await res.json();
362
+ } catch {
363
+ body = null;
364
+ }
365
+
366
+ if (!res.ok || body?.code !== 200) {
367
+ const message = body?.message || res.statusText || "unknown error";
368
+ throw new Error(`查询表单失败: HTTP ${res.status} ${message}`);
369
+ }
370
+
371
+ return Array.isArray(body?.data) ? body.data : Array.isArray(body) ? body : [];
372
+ }
373
+
374
+ export async function getOpenApiForm(
375
+ config,
376
+ accessToken,
377
+ { appType, formUuid },
378
+ ) {
379
+ if (!formUuid) return null;
380
+ const forms = await listOpenApiForms(
381
+ config,
382
+ accessToken,
383
+ appType || config.appType,
384
+ );
385
+ return (
386
+ forms.find(
387
+ (item) =>
388
+ item?.formUuid === formUuid &&
389
+ (!appType || !item?.appType || item.appType === appType),
390
+ ) || null
391
+ );
392
+ }
393
+
394
+ export async function listOpenApiMenus(config, accessToken, appType) {
395
+ assertRequiredOpenApiConfig(config);
396
+
397
+ const apiBase = getApiBaseUrl(config);
398
+ const targetAppType = appType || config.appType;
399
+ const url = isOpenXiangdaMode(config)
400
+ ? `${apiBase}/openxiangda-api/v1/apps/${encodeURIComponent(targetAppType)}/menus`
401
+ : `${apiBase}/dingtalk-api/v1.0/forms/getMenusByAppType`;
402
+ const res = await fetch(url, {
403
+ method: isOpenXiangdaMode(config) ? "GET" : "POST",
404
+ headers: {
405
+ "Content-Type": "application/json",
406
+ ...getAuthHeaders(config, accessToken),
407
+ },
408
+ body: isOpenXiangdaMode(config)
409
+ ? undefined
410
+ : JSON.stringify({
411
+ userId: config.userId,
412
+ appType: targetAppType,
413
+ }),
414
+ });
415
+
416
+ let body = null;
417
+ try {
418
+ body = await res.json();
419
+ } catch {
420
+ body = null;
421
+ }
422
+
423
+ if (!res.ok || body?.code !== 200) {
424
+ const message = body?.message || res.statusText || "unknown error";
425
+ throw new Error(`查询菜单失败: HTTP ${res.status} ${message}`);
426
+ }
427
+
428
+ return Array.isArray(body?.data) ? body.data : Array.isArray(body) ? body : [];
429
+ }
430
+
431
+ function flattenMenus(menus) {
432
+ const result = [];
433
+ const visit = (menu) => {
434
+ result.push(menu);
435
+ if (Array.isArray(menu.children)) {
436
+ for (const child of menu.children) {
437
+ visit(child);
438
+ }
439
+ }
440
+ };
441
+
442
+ for (const menu of menus) {
443
+ visit(menu);
444
+ }
445
+
446
+ return result;
447
+ }
448
+
449
+ export async function ensureOpenApiMenuForForm(
450
+ config,
451
+ accessToken,
452
+ { appType, formUuid, name, formType = "receipt", parentId = "", icon = "" },
453
+ ) {
454
+ assertRequiredOpenApiConfig(config);
455
+
456
+ const targetAppType = appType || config.appType;
457
+ const menus = flattenMenus(
458
+ await listOpenApiMenus(config, accessToken, targetAppType),
459
+ );
460
+ const existing = menus.find((menu) => menu.formUuid === formUuid);
461
+ if (existing) {
462
+ return {
463
+ created: false,
464
+ menu: existing,
465
+ };
466
+ }
467
+
468
+ if (isOpenXiangdaMode(config)) {
469
+ return {
470
+ created: false,
471
+ menu: null,
472
+ };
473
+ }
474
+
475
+ const apiBase = getApiBaseUrl(config);
476
+ const payload = {
477
+ userId: config.userId,
478
+ appType: targetAppType,
479
+ name,
480
+ type: formType || "receipt",
481
+ formUuid,
482
+ parentId: parentId || null,
483
+ icon: icon || null,
484
+ };
485
+
486
+ const res = await fetch(`${apiBase}/dingtalk-api/v1.0/forms/menu/create`, {
487
+ method: "POST",
488
+ headers: {
489
+ "Content-Type": "application/json",
490
+ "x-acs-dingtalk-access-token": accessToken,
491
+ },
492
+ body: JSON.stringify(payload),
493
+ });
494
+
495
+ let body = null;
496
+ try {
497
+ body = await res.json();
498
+ } catch {
499
+ body = null;
500
+ }
501
+
502
+ if (!res.ok || body?.code !== 200) {
503
+ const message = body?.message || res.statusText || "unknown error";
504
+ throw new Error(`创建菜单失败: HTTP ${res.status} ${message}`);
505
+ }
506
+
507
+ return {
508
+ created: true,
509
+ menu: body.data,
510
+ };
511
+ }
512
+
513
+ export async function ensureSchemaFormUuid({
514
+ config,
515
+ schemaPath,
516
+ formName,
517
+ accessToken,
518
+ dryRun = false,
519
+ ensureMenu = false,
520
+ }) {
521
+ const meta = readSchemaMeta(schemaPath);
522
+ const targetAppType = resolveTargetAppType(config, meta);
523
+ const staleOpenXiangdaBinding = isStaleOpenXiangdaBinding(
524
+ config,
525
+ meta,
526
+ targetAppType,
527
+ );
528
+
529
+ if (meta.formUuid && !staleOpenXiangdaBinding) {
530
+ let created = false;
531
+ if (!dryRun) {
532
+ const token = accessToken || (await getOpenApiAccessToken(config));
533
+ const existingForm = await getOpenApiForm(config, token, {
534
+ appType: targetAppType,
535
+ formUuid: meta.formUuid,
536
+ });
537
+
538
+ if (!existingForm) {
539
+ await createOpenApiForm(config, token, {
540
+ name: meta.title || formName,
541
+ appType: targetAppType,
542
+ formType: meta.formType || "receipt",
543
+ relateUuid: meta.relateUuid,
544
+ formUuid: meta.formUuid,
545
+ builderVersion: config.defaults?.formBuilderVersion || "2.0",
546
+ });
547
+ created = true;
548
+ console.log(
549
+ ` ✓ ${formName}: 已按 schema formUuid 创建表单 ${meta.formUuid}`,
550
+ );
551
+ }
552
+
553
+ if ((created || ensureMenu) && !isOpenXiangdaMode(config)) {
554
+ const menuResult = await ensureOpenApiMenuForForm(config, token, {
555
+ appType: targetAppType,
556
+ formUuid: meta.formUuid,
557
+ name: meta.title || formName,
558
+ formType: meta.formType || "receipt",
559
+ parentId: config.menu?.parentId,
560
+ icon: config.menu?.icon,
561
+ });
562
+
563
+ if (menuResult.created) {
564
+ console.log(
565
+ ` ✓ ${formName}: 已创建菜单 ${menuResult.menu?.id || ""}`,
566
+ );
567
+ }
568
+ }
569
+ }
570
+
571
+ return {
572
+ ...meta,
573
+ appType: targetAppType,
574
+ created,
575
+ dryRunCreated: false,
576
+ };
577
+ }
578
+
579
+ if (!meta.hasFormUuidProp) {
580
+ throw new Error(
581
+ `${formName}: schema.ts 中未找到 formUuid 字段,无法自动创建并回写`,
582
+ );
583
+ }
584
+
585
+ const createOptions = {
586
+ name: meta.title || formName,
587
+ appType: targetAppType,
588
+ formType: meta.formType || "receipt",
589
+ relateUuid: meta.relateUuid,
590
+ formUuid: staleOpenXiangdaBinding ? undefined : meta.formUuid || undefined,
591
+ builderVersion: config.defaults?.formBuilderVersion || "2.0",
592
+ };
593
+
594
+ if (dryRun) {
595
+ console.log(
596
+ ` [dry-run] ${formName}: ${
597
+ staleOpenXiangdaBinding ? "检测到旧 appType 绑定" : "formUuid 为空"
598
+ },将创建表单和菜单 "${createOptions.name}"`,
599
+ );
600
+ return {
601
+ ...meta,
602
+ ...createOptions,
603
+ formUuid: "<auto-created-formUuid>",
604
+ created: false,
605
+ dryRunCreated: true,
606
+ };
607
+ }
608
+
609
+ const token = accessToken || (await getOpenApiAccessToken(config));
610
+ const createdForm = await createOpenApiForm(config, token, createOptions);
611
+ syncCreatedFormMeta(schemaPath, meta, targetAppType, createdForm.formUuid);
612
+ console.log(` ✓ ${formName}: 已创建表单 ${createdForm.formUuid}`);
613
+
614
+ let menuResult = { created: false, menu: null };
615
+ if (!isOpenXiangdaMode(config)) {
616
+ menuResult = await ensureOpenApiMenuForForm(config, token, {
617
+ appType: createOptions.appType,
618
+ formUuid: createdForm.formUuid,
619
+ name: createOptions.name,
620
+ formType: createOptions.formType,
621
+ parentId: config.menu?.parentId,
622
+ icon: config.menu?.icon,
623
+ });
624
+ if (menuResult.created) {
625
+ console.log(` ✓ ${formName}: 已创建菜单 ${menuResult.menu?.id || ""}`);
626
+ } else {
627
+ console.log(` ✓ ${formName}: 菜单已存在 ${menuResult.menu?.id || ""}`);
628
+ }
629
+ }
630
+
631
+ return {
632
+ ...meta,
633
+ ...createOptions,
634
+ formUuid: createdForm.formUuid,
635
+ created: true,
636
+ menuCreated: menuResult.created,
637
+ dryRunCreated: false,
638
+ };
639
+ }