openyida 1.0.0-beta.3 → 1.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -156,9 +156,13 @@ npx clawhub@latest install nicky1108/yida-app
156
156
  感谢所有为 OpenYida 做出贡献的开发者!欢迎阅读 [贡献指南](./CONTRIBUTING.md) 参与共建。
157
157
 
158
158
  <p align="left">
159
- <a href="https://github.com/yize"><img src="https://avatars.githubusercontent.com/u/1578814?v=4&s=48" width="48" height="48" alt="yize" title="yize"/></a>
160
- <a href="https://github.com/alex-mm"><img src="https://avatars.githubusercontent.com/u/3302053?v=4&s=48" width="48" height="48" alt="alex-mm" title="alex-mm"/></a>
159
+ <a href="https://github.com/yize"><img src="https://avatars.githubusercontent.com/u/1578814?v=4&s=48" width="48" height="48" alt="九神" title="九神"/></a>
160
+ <a href="https://github.com/alex-mm"><img src="https://avatars.githubusercontent.com/u/3302053?v=4&s=48" width="48" height="48" alt="天晟" title="天晟"/></a>
161
161
  <a href="https://github.com/nicky1108"><img src="https://avatars.githubusercontent.com/u/4279283?v=4&s=48" width="48" height="48" alt="nicky1108" title="nicky1108"/></a>
162
+ <a href="https://github.com/angelinheys"><img src="https://avatars.githubusercontent.com/u/49426983?v=4&s=48" width="48" height="48" alt="angelinheys" title="angelinheys"/></a>
163
+ <a href="https://github.com/yipengmu"><img src="https://avatars.githubusercontent.com/u/3232735?v=4&s=48" width="48" height="48" alt="yipengmu" title="yipengmu"/></a>
164
+ <a href="https://github.com/Waawww"><img src="https://avatars.githubusercontent.com/u/31886449?v=4&s=48" width="48" height="48" alt="Waawww" title="Waawww"/></a>
165
+ <a href="https://github.com/kangjiano"><img src="https://avatars.githubusercontent.com/u/54129385?v=4&s=48" width="48" height="48" alt="kangjiano" title="kangjiano"/></a>
162
166
  </p>
163
167
 
164
168
  ---
package/bin/yida.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * openyida org switch --corp-id <corpId> 切换组织(无需重新登录)
19
19
  * openyida create-app "<名称>" [desc] [icon] [color] 创建应用
20
20
  * openyida create-page <appType> "<页面名>" 创建自定义页面
21
- * openyida create-form create <appType> "<表单名>" <字段JSON> 创建表单页面
21
+ * openyida create-form create <appType> "<表单名>" <字段JSON> [--layout <布局>] [--theme <主题>] [--label-align <对齐>] 创建表单页面
22
22
  * openyida create-form update <appType> <formUuid> <修改JSON> 更新表单页面
23
23
  * openyida get-schema <appType> <formUuid> 获取表单 Schema
24
24
  * openyida publish <源文件路径> <appType> <formUuid> 编译并发布自定义页面
@@ -57,7 +57,7 @@ openyida - 宜搭命令行工具
57
57
  logout 退出登录 / 切换账号
58
58
  create-app "<名称>" [描述] [图标] [颜色] 创建应用,输出 appType
59
59
  create-page <appType> "<页面名>" 创建自定义页面,输出 pageId
60
- create-form create <appType> "<表单名>" <字段JSON> 创建表单页面
60
+ create-form create <appType> "<表单名>" <字段JSON> [--layout <布局>] [--theme <主题>] [--label-align <对齐>] 创建表单页面
61
61
  create-form update <appType> <formUuid> <修改JSON> 更新表单页面
62
62
  get-schema <appType> <formUuid> 获取表单 Schema
63
63
  publish <源文件路径> <appType> <formUuid> 编译并发布自定义页面
package/lib/copy.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * openyida copy → 复制 project/ 目录模板(默认,合并模式)
6
6
  * openyida copy --force → 复制 project/ 目录模板(强制覆盖,先清空目标目录)
7
7
  * openyida copy -skills → 创建 yida-skills/ 软链接(如果存在实际目录则先删除)
8
+ * 悟空环境下:删除已有软链(悟空通过手动上传技能,不需要软链)
8
9
  * openyida copy -project → 复制 project/ 目录模板(与默认行为相同,显式指定)
9
10
  * openyida copy -project --force → 复制 project/ 目录模板(强制覆盖)
10
11
  *
@@ -16,7 +17,12 @@
16
17
  *
17
18
  * project/ 合并模式(默认):已存在的文件强制覆盖,目标目录中多余的文件保留不动
