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 +6 -2
- package/bin/yida.js +2 -2
- package/lib/copy.js +112 -29
- package/lib/create-form.js +240 -23
- package/lib/env.js +2 -0
- package/lib/locales/en.js +6 -0
- package/lib/locales/ja.js +6 -0
- package/lib/locales/zh.js +6 -0
- package/lib/publish.js +1 -1
- package/lib/utils.js +2 -1
- package/package.json +1 -1
- package/yida-skills/SKILL.md +1 -1
- package/yida-skills/skills/yida-create-form-page/SKILL.md +17 -4
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="
|
|
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="
|
|
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
|
-
*
|
|
103
|
+
* 删除已有的 yida-skills 软链接或目录(悟空环境专用)。
|
|
104
|
+
* 悟空通过手动上传技能,不需要软链,执行 -skills 时只做清理。
|
|
105
|
+
* 使用 lstatSync 而非 existsSync,可以检测到悬空软链(目标不存在但链接本身存在)。
|
|
106
|
+
* @returns {boolean} 是否执行了删除操作
|
|
99
107
|
*/
|
|
100
|
-
function
|
|
101
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
*
|
|
184
|
+
* 根据已检测的环境信息返回目标根目录,避免重复调用 detectEnvironment()。
|
|
185
|
+
* @param {string|null} activeToolName
|
|
186
|
+
* @param {string|null} activeProjectRoot
|
|
187
|
+
* @param {Array} envResults
|
|
125
188
|
* @returns {string} 目标根目录路径
|
|
126
189
|
*/
|
|
127
|
-
function
|
|
128
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
}
|
package/lib/create-form.js
CHANGED
|
@@ -95,7 +95,35 @@ function buildApiPath(appType, apiName, options = {}) {
|
|
|
95
95
|
// ── 参数解析 ─────────────────────────────────────────
|
|
96
96
|
|
|
97
97
|
function parseArgs() {
|
|
98
|
-
const
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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,
|
|
1013
|
-
|
|
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:
|
|
1075
|
-
labelWidthPc:
|
|
1076
|
-
labelWeightPc:
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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,
|
|
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
|
-
|
|
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
package/yida-skills/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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 "
|
|
60
|
+
openyida create-form create "APP_CQ2P5NRFI5L1D6PB8Q7J" "员工信息登记" fields.json --layout double
|
|
56
61
|
```
|
|
57
|
-
|
|
62
|
+
|
|
63
|
+
#### 示例 3:创建卡片式分组表单
|
|
64
|
+
|
|
58
65
|
```bash
|
|
59
|
-
|
|
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
|