@stalfh233/omc-cli 0.6.6 → 0.6.10
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 +2 -1
- package/dist/approval-flow.js +5 -3
- package/dist/bizservice-config.js +26 -10
- package/dist/call.js +47 -0
- package/dist/capability-overview.js +47 -24
- package/dist/cli-contract.js +90 -17
- package/dist/cli-output.js +41 -2
- package/dist/cli-stdout.js +74 -0
- package/dist/cli.js +162 -54
- package/dist/command-transport.js +8 -1
- package/dist/config-store.js +17 -2
- package/dist/discovery.js +62 -3
- package/dist/field-families.js +35 -6
- package/dist/goal-routes.js +118 -11
- package/dist/human-summary.js +11 -0
- package/dist/lanes/app-contract.js +12 -5
- package/dist/lanes/app.js +221 -134
- package/dist/lanes/apply-changes.js +2 -1
- package/dist/lanes/b2-registration-contract.js +3 -3
- package/dist/lanes/b2-registration.js +890 -693
- package/dist/lanes/businessrule.js +75 -51
- package/dist/lanes/custom-page.js +166 -115
- package/dist/lanes/datarule-contract.js +2 -2
- package/dist/lanes/datarule.js +238 -154
- package/dist/lanes/dictionary.js +253 -110
- package/dist/lanes/environment.js +57 -9
- package/dist/lanes/field.js +160 -107
- package/dist/lanes/folder-contract.js +7 -3
- package/dist/lanes/folder.js +249 -118
- package/dist/lanes/form-contract.js +1 -1
- package/dist/lanes/form.js +188 -64
- package/dist/lanes/list-view.js +405 -369
- package/dist/lanes/meta.js +31 -6
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +610 -512
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +160 -146
- package/dist/lanes/organization.js +905 -640
- package/dist/lanes/programming.js +274 -136
- package/dist/lanes/report-contract.js +2 -2
- package/dist/lanes/report.js +354 -198
- package/dist/lanes/rule-lifecycle.js +294 -140
- package/dist/lanes/runtime-data-contract.js +4 -4
- package/dist/lanes/runtime-data.js +869 -745
- package/dist/lanes/workflow-contract.js +3 -3
- package/dist/lanes/workflow.js +732 -605
- package/dist/model-scaffold.js +26 -5
- package/dist/path-projection.js +196 -14
- package/dist/plan.js +25 -0
- package/dist/programming-workflow.js +55 -3
- package/dist/read-shapes.js +146 -0
- package/dist/read.js +40 -9
- package/dist/recipes/businessrule.js +74 -24
- package/dist/recipes/dictionary.js +7 -4
- package/dist/recipes/form.js +46 -0
- package/dist/recipes/runtime-data.js +3 -2
- package/dist/refusal-guidance.js +29 -5
- package/dist/rule-support.js +14 -1
- package/dist/rule-when.js +2 -2
- package/dist/tool-types.js +9 -3
- package/dist/version-family-gaps.js +101 -0
- package/dist/write.js +2 -1
- package/package.json +6 -4
- package/skills/omc-glossary.md +13 -0
- package/skills/omc-refusals.md +13 -5
- package/skills/omc-start-here.md +2 -2
- package/skills/omc-tool-selection.md +2 -1
- package/skills/omc-write-hazards.md +5 -2
package/dist/lanes/folder.js
CHANGED
|
@@ -38,6 +38,23 @@ import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
|
|
|
38
38
|
*/
|
|
39
39
|
const LANE = LANES.folder;
|
|
40
40
|
const T = FOLDER_LANE_CONTRACT.tools;
|
|
41
|
+
/**
|
|
42
|
+
* Live 1.1.25 (r28d F2): platform `apppackage/create` seeds one top-level Folder
|
|
43
|
+
* whose `name_i18n.en` is the stable token `defaultGroup` (zh display is「默认分组」).
|
|
44
|
+
* Name alone is brittle — agents rename / locales differ — so mark by that en key.
|
|
45
|
+
*/
|
|
46
|
+
export function isDefaultFolderNode(node) {
|
|
47
|
+
const raw = node.name_i18n;
|
|
48
|
+
if (typeof raw !== "string" || raw.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return String(parsed.en ?? "") === "defaultGroup";
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
41
58
|
/** Flatten the nested app tree, keeping the path so a caller can tell WHICH "合同" it means. */
|
|
42
59
|
/**
|
|
43
60
|
* Flatten the nested app tree. Live 1.1.25 readback: `apppackage/trees?appCode=`
|
|
@@ -45,35 +62,76 @@ const T = FOLDER_LANE_CONTRACT.tools;
|
|
|
45
62
|
* carrying its children — so a top-level folder is depth 0 and a child is
|
|
46
63
|
* "父 / 子" at depth 1. Reading only the top array makes a freshly created child
|
|
47
64
|
* folder look absent (the customer's first failure).
|
|
65
|
+
*
|
|
66
|
+
* Sibling order: Folder before other types, then by name (stable for tree
|
|
67
|
+
* presentation). Preorder walk is preserved across depths. BizModel nodes also
|
|
68
|
+
* get `displayPath` (`path + " [model]"`) so a same-named folder/model pair is
|
|
69
|
+
* distinguishable without changing the existing `path` contract (r28d F5).
|
|
48
70
|
*/
|
|
49
|
-
function flattenFolders(rows, parentPath = []) {
|
|
71
|
+
export function flattenFolders(rows, parentPath = []) {
|
|
50
72
|
const out = [];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
const siblings = rows
|
|
74
|
+
.filter((raw) => raw !== null && typeof raw === "object")
|
|
75
|
+
.slice()
|
|
76
|
+
.sort((a, b) => {
|
|
77
|
+
const aFolder = String(a.type ?? "") === "Folder" ? 0 : 1;
|
|
78
|
+
const bFolder = String(b.type ?? "") === "Folder" ? 0 : 1;
|
|
79
|
+
if (aFolder !== bFolder)
|
|
80
|
+
return aFolder - bFolder;
|
|
81
|
+
return String(a.name ?? a.code ?? "").localeCompare(String(b.name ?? b.code ?? ""), "zh");
|
|
82
|
+
});
|
|
83
|
+
for (const node of siblings) {
|
|
55
84
|
const name = String(node.name ?? node.code ?? "");
|
|
56
|
-
const
|
|
85
|
+
const pathParts = [...parentPath, name];
|
|
86
|
+
const path = pathParts.join(" / ");
|
|
87
|
+
const type = node.type ?? null;
|
|
88
|
+
const isDefault = type === "Folder" && isDefaultFolderNode(node);
|
|
57
89
|
out.push({
|
|
58
90
|
id: node.id ?? null,
|
|
59
|
-
type
|
|
91
|
+
type,
|
|
60
92
|
code: node.code ?? null,
|
|
61
93
|
name: node.name ?? null,
|
|
62
94
|
parentId: node.parentId ?? null,
|
|
63
|
-
depth:
|
|
64
|
-
path
|
|
95
|
+
depth: pathParts.length - 1,
|
|
96
|
+
path,
|
|
97
|
+
...(type === "BizModel" ? { displayPath: `${path} [model]` } : {}),
|
|
98
|
+
...(isDefault ? { isDefault: true } : {}),
|
|
65
99
|
});
|
|
66
100
|
if (Array.isArray(node.children) && node.children.length > 0)
|
|
67
|
-
out.push(...flattenFolders(node.children,
|
|
101
|
+
out.push(...flattenFolders(node.children, pathParts));
|
|
68
102
|
}
|
|
69
103
|
return out;
|
|
70
104
|
}
|
|
71
105
|
async function readTree(session, appCode) {
|
|
72
106
|
return flattenFolders(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode } })).data));
|
|
73
107
|
}
|
|
108
|
+
/** Shared by app.delete (r28d F8) so the non-empty refusal can list children with path. */
|
|
109
|
+
export async function readAppTree(session, appCode) {
|
|
110
|
+
return readTree(session, appCode);
|
|
111
|
+
}
|
|
74
112
|
function folderRefusal(schemaCode, message, extra = {}) {
|
|
75
113
|
throw new ToolRefusalError({ status: "refused", blocker: "folder-not-found", ...(schemaCode ? { applicationCode: schemaCode } : {}), ...extra, message });
|
|
76
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* F6(r27):folder.* 与 report.* 的应用范围参数名历史上不统一
|
|
117
|
+
* (folder 用 applicationCode,report 用 appCode)。两边都接受,但两者都给且
|
|
118
|
+
* 不一致时拒绝,避免猜错应用。
|
|
119
|
+
*/
|
|
120
|
+
function resolveFolderAppCode(args) {
|
|
121
|
+
const fromLong = typeof args.applicationCode === "string" ? args.applicationCode.trim() : "";
|
|
122
|
+
const fromShort = typeof args.appCode === "string" ? args.appCode.trim() : "";
|
|
123
|
+
if (fromLong && fromShort && fromLong !== fromShort) {
|
|
124
|
+
throw new ToolRefusalError({
|
|
125
|
+
status: "refused",
|
|
126
|
+
blocker: "app-code-conflict",
|
|
127
|
+
applicationCode: fromLong,
|
|
128
|
+
appCode: fromShort,
|
|
129
|
+
message: `applicationCode="${fromLong}" 与 appCode="${fromShort}" 不一致——本工具两个键是同一含义的别名,请只传一个,或传相同值`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const chosen = fromLong || fromShort;
|
|
133
|
+
return chosen.length > 0 ? chosen : undefined;
|
|
134
|
+
}
|
|
77
135
|
/**
|
|
78
136
|
* Resolve a folder by id, code, or by full path ("合同管理 / 归档"). A bare name
|
|
79
137
|
* that matches several folders is ambiguous — name them with their paths instead
|
|
@@ -96,8 +154,13 @@ function resolveFolder(rows, ref) {
|
|
|
96
154
|
return byName[0];
|
|
97
155
|
if (byName.length > 1)
|
|
98
156
|
folderRefusal(undefined, `目录名「${ref}」不唯一(${byName.length} 个)——用完整 path 指定:${byName.map((row) => String(row.path)).join("、")}`);
|
|
99
|
-
|
|
157
|
+
// r28i F9: report.create.objectId / custompage page id is NOT an AppFunction
|
|
158
|
+
// tree node id. When the id is absent from the tree, say so instead of only
|
|
159
|
+
// "folder-not-found" (which sends people back to folder.tree hunting).
|
|
160
|
+
folderRefusal(undefined, `找不到目录「${ref}」——先 folder.tree 看 id/path;注意 create 省略 parentId 建的是顶层目录。` +
|
|
161
|
+
`若此 id 来自 report.create 的 objectId/reportObjectId,那不是目录 id 也不是应用树节点 id(树节点 id≠报表对象 id)——删报表用 report.delete --args '{"code":"<code>"}';删自定义页用 custompage.delete;应用树节点 id 请 folder.tree 按 code 查`, {
|
|
100
162
|
available: rows.filter((row) => row.type === "Folder").map((row) => ({ id: row.id, path: row.path })).slice(0, 40),
|
|
163
|
+
typeHint: "id-not-in-app-tree — if from report.create.objectId use report.delete by code; if AppFunction Report/CustomPage node use the matching delete tool",
|
|
101
164
|
});
|
|
102
165
|
}
|
|
103
166
|
function folderTools(tools) {
|
|
@@ -107,133 +170,201 @@ function folderTools(tools) {
|
|
|
107
170
|
kind: "read",
|
|
108
171
|
lane: LANE,
|
|
109
172
|
scope: T["folder.tree"].scope,
|
|
110
|
-
description: "
|
|
173
|
+
description: "读取**应用树**(应用**内**目录/分组 + 模型节点;不是应用包本身——应用包用 app.*)。**递归展平**并给出每个节点的 `path`/`depth`(平台 `apppackage/trees?appCode=` 返回嵌套 children,只看顶层会误判“目录没建成”——issue 2026-10-23)。`foldersOnly:true` 只回目录(type=Folder)。目录 id 就是 folder.update/delete 与 model.move/model.create 的 `folderId/parentId`。不存在的应用回 `applicationExists:false` + 空树(仍 ok,不拒——与「存在但空」可区分)。平台自动建的默认分组节点带 `isDefault:true`(判据:name_i18n.en=defaultGroup)。",
|
|
111
174
|
inputSchema: {
|
|
112
175
|
environment: environmentArg,
|
|
113
|
-
applicationCode: z.string().optional().describe("应用 code
|
|
176
|
+
applicationCode: z.string().optional().describe("应用 code;省略用配置默认应用(别名:appCode)"),
|
|
177
|
+
appCode: z.string().optional().describe("applicationCode 的别名(与 report.* 对齐);两者都给且不一致时报 app-code-conflict"),
|
|
114
178
|
foldersOnly: z.boolean().optional().describe("只回目录节点(type=Folder)"),
|
|
115
179
|
capability: capabilityArg,
|
|
116
180
|
},
|
|
117
181
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
118
|
-
const
|
|
119
|
-
const
|
|
182
|
+
const requested = resolveFolderAppCode(args);
|
|
183
|
+
const appCode = await resolveApplicationCode(session, requested);
|
|
184
|
+
// r28d F7: apppackage/trees answers [] for a missing app AND for an
|
|
185
|
+
// empty app. Distinguish with apppackage/list so a post-delete check
|
|
186
|
+
// is not forced through app.list by hand. Keep status=ok + empty
|
|
187
|
+
// nodes so existing empty-tree callers stay intact.
|
|
188
|
+
const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data);
|
|
189
|
+
const applicationExists = apps.some((row) => String(row.appCode ?? row.code ?? "") === appCode);
|
|
190
|
+
const rows = applicationExists ? await readTree(session, appCode) : [];
|
|
120
191
|
const nodes = args.foldersOnly === true ? rows.filter((row) => row.type === "Folder") : rows;
|
|
121
192
|
return {
|
|
122
193
|
applicationCode: appCode,
|
|
194
|
+
applicationExists,
|
|
123
195
|
nodeCount: nodes.length,
|
|
124
196
|
folderCount: rows.filter((row) => row.type === "Folder").length,
|
|
125
197
|
modelCount: rows.filter((row) => row.type === "BizModel").length,
|
|
126
198
|
nodes,
|
|
127
|
-
note: "type=Folder
|
|
199
|
+
note: "type=Folder 是应用内目录/分组(不是应用包);parentId 指向父目录(顶层目录的 parentId = 应用包 id)。嵌套 children 已展平,path 用「 / 」连接。" +
|
|
200
|
+
(applicationExists
|
|
201
|
+
? " 平台自动建的默认分组带 isDefault:true(name_i18n.en=defaultGroup);模型节点另有 displayPath。"
|
|
202
|
+
: " applicationExists:false——应用不在 apppackage/list;空树不等于「空应用」。") +
|
|
203
|
+
" nodeCount/folderCount/modelCount 是全应用聚合,共享环境含其它会话资源;核对自己的资源请按 code 前缀过滤 nodes[],不要只比计数。",
|
|
128
204
|
};
|
|
129
205
|
}),
|
|
130
206
|
}));
|
|
131
|
-
writeTool(
|
|
132
|
-
|
|
133
|
-
name:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
207
|
+
writeTool({
|
|
208
|
+
tools,
|
|
209
|
+
name: "folder.create",
|
|
210
|
+
lane: LANE,
|
|
211
|
+
scope: T["folder.create"].scope,
|
|
212
|
+
description: "新建应用目录(分组):`POST /api/app/folders/create`(AppFunctionController:105;`ApplicationFacadeImpl.createFolder:586` 强制 `type=Folder`,**省略 `parentId` 会挂到应用根**,即顶层目录)。给 `parentId` 才是子目录。写入后按 `path` 回读确认(树是嵌套的)。同名不等于重复:平台不校验重名,工具默认按「同父目录下同名」拒绝(`folder-duplicate`),要并列同名传 `allowDuplicate:true`。",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
applicationCode: z.string().optional().describe("应用 code;省略用配置默认应用(别名:appCode)"),
|
|
215
|
+
appCode: z.string().optional().describe("applicationCode 的别名(与 report.* 对齐);两者都给且不一致时报 app-code-conflict"),
|
|
216
|
+
name: z.string().min(1).describe("目录显示名(平台校验非空、中文长度 ≤50)"),
|
|
217
|
+
parentId: z.string().optional().describe("父目录 id;**省略=应用根(顶层目录)**。子目录必须显式给,值来自 folder.tree 的 id"),
|
|
218
|
+
code: z.string().optional().describe("目录 code(可选;平台只校验 code 唯一,空则自动生成)"),
|
|
219
|
+
name_i18n: z.string().optional().describe("多语言名(JSON 字符串,如 {\"zh-CN\":\"合同\",\"en\":\"Contract\"})"),
|
|
220
|
+
allowDuplicate: z.boolean().optional().describe("允许同父目录下同名(默认拒绝,避免建出两个同名目录后无法用 path 定位)"),
|
|
221
|
+
},
|
|
222
|
+
execute: async (session, args) => {
|
|
223
|
+
const appCode = await resolveApplicationCode(session, resolveFolderAppCode(args));
|
|
224
|
+
const before = await readTree(session, appCode);
|
|
225
|
+
if (args.allowDuplicate !== true) {
|
|
226
|
+
const siblings = before.filter((row) => String(row.parentId ?? "") === String(args.parentId ?? "") || (args.parentId === undefined && row.depth === 0));
|
|
227
|
+
if (siblings.some((row) => row.type === "Folder" && String(row.name ?? "") === args.name))
|
|
228
|
+
throw new ToolRefusalError({
|
|
229
|
+
status: "refused",
|
|
230
|
+
blocker: "folder-duplicate",
|
|
231
|
+
applicationCode: appCode,
|
|
232
|
+
path: `${args.parentId ?? "(root)"} / ${args.name}`,
|
|
233
|
+
message: `同一层级已有同名目录「${args.name}」——平台允许重名,但重名后 folder.tree 的 path 无法唯一定位;确认要并列同名再传 allowDuplicate:true`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const performed = await wireCall(session, {
|
|
237
|
+
path: ENDPOINTS.folderCreate,
|
|
238
|
+
method: "POST",
|
|
239
|
+
body: {
|
|
240
|
+
appCode,
|
|
241
|
+
name: args.name,
|
|
242
|
+
...(args.parentId !== undefined ? { parentId: args.parentId } : {}),
|
|
243
|
+
...(args.code !== undefined ? { code: args.code } : {}),
|
|
244
|
+
...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}),
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
const after = await readTree(session, appCode);
|
|
248
|
+
const created = after.filter((row) => row.type === "Folder" && String(row.name ?? "") === args.name && !before.some((old) => old.id === row.id));
|
|
249
|
+
return {
|
|
250
|
+
performed: { errcode: performed.errcode },
|
|
251
|
+
readback: {
|
|
147
252
|
applicationCode: appCode,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
...(args.code !== undefined ? { code: args.code } : {}),
|
|
160
|
-
...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}),
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
const after = await readTree(session, appCode);
|
|
164
|
-
const created = after.filter((row) => row.type === "Folder" && String(row.name ?? "") === args.name && !before.some((old) => old.id === row.id));
|
|
165
|
-
return {
|
|
166
|
-
performed: { errcode: performed.errcode },
|
|
167
|
-
readback: {
|
|
168
|
-
applicationCode: appCode,
|
|
169
|
-
present: created.length > 0,
|
|
170
|
-
created: created.map((row) => ({ id: row.id, path: row.path, parentId: row.parentId })),
|
|
171
|
-
folderCount: after.filter((row) => row.type === "Folder").length,
|
|
172
|
-
...(after.filter((row) => row.type === "Folder" && String(row.name ?? "") === args.name && String(row.parentId ?? "") === String(created[0]?.parentId ?? ""))
|
|
173
|
-
.length > 1
|
|
174
|
-
? { warning: "同层级出现了多个同名目录(allowDuplicate 生效或既有同名);后续用 id 定位,不要用 path/name" }
|
|
175
|
-
: {}),
|
|
176
|
-
hint: created.length > 0 ? "把模型放进该目录用 model.move(parentId = 这里的 id)" : "回读未见新目录:树是嵌套的,但 folder.tree 已递归展平;若确实没有,检查应用 code",
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
});
|
|
180
|
-
writeTool(tools, "folder.update", LANE, T["folder.update"].scope, "重命名目录或把目录挂到另一个目录下:`POST /api/app/folders/update`(AppFunctionController:219;`updateFolder:612` 拒绝 `parentId === id`,`parentId` 为空时保留原父级)。**只改目录节点本身,不会移动目录里的模型**——模型移动用 `model.move`。", {
|
|
181
|
-
applicationCode: z.string().optional(),
|
|
182
|
-
folderId: z.string().min(1).describe("目录 id(folder.tree 的 id)"),
|
|
183
|
-
name: z.string().min(1).optional().describe("新显示名"),
|
|
184
|
-
name_i18n: z.string().optional(),
|
|
185
|
-
parentId: z.string().min(1).optional().describe("新父目录 id(挂到根用应用包 id;不理解根 id 就省略此字段表示不改父级)"),
|
|
186
|
-
}, async (session, args) => {
|
|
187
|
-
const appCode = await resolveApplicationCode(session, args.applicationCode);
|
|
188
|
-
const before = await readTree(session, appCode);
|
|
189
|
-
const current = resolveFolder(before, args.folderId);
|
|
190
|
-
if (args.name === undefined && args.parentId === undefined)
|
|
191
|
-
throw new ToolRefusalError({ status: "refused", blocker: "folder-update-empty-patch", message: "没有可应用的补丁——至少给 name 或 parentId" });
|
|
192
|
-
if (args.parentId !== undefined && args.parentId === args.folderId)
|
|
193
|
-
throw new ToolRefusalError({ status: "refused", blocker: "folder-parent-self", message: "parentId 不能等于目录自身(平台 FOLDER_PARENTID_EQUALS_ID)" });
|
|
194
|
-
const performed = await wireCall(session, {
|
|
195
|
-
path: ENDPOINTS.folderUpdate,
|
|
196
|
-
method: "POST",
|
|
197
|
-
body: { id: args.folderId, name: args.name ?? current.name, ...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}), ...(args.parentId !== undefined ? { parentId: args.parentId } : {}) },
|
|
198
|
-
});
|
|
199
|
-
const after = await readTree(session, appCode);
|
|
200
|
-
const now = after.find((row) => String(row.id ?? "") === args.folderId);
|
|
201
|
-
return {
|
|
202
|
-
performed: { errcode: performed.errcode },
|
|
203
|
-
readback: {
|
|
204
|
-
folderId: args.folderId,
|
|
205
|
-
present: now !== undefined,
|
|
206
|
-
before: { name: current.name, parentId: current.parentId, path: current.path },
|
|
207
|
-
after: now ? { name: now.name, parentId: now.parentId, path: now.path } : null,
|
|
208
|
-
note: "只改了目录节点;目录内的模型不随目录移动(模型要动请用 model.move)",
|
|
209
|
-
},
|
|
210
|
-
};
|
|
253
|
+
present: created.length > 0,
|
|
254
|
+
created: created.map((row) => ({ id: row.id, path: row.path, parentId: row.parentId })),
|
|
255
|
+
folderCount: after.filter((row) => row.type === "Folder").length,
|
|
256
|
+
...(after.filter((row) => row.type === "Folder" && String(row.name ?? "") === args.name && String(row.parentId ?? "") === String(created[0]?.parentId ?? ""))
|
|
257
|
+
.length > 1
|
|
258
|
+
? { warning: "同层级出现了多个同名目录(allowDuplicate 生效或既有同名);后续用 id 定位,不要用 path/name" }
|
|
259
|
+
: {}),
|
|
260
|
+
hint: created.length > 0 ? "把模型放进该目录用 model.move(parentId = 这里的 id)" : "回读未见新目录:树是嵌套的,但 folder.tree 已递归展平;若确实没有,检查应用 code",
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
},
|
|
211
264
|
});
|
|
212
|
-
writeTool(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
265
|
+
writeTool({
|
|
266
|
+
tools,
|
|
267
|
+
name: "folder.update",
|
|
268
|
+
lane: LANE,
|
|
269
|
+
scope: T["folder.update"].scope,
|
|
270
|
+
description: "重命名目录或把目录挂到另一个目录下:`POST /api/app/folders/update`(AppFunctionController:219;`updateFolder:612` 拒绝 `parentId === id`,`parentId` 为空时保留原父级)。**只改目录节点本身,不会移动目录里的模型**——模型移动用 `model.move`。",
|
|
271
|
+
inputSchema: {
|
|
272
|
+
applicationCode: z.string().optional().describe("应用 code;省略用配置默认应用(别名:appCode)"),
|
|
273
|
+
appCode: z.string().optional().describe("applicationCode 的别名(与 report.* 对齐);两者都给且不一致时报 app-code-conflict"),
|
|
274
|
+
folderId: z.string().min(1).describe("目录 id(folder.tree 的 id)"),
|
|
275
|
+
name: z.string().min(1).optional().describe("新显示名"),
|
|
276
|
+
name_i18n: z.string().optional(),
|
|
277
|
+
parentId: z.string().min(1).optional().describe("新父目录 id(挂到根用应用包 id;不理解根 id 就省略此字段表示不改父级)"),
|
|
278
|
+
},
|
|
279
|
+
execute: async (session, args) => {
|
|
280
|
+
const appCode = await resolveApplicationCode(session, resolveFolderAppCode(args));
|
|
281
|
+
const before = await readTree(session, appCode);
|
|
282
|
+
const current = resolveFolder(before, args.folderId);
|
|
283
|
+
if (args.name === undefined && args.parentId === undefined)
|
|
284
|
+
throw new ToolRefusalError({ status: "refused", blocker: "folder-update-empty-patch", message: "没有可应用的补丁——至少给 name 或 parentId" });
|
|
285
|
+
if (args.parentId !== undefined && args.parentId === args.folderId)
|
|
286
|
+
throw new ToolRefusalError({ status: "refused", blocker: "folder-parent-self", message: "parentId 不能等于目录自身(平台 FOLDER_PARENTID_EQUALS_ID)" });
|
|
287
|
+
const performed = await wireCall(session, {
|
|
288
|
+
path: ENDPOINTS.folderUpdate,
|
|
289
|
+
method: "POST",
|
|
290
|
+
body: { id: args.folderId, name: args.name ?? current.name, ...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}), ...(args.parentId !== undefined ? { parentId: args.parentId } : {}) },
|
|
229
291
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
292
|
+
const after = await readTree(session, appCode);
|
|
293
|
+
const now = after.find((row) => String(row.id ?? "") === args.folderId);
|
|
294
|
+
return {
|
|
295
|
+
performed: { errcode: performed.errcode },
|
|
296
|
+
readback: {
|
|
297
|
+
folderId: args.folderId,
|
|
298
|
+
present: now !== undefined,
|
|
299
|
+
before: { name: current.name, parentId: current.parentId, path: current.path },
|
|
300
|
+
after: now ? { name: now.name, parentId: now.parentId, path: now.path } : null,
|
|
301
|
+
note: "只改了目录节点;目录内的模型不随目录移动(模型要动请用 model.move)",
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
},
|
|
236
305
|
});
|
|
306
|
+
writeTool({
|
|
307
|
+
tools,
|
|
308
|
+
name: "folder.delete",
|
|
309
|
+
lane: LANE,
|
|
310
|
+
scope: T["folder.delete"].scope,
|
|
311
|
+
description: "删除空目录:`POST /api/app/folders/delete?folderId=`(AppFunctionController:246)。**非递归**——目录里还有子目录或模型时平台回 `DIRECTORY_NOT_EMPTY`,工具在写前先查子节点并拒绝(`folder-not-empty`),避免半删。要清空请先 `model.move` 移走模型、删子目录。",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
applicationCode: z.string().optional().describe("应用 code;省略用配置默认应用(别名:appCode)"),
|
|
314
|
+
appCode: z.string().optional().describe("applicationCode 的别名(与 report.* 对齐);两者都给且不一致时报 app-code-conflict"),
|
|
315
|
+
folderId: z.string().min(1),
|
|
316
|
+
},
|
|
317
|
+
execute: async (session, args) => {
|
|
318
|
+
const appCode = await resolveApplicationCode(session, resolveFolderAppCode(args));
|
|
319
|
+
const before = await readTree(session, appCode);
|
|
320
|
+
const current = resolveFolder(before, args.folderId);
|
|
321
|
+
if (String(current.type ?? "") !== "Folder")
|
|
322
|
+
throw new ToolRefusalError({
|
|
323
|
+
status: "refused",
|
|
324
|
+
blocker: "folder-target-not-folder",
|
|
325
|
+
message: `${args.folderId} 不是目录节点(type=${String(current.type)});${folderDeleteTargetHint(String(current.type ?? ""), current)}`,
|
|
326
|
+
});
|
|
327
|
+
const children = before.filter((row) => String(row.parentId ?? "") === String(args.folderId));
|
|
328
|
+
if (children.length > 0)
|
|
329
|
+
throw new ToolRefusalError({
|
|
330
|
+
status: "refused",
|
|
331
|
+
blocker: "folder-not-empty",
|
|
332
|
+
folderId: args.folderId,
|
|
333
|
+
children: children.map((row) => ({ id: row.id, type: row.type, path: row.path })).slice(0, 40),
|
|
334
|
+
message: `目录「${current.path}」还有 ${children.length} 个子节点——平台非递归删除会回 DIRECTORY_NOT_EMPTY。先 model.move 移走模型 / 删子目录`,
|
|
335
|
+
});
|
|
336
|
+
const performed = await wireCall(session, { path: ENDPOINTS.folderDelete, method: "POST", query: { folderId: args.folderId } });
|
|
337
|
+
const after = await readTree(session, appCode);
|
|
338
|
+
return {
|
|
339
|
+
performed: { errcode: performed.errcode },
|
|
340
|
+
readback: { folderId: args.folderId, deleted: !after.some((row) => String(row.id ?? "") === args.folderId), path: current.path },
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* r27 F10: `folder.delete` must NOT send every non-Folder node to
|
|
347
|
+
* `model.delete` — a Report node then comes back `already-absent` and the
|
|
348
|
+
* app-tree residue stays. Dispatch by AppFunction type so the recovery
|
|
349
|
+
* points at the tool that actually owns that node.
|
|
350
|
+
*/
|
|
351
|
+
export function folderDeleteTargetHint(type, current = {}) {
|
|
352
|
+
const code = String(current.code ?? "").trim();
|
|
353
|
+
const codeArg = code ? `{"code":"${code}"}` : '{"code":"<code>"}';
|
|
354
|
+
switch (type) {
|
|
355
|
+
case "Report":
|
|
356
|
+
return (`删报表用 omc call report.delete --args '${codeArg}'` +
|
|
357
|
+
"(若 BI 端点不可达,report.delete 会说明环境缺失)");
|
|
358
|
+
case "BizModel":
|
|
359
|
+
return code
|
|
360
|
+
? `删模型用 omc call model.delete --args '{"schemaCode":"${code}"}'`
|
|
361
|
+
: "删模型用 model.delete";
|
|
362
|
+
case "CustomPage":
|
|
363
|
+
case "Page":
|
|
364
|
+
return `删自定义页用 omc call custompage.delete --args '${codeArg}'`;
|
|
365
|
+
default:
|
|
366
|
+
return "按节点 type 选择对应删除工具(folder.delete 只删空目录)";
|
|
367
|
+
}
|
|
237
368
|
}
|
|
238
369
|
export function registerFolderLane(tools) {
|
|
239
370
|
folderTools(tools);
|
|
@@ -20,7 +20,7 @@ export const FORM_WIRE = Object.freeze({
|
|
|
20
20
|
});
|
|
21
21
|
const FORM_GET_DESCRIPTION = '读取默认表单(bizsheet/get)的发布状态与控件清单(紧凑默认)。**发布态以返回的 `published` / `existDraft` 布尔为准**(`published:true`=已发布;`published:false,existDraft:true`=只有草稿,**未发布**)——不要用 `publishedControlCodes` 是否非空来推断(草稿字段会让它非空,误判为已发布)。**每个模型交付前其默认表单必须 `published:true`**:用 `configure_form` 或 `form.draft(rebuild:true)`+`form.publish` 发布,并逐模型 `form.get` 核对。`full:true` 还返回 `options` 与解析后的 `customButtonJson`(表单工具栏按钮)。传 `controls:["code",…]` 只取指定控件的 options(如关联控件的 mappings);传 `full:true` 才返回 draft/published 的完整 view/attributes/html/actions JSON(actions 列为设计器保存/发布的表单级按钮动作)。体积大,仅在需要整体布局时用。';
|
|
22
22
|
const FORM_DRAFT_DESCRIPTION = "Save the default-form DRAFT (bizsheet/update). Reads the current sheet, merges the caller's explicit draftViewJson/draftAttributesJson (or preserves the existing draft); when both are absent on a fresh default form, composes the designer-standard scaffold from the model's published fields. A redundant field (relation-mapping carry-through target) always stays bound; with no workflow it is hidden in the design (options.visible=false), with a workflow it stays visible for per-node propertyPermissions. Pass `draftHtmlJson` to thread custom online-script HTML through. Empty-layout guard: explicitly passing \"[]\"/\"{}\" view/attributes is refused (form-empty-layout) — the platform designer's Save is NOT lossless and clears drafts to exactly that shape; escape with allowEmptyLayout:true, and recover cleared drafts from the form version history (/api/api/app/bizsheet/history/list). **读回原样回写**:从 `form.get` 取回的 view/attributes 直接回传时加 `fromReadback:true`——工具会与已存草稿/发布面逐字比对,一致才放行(平台自身布局可能含子表列行/系统行默认宽度),被改动过的面照常按 validation 拒绝。**表单工具栏按钮**:`buttons`(数组)或 `customButtonJson`(原样字符串)写进 `biz_sheet.options.customButtonJson`;**要到 form.publish 才生成 h_biz_button(triggerType=FORM)**,且表单页是否显示还取决于流程节点 `buttonPermission.customBtns[].enabled`(设计器「表单操作」勾选)。`useLocation` 用 FORM/ROW/LIST,表单工具栏按钮用 FORM。 **表单控件属性(含关联填充 mappings)从字段行派生**:改关联映射的事实源在字段 options.mappings(field.update → field.publish),直接改这里的控件映射会被平台按字段重算覆盖(真机 2026-10-14 实证);字段发布后如需刷新已发布表单的控件映射,用本工具 rebuild:false 重存(保留现有布局与在线JS)再 form.publish。";
|
|
23
|
-
const FORM_PUBLISH_DESCRIPTION = "Publish the default form (bizsheet/publish, proven shape: draft*Json promoted to published*Json verbatim) with published-face readback. Degrade guard: refuses to promote a draft carrying FEWER controls than the published face (form-publish-degrade) — the designer's Save clears drafts non-losslessly; recover via the form version history (/api/api/app/bizsheet/history/list) and rewrite the draft first, or pass allowDegrade:true to force the reduction. rebuild:true is the one-call refresh: discard the existing draft, re-scaffold from the published fields (non-structural control options preserved), then force-promote — use it after a field shape change. **表单工具栏按钮**:传 `buttons`(或 `customButtonJson`)→ 写进 `options.customButtonJson` 并在本次 publish 生成 `h_biz_button(triggerType=FORM, triggerCode=<formCode>)`(平台 `publishBizForm:1022`);`options.customButtonJson` 必须是**字符串化数组**(对象会 50000 syntax error);表单页是否显示再由流程节点 `buttonPermission.customBtns[].enabled` 过滤。";
|
|
23
|
+
const FORM_PUBLISH_DESCRIPTION = "Publish the default form (bizsheet/publish, proven shape: draft*Json promoted to published*Json verbatim) with published-face readback. **不影响在线 JS**:本工具只提拔布局草稿;改 JS 请用 onlinejs.compose(它自己会连同整张表单一起发布,不必再调本工具)。 Degrade guard: refuses to promote a draft carrying FEWER controls than the published face (form-publish-degrade) — the designer's Save clears drafts non-losslessly; recover via the form version history (/api/api/app/bizsheet/history/list) and rewrite the draft first, or pass allowDegrade:true to force the reduction. rebuild:true is the one-call refresh: discard the existing draft, re-scaffold from the published fields (non-structural control options preserved), then force-promote — use it after a field shape change. **表单工具栏按钮**:传 `buttons`(或 `customButtonJson`)→ 写进 `options.customButtonJson` 并在本次 publish 生成 `h_biz_button(triggerType=FORM, triggerCode=<formCode>)`(平台 `publishBizForm:1022`);`options.customButtonJson` 必须是**字符串化数组**(对象会 50000 syntax error);表单页是否显示再由流程节点 `buttonPermission.customBtns[].enabled` 过滤。";
|
|
24
24
|
const FORM_RUN_BUTTON_DESCRIPTION = "执行表单工具栏按钮(平台同款面:`POST /api/api/runtime/form/executeCustomButton`,body `{bizButtonModel, bizObjectList:[<完整业务对象>], reacquireDataFromDb:true}`)。**与列表行按钮不同**:列表按钮发 `launcher/domain_service_runtime/<schema>/<rule>` + `ids[]`(用 `rule.run`),表单按钮带**整条记录**,不能互换。按钮来源 `GET /api/api/system/getCustomButtons`(只列 triggerType=FORM 的 h_biz_button)。前提:按钮已由 `form.publish` 的 `buttons`/`customButtonJson` 生成。返回是执行结果信封,规则副作用用 `data.load` 另读;被规则 UPDATE 的目标行须是 COMPLETED(引擎查不到 DRAFT)。`302045` = 数据被占用。**`10005` 执行失败时优先换等价面 `rule.run`(launcher/domain_service_runtime/<schema>/<rule> + ids[])**——同一绑定规则在 rule.run 面已验证可用(r28b 实证),不要在 executeCustomButton 上反复重试。";
|
|
25
25
|
const CONFIGURE_FORM_DESCRIPTION = "High-level: bind fields into the default form and publish it (form.draft → form.publish), subject to the write permission gate. Delegates to the proven form.draft scaffold + form.publish paths so the real empty-form layout (and any caller-supplied draftHtmlJson) is persisted and promoted — never bypasses with a shortcut body. `sheetCode` is optional: omitted means the model's default sheet (sheetCode = schemaCode).";
|
|
26
26
|
export const FORM_LANE_CONTRACT = Object.freeze({
|