18
19
  * project/ 强制模式(--force):先清空目标目录,再完整复制
19
- * yida-skills/:始终创建软链接,如目标存在实际目录则先删除
20
+ * yida-skills/(非悟空):始终创建软链接,如目标存在实际目录则先删除
21
+ * yida-skills/(悟空):删除已有软链或目录(悟空通过手动上传技能,不需要软链)
22
+ *
23
+ * Windows 兼容说明:
24
+ * - 软链接在 Windows 上需要管理员权限或开发者模式,失败时自动降级为目录复制
25
+ * - 路径分隔符统一使用 path.join 处理
20
26
  */
21
27
 
22
28
  "use strict";
@@ -94,15 +100,22 @@ function forceCopyDir(sourceDir, destDir) {
94
100
  }
95
101
 
96
102
  /**
97
- * 创建软链接:如果目标存在实际目录则先删除,再创建软链接。
98
- * @returns {boolean} 是否成功创建
103
+ * 删除已有的 yida-skills 软链接或目录(悟空环境专用)。
104
+ * 悟空通过手动上传技能,不需要软链,执行 -skills 时只做清理。
105
+ * 使用 lstatSync 而非 existsSync,可以检测到悬空软链(目标不存在但链接本身存在)。
106
+ * @returns {boolean} 是否执行了删除操作
99
107
  */
100
- function createSymlink(sourceDir, destLink) {
101
- if (!fs.existsSync(sourceDir)) return false;
108
+ function removeSkillsLink(destLink) {
109
+ let stats;
110
+ try {
111
+ stats = fs.lstatSync(destLink);
112
+ } catch {
113
+ // 路径不存在(包括悬空软链也不存在的情况)
114
+ console.log(t("copy.wukong_skills_not_found", destLink));
115
+ return false;
116
+ }
102
117
 
103
- // 如果目标已存在,判断是目录还是软链接
104
- if (fs.existsSync(destLink)) {
105
- const stats = fs.lstatSync(destLink);
118
+ try {
106
119
  if (stats.isSymbolicLink()) {
107
120
  fs.unlinkSync(destLink);
108
121
  console.log(t("copy.symlink_removed", destLink));
@@ -113,20 +126,69 @@ function createSymlink(sourceDir, destLink) {
113
126
  fs.unlinkSync(destLink);
114
127
  console.log(t("copy.removed", destLink));
115
128
  }
129
+ return true;
130
+ } catch (error) {
131
+ console.error(t("copy.remove_failed", destLink, error.message));
132
+ return false;
116
133
  }
134
+ }
117
135
 
118
- fs.symlinkSync(sourceDir, destLink, "junction");
119
- console.log(t("copy.symlink_created", destLink, sourceDir));
120
- return true;
136
+ /**
137
+ * 创建软链接:如果目标存在实际目录则先删除,再创建软链接。
138
+ * Windows 上软链需要管理员权限或开发者模式,失败时自动降级为目录复制。
139
+ * @returns {boolean} 是否成功创建
140
+ */
141
+ function createSymlink(sourceDir, destLink) {
142
+ if (!fs.existsSync(sourceDir)) return false;
143
+
144
+ // 如果目标已存在,判断是目录还是软链接
145
+ if (fs.existsSync(destLink)) {
146
+ try {
147
+ const stats = fs.lstatSync(destLink);
148
+ if (stats.isSymbolicLink()) {
149
+ fs.unlinkSync(destLink);
150
+ console.log(t("copy.symlink_removed", destLink));
151
+ } else if (stats.isDirectory()) {
152
+ fs.rmSync(destLink, { recursive: true, force: true });
153
+ console.log(t("copy.dir_deleted", destLink));
154
+ } else {
155
+ fs.unlinkSync(destLink);
156
+ console.log(t("copy.removed", destLink));
157
+ }
158
+ } catch (error) {
159
+ console.error(t("copy.remove_failed", destLink, error.message));
160
+ return false;
161
+ }
162
+ }
163
+
164
+ // Windows 上 junction 只支持目录,且需要管理员权限或开发者模式
165
+ // 失败时降级为目录复制
166
+ const symlinkType = process.platform === "win32" ? "junction" : "dir";
167
+ try {
168
+ fs.symlinkSync(sourceDir, destLink, symlinkType);
169
+ console.log(t("copy.symlink_created", destLink, sourceDir));
170
+ return true;
171
+ } catch (error) {
172
+ if (process.platform === "win32" && error.code === "EPERM") {
173
+ console.log(t("copy.symlink_fallback_copy", destLink));
174
+ const count = mergeCopyDir(sourceDir, destLink);
175
+ console.log(t("copy.files_copied", count));
176
+ return true;
177
+ }
178
+ console.error(t("copy.symlink_failed", destLink, error.message));
179
+ return false;
180
+ }
121
181
  }
122
182
 
123
183
  /**
124
- * 检测 AI 工具环境,返回目标根目录。
184
+ * 根据已检测的环境信息返回目标根目录,避免重复调用 detectEnvironment()。
185
+ * @param {string|null} activeToolName
186
+ * @param {string|null} activeProjectRoot
187
+ * @param {Array} envResults
125
188
  * @returns {string} 目标根目录路径
126
189
  */
127
- function resolveDestBase() {
128
- const { activeToolName, activeProjectRoot, results } = detectEnvironment();
129
- const activeResult = results.find((r) => r.displayName === activeToolName);
190
+ function resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults) {
191
+ const activeResult = envResults.find((r) => r.displayName === activeToolName);
130
192
  const isWukong = activeResult && activeResult.dirName === ".real";
131
193
 
132
194
  if (isWukong) {
@@ -141,7 +203,7 @@ function resolveDestBase() {
141
203
 
142
204
  // 未检测到活跃工具
143
205
  console.error(t("copy.no_ai_tool"));
144
- results.forEach((r) => {
206
+ envResults.forEach((r) => {
145
207
  console.error(` ${r.isActive ? "✅" : "⬜"} ${r.displayName}`);
146
208
  });
147
209
  console.error(t("copy.force_hint"));
@@ -188,17 +250,24 @@ function run() {
188
250
  console.log(t("copy.package_root", packageRoot));
189
251
 
190
252
  // 2. 确定目标根目录(检测 AI 工具环境)
191
- const destBase = resolveDestBase();
253
+ // 同时获取 isWukong 标志,避免后续重复调用 detectEnvironment()
254
+ const { activeToolName, activeProjectRoot, results: envResults } = detectEnvironment();
255
+ const activeEnvResult = envResults.find((r) => r.isActive);
256
+ const isWukong = activeEnvResult && activeEnvResult.dirName === ".real";
257
+ const destBase = resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults);
192
258
  console.log(t("copy.dest_base", destBase));
193
259
  if (isForce) {
194
260
  console.log(t("copy.force_mode"));
195
261
  }
196
262
 
197
263
  // 3. 确定要复制/链接的内容
198
- // - 指定了 -skills:只创建 yida-skills/ 软链接
264
+ // - 指定了 -skills
265
+ // 悟空环境:删除已有的 yida-skills/ 软链(悟空手动上传技能,不需要软链)
266
+ // 其他环境:创建 yida-skills/ 软链接(如果存在实际目录则先删除)
199
267
  // - 指定了 -project:只复制 project/
200
268
  // - 两者都没指定(默认):只复制 project/
201
269
  // - 两者都指定:同时处理两项
270
+
202
271
  const shouldCopyProject = wantsProject || (!wantsSkills);
203
272
  const shouldLinkSkills = wantsSkills;
204
273
 
@@ -215,17 +284,28 @@ function run() {
215
284
  }
216
285
 
217
286
  if (shouldLinkSkills) {
218
- console.log(t("copy.creating_symlink"));
219
- const success = createSymlink(
220
- packageYidaSkillsDir,
221
- path.join(destBase, "yida-skills")
222
- );
223
- results.push({
224
- label: "yida-skills/",
225
- dest: path.join(destBase, "yida-skills"),
226
- count: success ? 1 : 0,
227
- type: "symlink"
228
- });
287
+ const destSkillsLink = path.join(destBase, "yida-skills");
288
+ if (isWukong) {
289
+ // 悟空环境:删除已有软链,不创建新软链
290
+ console.log(t("copy.wukong_skills_cleanup"));
291
+ const removed = removeSkillsLink(destSkillsLink);
292
+ results.push({
293
+ label: "yida-skills/",
294
+ dest: destSkillsLink,
295
+ count: removed ? 1 : 0,
296
+ type: "wukong-cleanup"
297
+ });
298
+ } else {
299
+ // 其他环境:创建软链接
300
+ console.log(t("copy.creating_symlink"));
301
+ const success = createSymlink(packageYidaSkillsDir, destSkillsLink);
302
+ results.push({
303
+ label: "yida-skills/",
304
+ dest: destSkillsLink,
305
+ count: success ? 1 : 0,
306
+ type: "symlink"
307
+ });
308
+ }
229
309
  }
230
310
 
231
311
  // 4. 打印汇总
@@ -242,6 +322,9 @@ function run() {
242
322
  results.forEach((r) => {
243
323
  if (r.type === "symlink") {
244
324
  console.log(` ${r.label.padEnd(14)} → ${r.dest} (${t("copy.symlink_label")})`);
325
+ } else if (r.type === "wukong-cleanup") {
326
+ const statusText = r.count > 0 ? t("copy.wukong_skills_cleaned") : t("copy.wukong_skills_not_found", r.dest);
327
+ console.log(` ${r.label.padEnd(14)} → ${r.dest} (${statusText})`);
245
328
  } else {
246
329
  console.log(` ${r.label.padEnd(14)} → ${r.dest} (${t("copy.files_count", r.count)})`);
247
330
  }
@@ -95,7 +95,35 @@ function buildApiPath(appType, apiName, options = {}) {
95
95
  // ── 参数解析 ─────────────────────────────────────────
96
96
 
97
97
  function parseArgs() {
98
- const args = process.argv.slice(2);
98
+ const rawArgs = process.argv.slice(2);
99
+
100
+ // 解析可选参数
101
+ const options = {
102
+ layout: "single", // 布局:single/double/card/section
103
+ theme: "default", // 主题:default/compact/comfortable
104
+ labelAlign: "top", // 标签对齐:top/left/right
105
+ };
106
+
107
+ // 复制一份 args 用于解析(避免修改原始数组影响后续处理)
108
+ const args = [...rawArgs];
109
+
110
+ // 解析 --layout, --theme, --label-align 参数
111
+ for (let i = 0; i < args.length; i++) {
112
+ if (args[i] === "--layout" && i + 1 < args.length) {
113
+ options.layout = args[i + 1];
114
+ args.splice(i, 2);
115
+ i--;
116
+ } else if (args[i] === "--theme" && i + 1 < args.length) {
117
+ options.theme = args[i + 1];
118
+ args.splice(i, 2);
119
+ i--;
120
+ } else if (args[i] === "--label-align" && i + 1 < args.length) {
121
+ options.labelAlign = args[i + 1];
122
+ args.splice(i, 2);
123
+ i--;
124
+ }
125
+ }
126
+
99
127
  const mode = args[0];
100
128
 
101
129
  if (mode === "create") {
@@ -104,7 +132,13 @@ function parseArgs() {
104
132
  console.error(t("create_form.example_create"));
105
133
  process.exit(1);
106
134
  }
107
- return { mode: "create", appType: args[1], formTitle: args[2], fieldsJsonOrFile: args[3] };
135
+ return {
136
+ mode: "create",
137
+ appType: args[1],
138
+ formTitle: args[2],
139
+ fieldsJsonOrFile: args[3],
140
+ ...options
141
+ };
108
142
  }
109
143
 
110
144
  if (mode === "update") {
@@ -113,12 +147,24 @@ function parseArgs() {
113
147
  console.error(t("create_form.example_update"));
114
148
  process.exit(1);
115
149
  }
116
- return { mode: "update", appType: args[1], formUuid: args[2], changesJsonOrFile: args[3] };
150
+ return {
151
+ mode: "update",
152
+ appType: args[1],
153
+ formUuid: args[2],
154
+ changesJsonOrFile: args[3],
155
+ ...options
156
+ };
117
157
  }
118
158
 
119
159
  // 兼容旧用法(无 mode 参数,默认 create 模式)
120
160
  if (args.length >= 3 && mode !== "create" && mode !== "update") {
121
- return { mode: "create", appType: args[0], formTitle: args[1], fieldsJsonOrFile: args[2] };
161
+ return {
162
+ mode: "create",
163
+ appType: args[0],
164
+ formTitle: args[1],
165
+ fieldsJsonOrFile: args[2],
166
+ ...options
167
+ };
122
168
  }
123
169
 
124
170
  console.error(t("create_form.usage_label"));
@@ -1007,10 +1053,160 @@ function resolveFieldIdReferences(fieldComponents) {
1007
1053
  });
1008
1054
  }
1009
1055
 
1056
+ // ── 布局配置映射 ─────────────────────────────────────
1057
+
1058
+ /**
1059
+ * 获取布局配置
1060
+ * @param {string} layout - 布局类型:single/double/card/section
1061
+ * @returns {object} 布局配置对象 { columns, formLayout, groupFields }
1062
+ */
1063
+ function getLayoutConfig(layout) {
1064
+ const layoutMap = {
1065
+ single: { columns: 1, formLayout: "default", groupFields: false },
1066
+ "1": { columns: 1, formLayout: "default", groupFields: false },
1067
+ double: { columns: 2, formLayout: "default", groupFields: false },
1068
+ "2": { columns: 2, formLayout: "default", groupFields: false },
1069
+ card: { columns: 1, formLayout: "card", groupFields: true },
1070
+ section: { columns: 1, formLayout: "section", groupFields: true },
1071
+ };
1072
+ return layoutMap[layout] || layoutMap.single;
1073
+ }
1074
+
1075
+ /**
1076
+ * 获取主题样式配置
1077
+ * @param {string} theme - 主题类型:default/compact/comfortable
1078
+ * @param {string} labelAlign - 标签对齐:top/left/right
1079
+ * @returns {object} 样式配置对象
1080
+ */
1081
+ function getThemeConfig(theme, labelAlign) {
1082
+ const baseConfig = {
1083
+ labelAlignPc: labelAlign || "top",
1084
+ labelWidthPc: labelAlign === "left" || labelAlign === "right" ? "130px" : "auto",
1085
+ labelWeightPc: "normal",
1086
+ contentMargin: "20",
1087
+ contentPadding: "20",
1088
+ fieldSpacing: "medium",
1089
+ };
1090
+
1091
+ const themeMap = {
1092
+ default: {
1093
+ ...baseConfig,
1094
+ contentMargin: "20",
1095
+ contentPadding: "20",
1096
+ },
1097
+ compact: {
1098
+ ...baseConfig,
1099
+ contentMargin: "12",
1100
+ contentPadding: "12",
1101
+ fieldSpacing: "small",
1102
+ },
1103
+ comfortable: {
1104
+ ...baseConfig,
1105
+ contentMargin: "32",
1106
+ contentPadding: "32",
1107
+ fieldSpacing: "large",
1108
+ },
1109
+ };
1110
+
1111
+ return themeMap[theme] || themeMap.default;
1112
+ }
1113
+
1114
+ // ── 按 group 分组字段 ─────────────────────────────────
1115
+
1116
+ /**
1117
+ * 将字段按 group 属性分组
1118
+ * @param {Array} fields - 字段定义数组
1119
+ * @returns {Array} 分组后的字段数组,每个元素是 { groupName, fields }
1120
+ */
1121
+ function groupFieldsByGroup(fields) {
1122
+ const groups = [];
1123
+ const groupMap = new Map();
1124
+
1125
+ fields.forEach((field) => {
1126
+ const groupName = field.group || "基本信息";
1127
+ if (!groupMap.has(groupName)) {
1128
+ groupMap.set(groupName, []);
1129
+ }
1130
+ groupMap.get(groupName).push(field);
1131
+ });
1132
+
1133
+ groupMap.forEach((groupFields, groupName) => {
1134
+ groups.push({ groupName, fields: groupFields });
1135
+ });
1136
+
1137
+ return groups;
1138
+ }
1139
+
1140
+ // ── 构建分组字段组件 ─────────────────────────────────
1141
+
1142
+ /**
1143
+ * 构建分组/卡片布局的字段组件
1144
+ * @param {Array} fields - 字段定义数组
1145
+ * @param {string} formLayout - 布局类型:card/section
1146
+ * @returns {Array} 分组后的组件数组
1147
+ */
1148
+ function buildGroupedFieldComponents(fields, formLayout) {
1149
+ const groups = groupFieldsByGroup(fields);
1150
+
1151
+ return groups.map((group, groupIndex) => {
1152
+ // 构建分组内的字段组件
1153
+ const groupFieldComponents = group.fields.map((field) => buildFieldComponent(field));
1154
+
1155
+ if (formLayout === "card") {
1156
+ // 卡片式布局:每个分组是一个卡片容器
1157
+ return {
1158
+ componentName: "CardContainer",
1159
+ id: nextNodeId(),
1160
+ props: {
1161
+ title: i18n(group.groupName, group.groupName),
1162
+ collapsible: true,
1163
+ defaultCollapsed: false,
1164
+ showTitle: true,
1165
+ cardStyle: "default",
1166
+ headerStyle: "default",
1167
+ __gridSpan: 1,
1168
+ },
1169
+ condition: true,
1170
+ hidden: false,
1171
+ title: "",
1172
+ isLocked: false,
1173
+ conditionGroup: "",
1174
+ children: groupFieldComponents,
1175
+ };
1176
+ } else {
1177
+ // 分组式布局:每个分组是一个区块
1178
+ return {
1179
+ componentName: "SectionContainer",
1180
+ id: nextNodeId(),
1181
+ props: {
1182
+ title: i18n(group.groupName, group.groupName),
1183
+ collapsible: true,
1184
+ defaultCollapsed: false,
1185
+ showTitle: true,
1186
+ sectionStyle: "default",
1187
+ divider: groupIndex > 0, // 第一个分组不显示分隔线
1188
+ __gridSpan: 1,
1189
+ },
1190
+ condition: true,
1191
+ hidden: false,
1192
+ title: "",
1193
+ isLocked: false,
1194
+ conditionGroup: "",
1195
+ children: groupFieldComponents,
1196
+ };
1197
+ }
1198
+ });
1199
+ }
1200
+
1010
1201
  // ── 生成表单 Schema ──────────────────────────────────
1011
1202
 
1012
- function buildFormSchema(formTitle, fields, formUuid, corpId, appType, columns) {
1013
- columns = columns || 1;
1203
+ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, theme, labelAlign) {
1204
+ // 解析布局配置
1205
+ const layoutConfig = getLayoutConfig(layout || "single");
1206
+ const columns = layoutConfig.columns;
1207
+
1208
+ // 解析主题配置
1209
+ const themeConfig = getThemeConfig(theme || "default", labelAlign || "top");
1014
1210
  const fieldComponents = fields.map(function (field) {
1015
1211
  return buildFieldComponent(field);
1016
1212
  });
@@ -1070,12 +1266,12 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, columns)
1070
1266
  titleColor: "light",
1071
1267
  titleBg: "https://img.alicdn.com/imgextra/i2/O1CN0143ATPP1wIa9TrVvzN_!!6000000006285-2-tps-3360-400.png_.webp",
1072
1268
  backgroundColorCustom: "#f1f2f3",
1073
- sizePc: "medium",
1074
- labelAlignPc: "top",
1075
- labelWidthPc: "130px",
1076
- labelWeightPc: "normal",
1077
- labelAlignMobile: "top",
1078
- labelWidthMobile: "80px",
1269
+ sizePc: themeConfig.fieldSpacing === "small" ? "small" : themeConfig.fieldSpacing === "large" ? "large" : "medium",
1270
+ labelAlignPc: themeConfig.labelAlignPc,
1271
+ labelWidthPc: themeConfig.labelWidthPc,
1272
+ labelWeightPc: themeConfig.labelWeightPc,
1273
+ labelAlignMobile: labelAlign || "top",
1274
+ labelWidthMobile: labelAlign === "left" || labelAlign === "right" ? "80px" : "auto",
1079
1275
  labelWeightMobile: "normal",
1080
1276
  },
1081
1277
  condition: true,
@@ -1150,15 +1346,36 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, columns)
1150
1346
  afterSubmit: false,
1151
1347
  onProcessActionValidate: false,
1152
1348
  afterFormDataInit: false,
1153
- },
1154
- condition: true,
1155
- hidden: false,
1156
- title: "",
1157
- isLocked: false,
1158
- conditionGroup: "",
1159
- // ★ 核心:FormContainer 内层的字段组件
1160
- children: fieldComponents,
1161
- },
1349
+ }, children: [
1350
+ {
1351
+ componentName: "FormContainer",
1352
+ id: nextNodeId(),
1353
+ props: {
1354
+ formLabel: i18n(formTitle, formTitle),
1355
+ formLabelVisible: true,
1356
+ columns: columns,
1357
+ labelAlign: labelAlign || "top",
1358
+ submitText: i18n("提交", "Submit"),
1359
+ stageText: i18n("暂存", "Stage"),
1360
+ submitAndNewText: i18n("提交并继续", "Submit and New"),
1361
+ fieldId: "formContainer_" + Date.now().toString(36),
1362
+ aiFormConfig: { systemPrompt: "", model: "qwen" },
1363
+ beforeSubmit: false,
1364
+ afterSubmit: false,
1365
+ onProcessActionValidate: false,
1366
+ afterFormDataInit: false,
1367
+ },
1368
+ condition: true,
1369
+ hidden: false,
1370
+ title: "",
1371
+ isLocked: false,
1372
+ conditionGroup: "",
1373
+ // ★ 核心:FormContainer 内层的字段组件(支持分组布局)
1374
+ children: layoutConfig.groupFields
1375
+ ? buildGroupedFieldComponents(fields, layoutConfig.formLayout)
1376
+ : fieldComponents,
1377
+ },
1378
+ ], },
1162
1379
  ],
1163
1380
  },
1164
1381
  {
@@ -1974,7 +2191,7 @@ async function saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, ver
1974
2191
  // ── create 模式主流程 ─────────────────────────────────
1975
2192
 
1976
2193
  async function mainCreate(parsedArgs, csrfToken, cookies, baseUrl, cookieData) {
1977
- const { appType, formTitle, fieldsJsonOrFile } = parsedArgs;
2194
+ const { appType, formTitle, fieldsJsonOrFile, layout, theme, labelAlign } = parsedArgs;
1978
2195
 
1979
2196
  const SEP = "=".repeat(50);
1980
2197
  console.error(SEP);
@@ -2025,7 +2242,7 @@ async function mainCreate(parsedArgs, csrfToken, cookies, baseUrl, cookieData) {
2025
2242
  console.error(t("create_form.corp_id_ok", corpId));
2026
2243
  }
2027
2244
 
2028
- const schema = buildFormSchema(formTitle, fields, formUuid, corpId, appType, columns);
2245
+ const schema = buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, theme, labelAlign);
2029
2246
  var { configResult } = await saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, 1, 4);
2030
2247
 
2031
2248
  // 输出结果
package/lib/env.js CHANGED
@@ -17,6 +17,7 @@ const home = os.homedir();
17
17
  /**
18
18
  * 获取所有已安装的 AI 工具列表(用于展示)。
19
19
  * 不判断当前是否活跃,只判断是否安装过。
20
+ * 使用 path.join 拼接路径,兼容 Windows 和 macOS/Linux。
20
21
  *
21
22
  * @returns {Array} 已安装工具列表
22
23
  */
@@ -49,6 +50,7 @@ function detectEnvironment() {
49
50
  const results = installedTools.map(({ dirName, displayName }) => {
50
51
  const isWukong = dirName === ".real";
51
52
  const isActive = activeTool && activeTool.dirName === dirName;
53
+ // path.join 在 Windows 上自动使用反斜杠,兼容所有平台
52
54
  const workspaceRoot = isWukong
53
55
  ? path.join(home, ".real", "workspace", "project")
54
56
  : cwdProject;
package/lib/locales/en.js CHANGED
@@ -586,6 +586,12 @@ Examples:
586
586
  files_copied: " Files copied: {0}",
587
587
  files_count: "{0} files",
588
588
  symlinks_created: " Symlinks created: {0}",
589
+ wukong_skills_cleanup: "\n🗑️ Wukong env: Cleaning up yida-skills/ symlink (Wukong uses manual skill upload, no symlink needed)...",
590
+ wukong_skills_cleaned: "cleaned up",
591
+ wukong_skills_not_found: " ℹ️ No yida-skills/ symlink or directory found, nothing to clean: {0}",
592
+ remove_failed: " ❌ Remove failed: {0} ({1})",
593
+ symlink_fallback_copy: " ⚠️ Windows symlink creation failed (requires admin privileges), falling back to directory copy: {0}",
594
+ symlink_failed: " ❌ Symlink creation failed: {0} ({1})",
589
595
  result_symlink: " {0} → {1} (symlink)",
590
596
  result_copy: " {0} → {1} ({2} files)",
591
597
  },
package/lib/locales/ja.js CHANGED
@@ -584,6 +584,12 @@ openyida - Yida CLI ツール
584
584
  files_copied: " コピーしたファイル: {0} 個",
585
585
  files_count: "{0} ファイル",
586
586
  symlinks_created: " 作成したシンボリックリンク: {0} 個",
587
+ wukong_skills_cleanup: "\n🗑️ 悟空環境:yida-skills/ シンボリックリンクを削除中(悟空はスキルを手動アップロードするため、シンボリックリンク不要)...",
588
+ wukong_skills_cleaned: "削除済み",
589
+ wukong_skills_not_found: " ℹ️ yida-skills/ シンボリックリンクまたはディレクトリが見つかりません: {0}",
590
+ remove_failed: " ❌ 削除失敗: {0} ({1})",
591
+ symlink_fallback_copy: " ⚠️ Windows でシンボリックリンク作成失敗(管理者権限が必要)、ディレクトリコピーにフォールバック: {0}",
592
+ symlink_failed: " ❌ シンボリックリンク作成失敗: {0} ({1})",
587
593
  result_symlink: " {0} → {1}(シンボリックリンク)",
588
594
  result_copy: " {0} → {1}({2} ファイル)",
589
595
  },
package/lib/locales/zh.js CHANGED
@@ -487,6 +487,12 @@ openyida - 宜搭命令行工具
487
487
  symlinks_created: " 创建软链接: {0} 个",
488
488
  result_symlink: " {0} → {1} (软链接)",
489
489
  result_copy: " {0} → {1} ({2} 个文件)",
490
+ wukong_skills_cleanup: "\n🗑️ 悟空环境:清理 yida-skills/ 软链(悟空通过手动上传技能,不需要软链)...",
491
+ wukong_skills_cleaned: "已清理",
492
+ wukong_skills_not_found: " ℹ️ 未找到 yida-skills/ 软链或目录,无需清理: {0}",
493
+ remove_failed: " ❌ 删除失败: {0} ({1})",
494
+ symlink_fallback_copy: " ⚠️ Windows 软链创建失败(需要管理员权限),降级为目录复制: {0}",
495
+ symlink_failed: " ❌ 软链接创建失败: {0} ({1})",
490
496
  },
491
497
 
492
498
  // ── lib/publish.js ─────────────────────────────────
package/lib/publish.js CHANGED
@@ -23,7 +23,7 @@ const http = require("http");
23
23
  const querystring = require("querystring");
24
24
  const { default: babelTransform } = require("./babel-transform");
25
25
  const UglifyJS = require("uglify-js");
26
- const { isLoginExpired, isCsrfTokenExpired } = require("./utils");
26
+ const { findProjectRoot, isLoginExpired, isCsrfTokenExpired, loadCookieData, triggerLogin, refreshCsrfToken } = require("./utils");
27
27
  const { t } = require("./i18n");
28
28
 
29
29
  // ── 配置读取 ──────────────────────────────────────────
package/lib/utils.js CHANGED
@@ -88,7 +88,8 @@ function detectActiveTool() {
88
88
  }
89
89
 
90
90
  // 悟空(Wukong)
91
- if (env.AGENT_WORK_ROOT && env.AGENT_WORK_ROOT.includes(".real")) {
91
+ // Windows 路径可能使用反斜杠,需同时兼容正斜杠和反斜杠
92
+ if (env.AGENT_WORK_ROOT && (env.AGENT_WORK_ROOT.includes(".real") || env.AGENT_WORK_ROOT.includes(path.join(".real")))) {
92
93
  return {
93
94
  tool: "wukong",
94
95
  displayName: "悟空(Wukong)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "./bin/yida.js",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: Yida
2
+ name: yida
3
3
  description: >
4
4
  宜搭 AI 应用开发总入口技能。通过有 AI Coding 能力的智能体(悟空/Claude/Open Code 等)+ 宜搭低代码平台,实现一句话生成完整应用。
5
5
  包含应用创建、表单设计、自定义页面开发、页面发布、登录态管理等完整开发流程。
@@ -48,17 +48,30 @@ openyida create-form create <appType> <formTitle> <fieldsJsonOrFile>
48
48
  | `appType` | 是 | 应用 ID,如 `APP_XXX` |
49
49
  | `formTitle` | 是 | 表单名称 |
50
50
  | `fieldsJsonOrFile` | 是 | 字段定义,支持两种格式:JSON 字符串(以 `[` 开头)或 JSON 文件路径 |
51
+ #### 示例 1:创建简单表单
51
52
 
52
- **示例(JSON 字符串,推荐)**:
53
+ ```bash
54
+ openyida create-form create "APP_CQ2P5NRFI5L1D6PB8Q7J" "员工信息登记" fields.json
55
+ ```
56
+
57
+ #### 示例 2:创建双列表单
53
58
 
54
59
  ```bash
55
- openyida create-form create "APP_XXX" "用户信息表" '[{"type":"TextField","label":"姓名","required":true},{"type":"SelectField","label":"部门","dataSource":[{"text":{"zh_CN":"技术部","en_US":"技术部","type":"i18n"},"value":"技术部","sid":"serial_xxx","disable":false,"defaultChecked":false},{"text":{"zh_CN":"产品部","en_US":"产品部","type":"i18n"},"value":"产品部","sid":"serial_xxx","disable":false,"defaultChecked":false}]}]'
60
+ openyida create-form create "APP_CQ2P5NRFI5L1D6PB8Q7J" "员工信息登记" fields.json --layout double
56
61
  ```
57
- **示例(JSON 文件)**:
62
+
63
+ #### 示例 3:创建卡片式分组表单
64
+
58
65
  ```bash
59
- openyida create-form create "APP_xxx" "用户信息表" .cache/user-info-fields.json
66
+ # fields.json 中包含 group 字段分组
67
+ openyida create-form create "APP_CQ2P5NRFI5L1D6PB8Q7J" "员工信息登记" fields.json --layout card --theme comfortable
60
68
  ```
61
69
 
70
+ #### 示例 4:创建紧凑主题、左对齐标签的表单
71
+
72
+ ```bash
73
+ openyida create-form create "APP_CQ2P5NRFI5L1D6PB8Q7J" "员工信息登记" fields.json --layout double --theme compact --label-align left
74
+ ```
62
75
  **输出**:日志输出到 stderr,JSON 结果输出到 stdout:
63
76
 
64
77
  ```json