@stalfh233/omc-cli 0.4.87 → 0.4.90
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 +3 -1
- package/dist/cli-contract.js +97 -7
- package/dist/cli.js +103 -15
- package/dist/endpoints.js +10 -0
- package/dist/goal-routes.js +73 -13
- package/dist/lanes/field.js +74 -2
- package/dist/lanes/list-view.js +21 -1
- package/dist/lanes/model.js +4 -1
- package/dist/lanes/org-tree.js +52 -0
- package/dist/lanes/organization.js +110 -6
- package/dist/lanes/rule-lifecycle.js +26 -5
- package/dist/lanes/runtime-data.js +59 -3
- package/dist/lanes/workflow.js +95 -70
- package/dist/model-purge.js +343 -0
- package/dist/path-projection.js +40 -0
- package/dist/recipes/businessrule.js +3 -0
- package/dist/recipes/list-view.js +15 -0
- package/dist/rule-support.js +8 -0
- package/dist/runtime-support.js +1 -0
- package/dist/write.js +1 -1
- package/package.json +3 -3
- package/skills/omc-start-here.md +2 -1
package/dist/lanes/field.js
CHANGED
|
@@ -45,6 +45,54 @@ function fieldRollbackSpec(toolName) {
|
|
|
45
45
|
function parseFieldOptionsForReadback(options) {
|
|
46
46
|
return decodeJsonColumn(options, JSON_COLUMN_PASSTHROUGH);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* CHILD_TABLE(8) columns as a compact list (issue 2026-10-25 §7).
|
|
50
|
+
*
|
|
51
|
+
* The stored `subSchema` is the full designer payload — system columns included,
|
|
52
|
+
* ~37.6 KB for a 10-column child table when asked for one field by code. A caller
|
|
53
|
+
* almost always wants the column codes (to build formData / verify the attach);
|
|
54
|
+
* the raw payload stays available behind `full:true`. Accepts both the object and
|
|
55
|
+
* the JSON-string wire form.
|
|
56
|
+
*/
|
|
57
|
+
function childColumnsOf(subSchema) {
|
|
58
|
+
let parsed = subSchema;
|
|
59
|
+
if (typeof subSchema === "string") {
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(subSchema);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (parsed === null || typeof parsed !== "object")
|
|
68
|
+
return [];
|
|
69
|
+
const properties = parsed.properties;
|
|
70
|
+
if (!Array.isArray(properties))
|
|
71
|
+
return [];
|
|
72
|
+
return properties
|
|
73
|
+
.filter((child) => child !== null && typeof child === "object")
|
|
74
|
+
.map((child) => {
|
|
75
|
+
const rawType = child.propertyType ?? child.type;
|
|
76
|
+
const type = rawType === undefined || rawType === null || Number.isNaN(Number(rawType)) ? null : Number(rawType);
|
|
77
|
+
const typeLabel = type === null ? undefined : fieldTypeLabel(type);
|
|
78
|
+
const code = String(child.code ?? child.propertyCode ?? "");
|
|
79
|
+
return {
|
|
80
|
+
code,
|
|
81
|
+
name: String(child.name ?? child.code ?? child.propertyCode ?? ""),
|
|
82
|
+
type,
|
|
83
|
+
...(typeLabel !== undefined ? { typeName: typeLabel } : {}),
|
|
84
|
+
...(typeof child.relativeCode === "string" && child.relativeCode ? { relativeCode: child.relativeCode } : {}),
|
|
85
|
+
// r15 feedback: the projection mixed the platform's system columns with
|
|
86
|
+
// the authored ones, so "which child columns did I declare?" still needed
|
|
87
|
+
// a filter. Keep every column but mark the system ones.
|
|
88
|
+
// `parentId` / `sortKey` are injected by the platform into every child
|
|
89
|
+
// schema (they are the child row's link + order), so they are system
|
|
90
|
+
// columns here even though a MAIN model never carries them.
|
|
91
|
+
...(SYSTEM_FIELD_CODES.has(code) || code === "parentId" || code === "sortKey" ? { system: true } : {}),
|
|
92
|
+
};
|
|
93
|
+
})
|
|
94
|
+
.filter((child) => child.code.length > 0);
|
|
95
|
+
}
|
|
48
96
|
/**
|
|
49
97
|
* `name_i18n` is a JSON locale map. When every locale value equals the display
|
|
50
98
|
* `name` there is no translation to convey, so echoing the map on every field of
|
|
@@ -117,7 +165,21 @@ export function registerFieldLane(tools) {
|
|
|
117
165
|
...(redundantNameI18n(row.name_i18n, row.name) ? {} : typeof row.name_i18n === "string" ? { name_i18n: row.name_i18n } : {}),
|
|
118
166
|
type: row.type ?? row.propertyType,
|
|
119
167
|
...(typeLabel !== undefined ? { typeName: typeLabel } : {}),
|
|
120
|
-
|
|
168
|
+
// A CHILD_TABLE row's `subSchema` is the whole designer payload; the
|
|
169
|
+
// default face carries the column list + count and keeps the raw
|
|
170
|
+
// payload behind `full:true` (issue 2026-10-25 §7: one field read was
|
|
171
|
+
// 37.6 KB for a 5-code answer).
|
|
172
|
+
...(row.subSchema !== undefined && row.subSchema !== null
|
|
173
|
+
? args.full === true
|
|
174
|
+
? { subSchema: row.subSchema }
|
|
175
|
+
: (() => {
|
|
176
|
+
const subColumns = childColumnsOf(row.subSchema);
|
|
177
|
+
const businessColumns = subColumns.filter((column) => column.system !== true).length;
|
|
178
|
+
return subColumns.length > 0
|
|
179
|
+
? { subColumnCount: subColumns.length, businessSubColumnCount: businessColumns, subColumns, subSchemaHint: "子表列已投影为 code/name/type(system:true 是平台注入的系统列);要平台原始 subSchema 加 full:true" }
|
|
180
|
+
: { subSchema: row.subSchema };
|
|
181
|
+
})()
|
|
182
|
+
: {}),
|
|
121
183
|
...(row.relativeCode ? { relativeCode: row.relativeCode } : {}),
|
|
122
184
|
...(typeof row.relativePropertyCode === "string" && row.relativePropertyCode ? { relativePropertyCode: row.relativePropertyCode } : {}),
|
|
123
185
|
...(options !== undefined ? { options } : {}),
|
|
@@ -150,11 +212,20 @@ export function registerFieldLane(tools) {
|
|
|
150
212
|
const value = options[key];
|
|
151
213
|
return key === "readonlyFormula" ? value === true : typeof value === "string" && value.trim() !== "";
|
|
152
214
|
});
|
|
215
|
+
// Inventory reads could not see child columns at all (r13 §7): the
|
|
216
|
+
// only face that carried them was the 37.6 KB subSchema. Codes alone
|
|
217
|
+
// are ~12 B each and answer "did the child columns land?".
|
|
218
|
+
const subColumns = field.subColumns;
|
|
219
|
+
const businessCodes = Array.isArray(subColumns) ? subColumns.filter((column) => column.system !== true).map((column) => column.code) : undefined;
|
|
153
220
|
return {
|
|
154
221
|
code: field.code,
|
|
155
222
|
type: field.type,
|
|
156
223
|
...(field.typeName !== undefined ? { typeName: field.typeName } : {}),
|
|
157
224
|
...(field.name !== undefined ? { name: field.name } : {}),
|
|
225
|
+
// Inventory answers "which columns did I author?" — system
|
|
226
|
+
// columns are reported as a count, not mixed into the list.
|
|
227
|
+
...(businessCodes !== undefined ? { subColumnCodes: businessCodes } : {}),
|
|
228
|
+
...(Array.isArray(subColumns) && subColumns.length > businessCodes.length ? { systemSubColumnCount: subColumns.length - businessCodes.length } : {}),
|
|
158
229
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
159
230
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
160
231
|
};
|
|
@@ -202,6 +273,7 @@ export function registerFieldLane(tools) {
|
|
|
202
273
|
type: field.type,
|
|
203
274
|
...(field.typeName !== undefined ? { typeName: field.typeName } : {}),
|
|
204
275
|
...(field.name !== undefined ? { name: field.name } : {}),
|
|
276
|
+
...(Array.isArray(field.subColumns) ? { subColumns: field.subColumns } : {}),
|
|
205
277
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
206
278
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
207
279
|
};
|
|
@@ -225,7 +297,7 @@ export function registerFieldLane(tools) {
|
|
|
225
297
|
const draft = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data);
|
|
226
298
|
const published = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
|
|
227
299
|
if ([...draft, ...published].some((row) => (row.code ?? row.propertyCode) === args.code))
|
|
228
|
-
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST` });
|
|
300
|
+
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST`, nextCommand: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: true, code: args.code })}'` });
|
|
229
301
|
// One seam owns field-change normalization, every reproduced gate, and the
|
|
230
302
|
// wire body (src/field-change.ts), so configure_model and intent share it.
|
|
231
303
|
const prepared = prepareFieldChange(args);
|
package/dist/lanes/list-view.js
CHANGED
|
@@ -776,7 +776,27 @@ export function registerListViewLane(tools) {
|
|
|
776
776
|
const before = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
|
|
777
777
|
if (before.data == null)
|
|
778
778
|
return { performed: { skipped: "already-absent" }, readback: { absent: true, header: null } };
|
|
779
|
-
|
|
779
|
+
let performed;
|
|
780
|
+
try {
|
|
781
|
+
performed = await wireCall(session, { path: ENDPOINTS.listQueryDelete, method: "POST", query: { schemaCode: args.schemaCode, code: args.code } });
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
// 306103 删除后将无可用视图 — the platform requires at least one visible
|
|
785
|
+
// list view per model. r15's `nextCommand` fell back to the generic
|
|
786
|
+
// refusal router; name the constraint and the only real deletion path.
|
|
787
|
+
if (error instanceof WireError && error.kind === "business" && (error.errcode === 306103 || /无可用视图/.test(error.message)))
|
|
788
|
+
throw new ToolRefusalError({
|
|
789
|
+
status: "refused",
|
|
790
|
+
blocker: "listview-last-visible",
|
|
791
|
+
schemaCode: args.schemaCode,
|
|
792
|
+
code: args.code,
|
|
793
|
+
nextCommand: `omc model-purge --code ${args.schemaCode}`,
|
|
794
|
+
message: `不能删除视图 ${args.code}:平台要求每个模型至少保留一条可见列表视图(306103)。` +
|
|
795
|
+
`单独删最后一条视图没有意义——视图会随模型一起级联删除。要继续用这个模型就**就地改**(listview.configure 省略 code 即改默认视图),` +
|
|
796
|
+
`要整体清理就用 model-purge(默认 dry-run,先看计划)。`,
|
|
797
|
+
});
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
780
800
|
const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
|
|
781
801
|
return { performed: { errcode: performed.errcode }, readback: { absent: readback.data == null, header: readback.data ?? null } };
|
|
782
802
|
}, undefined, undefined, listviewRollbackSpec("listview.delete"));
|
package/dist/lanes/model.js
CHANGED
|
@@ -199,7 +199,10 @@ export function registerModelLane(tools) {
|
|
|
199
199
|
const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
200
200
|
const all = tree
|
|
201
201
|
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code)
|
|
202
|
-
|
|
202
|
+
// `schemaCode` is the parameter name every other tool takes; `code` is
|
|
203
|
+
// the tree row's key. Echo both so `--jq 'models[].schemaCode'` works
|
|
204
|
+
// (r15 P6: the alias was missing and the path missed).
|
|
205
|
+
.map((row) => ({ code: row.code, schemaCode: row.code, name: row.name ?? row.text ?? null }));
|
|
203
206
|
const needle = args.filter?.trim();
|
|
204
207
|
const models = needle ? all.filter((model) => String(model.code).includes(needle) || String(model.name ?? "").includes(needle)) : all;
|
|
205
208
|
// `filterApplied` disambiguates an empty result: it distinguishes "the
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ENDPOINTS } from "../endpoints.js";
|
|
2
|
+
import { wireCall, wireRows } from "../wire.js";
|
|
3
|
+
export async function collectOrgDepartments(session, options = {}) {
|
|
4
|
+
const limit = options.limit ?? 500;
|
|
5
|
+
const fetchLevel = async (deptId) => {
|
|
6
|
+
const query = deptId === undefined ? {} : { deptIds: deptId };
|
|
7
|
+
const data = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentTree, query })).data;
|
|
8
|
+
return Array.isArray(data?.departmentList) ? data.departmentList : wireRows(data);
|
|
9
|
+
};
|
|
10
|
+
const rows = [];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const push = (level) => {
|
|
13
|
+
const added = [];
|
|
14
|
+
for (const row of level) {
|
|
15
|
+
const id = String(row.id ?? "");
|
|
16
|
+
if (id === "" || seen.has(id))
|
|
17
|
+
continue;
|
|
18
|
+
seen.add(id);
|
|
19
|
+
rows.push({ ...row, id, name: String(row.name ?? ""), parentId: String(row.parentId ?? "") });
|
|
20
|
+
added.push(id);
|
|
21
|
+
}
|
|
22
|
+
return added;
|
|
23
|
+
};
|
|
24
|
+
const queue = [];
|
|
25
|
+
if (options.rootId !== undefined) {
|
|
26
|
+
try {
|
|
27
|
+
const self = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentGet, query: { deptId: options.rootId } })).data;
|
|
28
|
+
if (self)
|
|
29
|
+
push([self]);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// unknown id → empty result
|
|
33
|
+
}
|
|
34
|
+
queue.push(...push(await fetchLevel(options.rootId)));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const roots = await fetchLevel(undefined);
|
|
38
|
+
push(roots);
|
|
39
|
+
queue.push(...roots.map((row) => String(row.id ?? "")).filter((id) => id.length > 0));
|
|
40
|
+
}
|
|
41
|
+
let truncated = false;
|
|
42
|
+
while (queue.length > 0) {
|
|
43
|
+
if (rows.length >= limit) {
|
|
44
|
+
truncated = true;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const parentId = queue.shift();
|
|
48
|
+
queue.push(...push(await fetchLevel(parentId)));
|
|
49
|
+
}
|
|
50
|
+
return { rows, truncated };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=org-tree.js.map
|
|
@@ -7,6 +7,7 @@ import { wireCall, wireRows, WireError } from "../wire.js";
|
|
|
7
7
|
import { ENDPOINTS } from "../endpoints.js";
|
|
8
8
|
import { environmentArg, capabilityArg } from "../args.js";
|
|
9
9
|
import { LANES } from "./lane-ids.js";
|
|
10
|
+
import { collectOrgDepartments } from "./org-tree.js";
|
|
10
11
|
/**
|
|
11
12
|
* Organization WRITE faces (8.6.41 customer transcript, 2026-10-21).
|
|
12
13
|
*
|
|
@@ -130,11 +131,33 @@ export function registerOrganizationLane(tools) {
|
|
|
130
131
|
query: { ...orgQuery(session), roleId: String(args.roleId), page: String(args.page ?? 0), size: String(args.size ?? 500) },
|
|
131
132
|
})).data;
|
|
132
133
|
const rows = Array.isArray(data?.content) ? data.content : wireRows(data);
|
|
134
|
+
// The wire row carries `deptId: null` and `departmentId` absent; the
|
|
135
|
+
// member's department rides `parentDepartmentName[0].deptId` (and
|
|
136
|
+
// `ouScope[0].id`). Projecting only `departmentId` made every member look
|
|
137
|
+
// department-less (r15 misdiagnosis; P4).
|
|
138
|
+
const memberDepartment = (row) => {
|
|
139
|
+
const direct = row.departmentId ?? row.deptId;
|
|
140
|
+
if (typeof direct === "string" && direct.length > 0)
|
|
141
|
+
return direct;
|
|
142
|
+
const parents = Array.isArray(row.parentDepartmentName) ? row.parentDepartmentName : [];
|
|
143
|
+
for (const parent of parents)
|
|
144
|
+
if (typeof parent.deptId === "string" && parent.deptId)
|
|
145
|
+
return parent.deptId;
|
|
146
|
+
const scopes = Array.isArray(row.ouScope) ? row.ouScope : [];
|
|
147
|
+
for (const scope of scopes)
|
|
148
|
+
if (typeof scope.id === "string" && scope.id)
|
|
149
|
+
return scope.id;
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
133
152
|
const members = rows.map((row) => ({
|
|
134
153
|
id: roleUserId(row),
|
|
135
154
|
name: row.name ?? null,
|
|
136
155
|
username: row.username ?? null,
|
|
137
|
-
departmentId: row
|
|
156
|
+
departmentId: memberDepartment(row),
|
|
157
|
+
departmentName: row.departmentName ?? null,
|
|
158
|
+
// `ouScope` decides whether a role member can be resolved as a workflow
|
|
159
|
+
// participant. Surface the count so an empty scope is visible here.
|
|
160
|
+
scopeCount: Array.isArray(row.ouScope) ? row.ouScope.length : null,
|
|
138
161
|
unitType: row.unitType ?? null,
|
|
139
162
|
}));
|
|
140
163
|
return { roleId: String(args.roleId), count: members.length, total: data?.totalElements ?? members.length, members };
|
|
@@ -301,10 +324,67 @@ export function registerOrganizationLane(tools) {
|
|
|
301
324
|
const after = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentGet, query: { ...orgQuery(session), deptId: args.deptId } })).data;
|
|
302
325
|
return { performed: { errcode: 0 }, readback: { deptId: args.deptId, name: after?.name ?? null, parentId: after?.parentId ?? null, managerId: after?.managerId ?? null, enabled: after?.enabled ?? null } };
|
|
303
326
|
});
|
|
304
|
-
writeTool(tools, "org.department.delete", LANE, SCOPES.deptDelete, "删除部门(`GET organization/department/strike_out?id=`)。平台会拒绝:有子部门 → 200009「部门下面存在子部门,不能删除!」;有用户 → 200002「部门下面存在用户,不能删除!」。工具先计数并在回读里给出,仍由平台裁决。", { deptId: z.string().min(1) }, async (session, args) => {
|
|
327
|
+
writeTool(tools, "org.department.delete", LANE, SCOPES.deptDelete, "删除部门(`GET organization/department/strike_out?id=`)。平台会拒绝:有子部门 → 200009「部门下面存在子部门,不能删除!」;有用户 → 200002「部门下面存在用户,不能删除!」。工具先计数并在回读里给出,仍由平台裁决。", { deptId: z.string().min(1), force: z.boolean().optional().describe("true = 先逐个删除子部门(仅当子部门无用户)再删本部门;有用户仍由平台裁决") }, async (session, args) => {
|
|
305
328
|
const users = wireRows((await wireCall(session, { path: ENDPOINTS.organizationDepartmentUsers, query: { ...orgQuery(session), deptId: args.deptId } })).data);
|
|
329
|
+
// Pre-check children: the platform's 200009 ("部门下面存在子部门") is
|
|
330
|
+
// accurate but sends the caller hunting for ids. Name them and hand over
|
|
331
|
+
// the child-delete command (found during r15 org cleanup).
|
|
332
|
+
const walked = await collectOrgDepartments(session);
|
|
333
|
+
const children = walked.rows
|
|
334
|
+
.filter((row) => row.parentId === String(args.deptId))
|
|
335
|
+
.map((row) => ({ id: row.id, name: row.name }))
|
|
336
|
+
.filter((row) => row.id.length > 0);
|
|
337
|
+
let deletedChildren = 0;
|
|
338
|
+
if (children.length > 0) {
|
|
339
|
+
if (args.force !== true)
|
|
340
|
+
throw new ToolRefusalError({
|
|
341
|
+
status: "refused", blocker: "department-has-children", deptId: String(args.deptId), children,
|
|
342
|
+
nextCommand: `omc call org.department.delete --args '${JSON.stringify({ deptId: children[0].id })}'`,
|
|
343
|
+
message: `部门 ${args.deptId} 下还有 ${children.length} 个子部门,平台拒绝删除(200009)。先删子部门,或传 force:true 让工具按顺序删除:${children.map((child) => `${child.name}(${child.id})`).join("、")}。`,
|
|
344
|
+
});
|
|
345
|
+
// force = deepest-first over the WHOLE subtree (a direct child with its
|
|
346
|
+
// own children would fail 200009 otherwise).
|
|
347
|
+
const byParent = new Map();
|
|
348
|
+
for (const row of walked.rows) {
|
|
349
|
+
if (row.id === String(args.deptId))
|
|
350
|
+
continue;
|
|
351
|
+
const list = byParent.get(row.parentId) ?? [];
|
|
352
|
+
list.push({ id: row.id, name: row.name });
|
|
353
|
+
byParent.set(row.parentId, list);
|
|
354
|
+
}
|
|
355
|
+
const ordered = [];
|
|
356
|
+
const walk = (parentId) => {
|
|
357
|
+
for (const child of byParent.get(parentId) ?? []) {
|
|
358
|
+
walk(child.id);
|
|
359
|
+
ordered.push(child);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
walk(String(args.deptId));
|
|
363
|
+
deletedChildren = ordered.length;
|
|
364
|
+
for (const child of ordered) {
|
|
365
|
+
try {
|
|
366
|
+
await wireCall(session, { path: ENDPOINTS.organizationDepartmentDelete, query: { ...orgQuery(session), id: child.id } });
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
370
|
+
const usersPending = /200002|用户/.test(message);
|
|
371
|
+
throw new ToolRefusalError({
|
|
372
|
+
status: "refused", blocker: usersPending ? "department-has-users" : "department-delete-failed",
|
|
373
|
+
deptId: String(args.deptId), blockedChild: child,
|
|
374
|
+
nextCommand: `omc call org.department.users --args '${JSON.stringify({ deptId: child.id })}'`,
|
|
375
|
+
message: `force 清理到子部门 ${child.name}(${child.id}) 时失败:${message}。${usersPending ? "该部门下还有员工——先转出再删。" : "平台拒绝删除该子部门。"}`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (users.length > 0)
|
|
381
|
+
throw new ToolRefusalError({
|
|
382
|
+
status: "refused", blocker: "department-has-users", deptId: String(args.deptId), users: users.length,
|
|
383
|
+
nextCommand: `omc call org.department.users --args '${JSON.stringify({ deptId: String(args.deptId) })}'`,
|
|
384
|
+
message: `部门 ${args.deptId} 下还有 ${users.length} 名员工,平台拒绝删除(200002)。先用 org.department.users 看人,再用 org.user.transferDepartment 转出(平台不允许删带人的部门)。`,
|
|
385
|
+
});
|
|
306
386
|
await wireCall(session, { path: ENDPOINTS.organizationDepartmentDelete, query: { ...orgQuery(session), id: args.deptId } });
|
|
307
|
-
return { performed: { errcode: 0 }, readback: { deptId: args.deptId, deleted: true, usersBeforeDelete: users.length } };
|
|
387
|
+
return { performed: { errcode: 0 }, readback: { deptId: args.deptId, deleted: true, usersBeforeDelete: users.length, deletedChildren } };
|
|
308
388
|
});
|
|
309
389
|
tools.push(defineTool({
|
|
310
390
|
name: "org.department.users",
|
|
@@ -388,7 +468,26 @@ export function registerOrganizationLane(tools) {
|
|
|
388
468
|
const departments = wireRows((await wireCall(session, {
|
|
389
469
|
path: ENDPOINTS.organizationUserDepartments, query: { ...orgQuery(session), id: String(args.userId) },
|
|
390
470
|
})).data).map((row) => ({ deptId: row.deptId ?? null, deptName: row.deptName ?? null, isMain: row.isMain ?? null }));
|
|
391
|
-
return {
|
|
471
|
+
return {
|
|
472
|
+
performed: { errcode: 0 },
|
|
473
|
+
readback: {
|
|
474
|
+
userId: args.userId,
|
|
475
|
+
name: after?.name ?? null,
|
|
476
|
+
departmentId: after?.departmentId ?? null,
|
|
477
|
+
mobile: after?.mobile ?? null,
|
|
478
|
+
email: after?.email ?? null,
|
|
479
|
+
active: after?.active ?? null,
|
|
480
|
+
userStatus: after?.status ?? null,
|
|
481
|
+
departments,
|
|
482
|
+
// r15 P7: `active` did not persist on 1.1.25 (passed true, read false).
|
|
483
|
+
// It looks derived from account activation/binding rather than a
|
|
484
|
+
// writable switch — say so instead of letting the caller re-create the
|
|
485
|
+
// user to "fix" it.
|
|
486
|
+
...(args.active !== undefined && after?.active !== args.active
|
|
487
|
+
? { activeWarning: `传了 active=${String(args.active)},平台读回 ${String(after?.active)}:active 在 1.1.25 上不是可直接写的开关(疑似由账号激活/绑定派生)。启停账号用 status=ENABLE/DISABLE/LOCK;不要为改 active 新建用户。` }
|
|
488
|
+
: {}),
|
|
489
|
+
},
|
|
490
|
+
};
|
|
392
491
|
});
|
|
393
492
|
writeTool(tools, "org.user.delete", LANE, SCOPES.userDelete, "删除员工(`GET organization/user/strike_out?id=`)。只传用户 id;返回 deleted。", { userId: z.string().min(1) }, async (session, args) => {
|
|
394
493
|
await wireCall(session, { path: ENDPOINTS.organizationUserDelete, query: { ...orgQuery(session), id: args.userId } });
|
|
@@ -444,6 +543,7 @@ export function registerOrganizationLane(tools) {
|
|
|
444
543
|
if (rows.length > 0 && args.force !== true)
|
|
445
544
|
throw new ToolRefusalError({
|
|
446
545
|
status: "refused", blocker: "role-has-members", roleId: String(args.roleId), members: rows.length,
|
|
546
|
+
nextCommand: `omc call org.role.delete --args '${JSON.stringify({ roleId: String(args.roleId), force: true })}'`,
|
|
447
547
|
message: `角色下还有 ${rows.length} 名成员,平台拒绝删除(errmsg:角色下已经包含用户数据)。先 org.role.removeUsers,或传 force:true 让工具移出后再删。`,
|
|
448
548
|
});
|
|
449
549
|
if (rows.length > 0)
|
|
@@ -459,6 +559,7 @@ export function registerOrganizationLane(tools) {
|
|
|
459
559
|
if (roles.length > 0 && args.force !== true)
|
|
460
560
|
throw new ToolRefusalError({
|
|
461
561
|
status: "refused", blocker: "role-group-has-roles", roleGroupId: String(args.roleGroupId), roles: roles.length,
|
|
562
|
+
nextCommand: `omc call org.roleGroup.delete --args '${JSON.stringify({ roleGroupId: String(args.roleGroupId), force: true })}'`,
|
|
462
563
|
message: `角色组下还有 ${roles.length} 个角色,平台拒绝删除(errmsg:角色组下已经包含角色数据)。先删角色,或传 force:true。`,
|
|
463
564
|
});
|
|
464
565
|
for (const role of roles) {
|
|
@@ -624,7 +725,10 @@ export function registerOrganizationLane(tools) {
|
|
|
624
725
|
id: z.string().min(1).describe("部门 id"),
|
|
625
726
|
name: z.string().min(1),
|
|
626
727
|
deptPath: z.string().optional().describe("完整路径,如 根组织/广州总所"),
|
|
627
|
-
|
|
728
|
+
// The wire wants the enum as a STRING ("1"); a number used to die in
|
|
729
|
+
// input validation with a generic tool-input-invalid (r10). Accept both
|
|
730
|
+
// and normalize, so the mistake cannot cost a round.
|
|
731
|
+
type: z.union([z.string(), z.number()]).optional().describe("部门节点固定 \"1\"(字符串;也接受数字 1,工具会归一化)"),
|
|
628
732
|
})).describe("要设置的管理范围节点"),
|
|
629
733
|
mode: z.enum(["merge", "replace"]).optional().describe("默认 merge(读旧值合并);replace = 覆盖整个范围"),
|
|
630
734
|
dryRun: z.boolean().optional(),
|
|
@@ -633,7 +737,7 @@ export function registerOrganizationLane(tools) {
|
|
|
633
737
|
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
634
738
|
query: { ...orgQuery(session), roleId: args.roleId, userId: args.userId },
|
|
635
739
|
})).data);
|
|
636
|
-
const wanted = args.scopes.map((scope) => ({ id: scope.id, name: scope.name, type: scope.type
|
|
740
|
+
const wanted = args.scopes.map((scope) => ({ id: scope.id, name: scope.name, type: scope.type === undefined ? "1" : String(scope.type), ...(scope.deptPath ? { deptPath: scope.deptPath } : {}) }));
|
|
637
741
|
const merged = args.mode === "replace"
|
|
638
742
|
? wanted
|
|
639
743
|
: [...before.map((row) => ({ id: String(row.id ?? ""), name: String(row.name ?? ""), type: String(row.type ?? "1"), ...(row.deptPath ? { deptPath: String(row.deptPath) } : {}) })).filter((row) => row.id !== ""), ...wanted]
|
|
@@ -204,11 +204,32 @@ export function registerRuleLifecycleLane(tools) {
|
|
|
204
204
|
ruleCode: args.ruleCode,
|
|
205
205
|
message: `规则 ${args.ruleCode} 当前未启用(enabled=${String(row.enabled)})——先 rule.enable(publish+enable 两步),否则设计器显示「失效·未启用」且按钮不会执行它`,
|
|
206
206
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
let performed;
|
|
208
|
+
try {
|
|
209
|
+
performed = await wireCall(session, {
|
|
210
|
+
path: ruleRuntimeExecutePath(args.schemaCode, args.ruleCode),
|
|
211
|
+
method: "POST",
|
|
212
|
+
body: { schemaCode: args.schemaCode, ids, ruleCode: args.ruleCode, consolidation: args.consolidation === true },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// 550001 业务规则不存在 on the launcher runtime path means the rule was
|
|
217
|
+
// not REGISTERED as a custom trigger (r15: a data-event graph saved with
|
|
218
|
+
// customTrigger:true still 550001s). Name the semantic instead of
|
|
219
|
+
// forwarding a bare platform code.
|
|
220
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 550001)
|
|
221
|
+
throw new ToolRefusalError({
|
|
222
|
+
status: "refused",
|
|
223
|
+
blocker: "rule-not-custom-trigger",
|
|
224
|
+
schemaCode: args.schemaCode,
|
|
225
|
+
ruleCode: args.ruleCode,
|
|
226
|
+
nextCommand: `omc call rule.get --args '${JSON.stringify({ schemaCode: args.schemaCode, ruleCode: args.ruleCode })}'`,
|
|
227
|
+
message: `平台 launcher 运行时没有注册规则 ${args.ruleCode}(550001 业务规则不存在)。\`rule.run\` 走的是「按钮/自定义触发」端点,` +
|
|
228
|
+
`只认 CUSTOM_NODE 触发的规则;数据事件(Create/Update/…)的规则不会注册在这里。` +
|
|
229
|
+
`要看规则图里有没有 CUSTOM_NODE,用 rule.get 看 nodes;要验证数据事件规则请走 data.submit/data.update,要验证按钮请绑到列表/表单按钮后在门户点击。`,
|
|
230
|
+
});
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
212
233
|
const payload = (performed.data ?? null);
|
|
213
234
|
const objects = Array.isArray(payload) ? payload : [];
|
|
214
235
|
return {
|
|
@@ -216,6 +216,47 @@ function projectReadbackRecord(record, args) {
|
|
|
216
216
|
return record;
|
|
217
217
|
return compactRuntimeRecord(source);
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Approval-submit readback (issue 2026-10-25 §2).
|
|
221
|
+
*
|
|
222
|
+
* An approval submit changed almost nothing: the caller needs "did the flow
|
|
223
|
+
* advance" (sequenceStatus) plus "did the fields I typed persist". Returning the
|
|
224
|
+
* whole compact record made every approval level pay for the full row again
|
|
225
|
+
* (r12: 25 data.submit calls / 55.6 KB — the single largest read face).
|
|
226
|
+
* `summary`/`fields`/`full` still win when supplied; this is only the default.
|
|
227
|
+
*/
|
|
228
|
+
/**
|
|
229
|
+
* Which submitted values did the immediate readback NOT show? (r10 F8)
|
|
230
|
+
*
|
|
231
|
+
* The platform's form/load face can answer with only system columns right after
|
|
232
|
+
* a write transaction (r14: `readback.authoritative` held just `modifiedTime`
|
|
233
|
+
* while `data.load` a moment later had every business field). Without a note the
|
|
234
|
+
* caller reads that as "the write dropped my data" and writes again.
|
|
235
|
+
*/
|
|
236
|
+
function readbackEmptyFields(formData, record) {
|
|
237
|
+
const source = record !== null && typeof record === "object" && !Array.isArray(record) ? record : {};
|
|
238
|
+
return Object.entries(formData)
|
|
239
|
+
.filter(([key, value]) => value !== null && value !== undefined && (source[key] === null || source[key] === undefined))
|
|
240
|
+
.map(([key]) => key);
|
|
241
|
+
}
|
|
242
|
+
function readbackWarningFor(emptyFields, toolName) {
|
|
243
|
+
if (emptyFields.length === 0)
|
|
244
|
+
return undefined;
|
|
245
|
+
return `${toolName} 已 DELIVERED,但即时读回里这些本次提交的字段为空/缺省:${emptyFields.join("、")}。平台 form/load 在写事务后可能先回系统字段、业务字段滞后(真机 1.1.25,r14 见过只回 modifiedTime 的一笔)。**先 omc call data.load 核对,不要重写**;若 data.load 仍为空,才按投影/字段 code 排查。`;
|
|
246
|
+
}
|
|
247
|
+
function projectApprovalReadback(record, submittedKeys) {
|
|
248
|
+
if (record === null || typeof record !== "object" || Array.isArray(record))
|
|
249
|
+
return {};
|
|
250
|
+
const source = record;
|
|
251
|
+
const projected = {};
|
|
252
|
+
for (const key of ["id", "bizObjectId", "schemaCode", "sequenceStatus"])
|
|
253
|
+
if (key in source)
|
|
254
|
+
projected[key] = source[key];
|
|
255
|
+
for (const key of submittedKeys)
|
|
256
|
+
if (key in source && !(key in projected))
|
|
257
|
+
projected[key] = compactRuntimeValue(source[key]);
|
|
258
|
+
return projected;
|
|
259
|
+
}
|
|
219
260
|
/** Shared optional schema fields for the write readback projection. */
|
|
220
261
|
export const readbackProjectionArgs = {
|
|
221
262
|
summary: z.boolean().optional().describe("只回身份/状态字段,裁剪 readback.authoritative(默认关闭,读回为业务字段+身份的精简记录)"),
|
|
@@ -750,8 +791,11 @@ export function registerRuntimeDataLane(tools) {
|
|
|
750
791
|
// branch) — business rules only run once data.submit advances the
|
|
751
792
|
// row. Saying so on every plain-save readback prevents the
|
|
752
793
|
// "rule didn't fire" misdiagnosis.
|
|
794
|
+
const savedRecord = normalizeLoadedRecord((readbackData ?? {}));
|
|
795
|
+
const savedEmptyFields = readbackEmptyFields(formData, savedRecord);
|
|
753
796
|
const plainReadback = {
|
|
754
|
-
authoritative: projectReadbackRecord(
|
|
797
|
+
authoritative: projectReadbackRecord(savedRecord, args),
|
|
798
|
+
...(savedEmptyFields.length > 0 ? { readbackWarning: readbackWarningFor(savedEmptyFields, "data.save") } : {}),
|
|
755
799
|
draftRuleHint: "草稿态(DRAFT)保存不触发业务规则——DRAFT 行跳过 Create/Update 分发(DomainServiceImpl.java:240-248);要一调完成态本调用加 submit:true;已存草稿直提形状:data.submit {schemaCode, bizObjectId: 本读回的 bizObjectId, formData}(submit 自持久化)",
|
|
756
800
|
};
|
|
757
801
|
// r18 retro (G3): one-call COMPLETED creation — the agent goal is usually
|
|
@@ -824,6 +868,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
824
868
|
schemaCode: args.schemaCode,
|
|
825
869
|
bizObjectId: args.bizObjectId,
|
|
826
870
|
sheetCode,
|
|
871
|
+
nextCommand: `omc call data.list --args '${JSON.stringify({ schemaCode: args.schemaCode })}'`,
|
|
827
872
|
message: "no live business object with this id; refusing to update (the platform would otherwise insert a new row)",
|
|
828
873
|
});
|
|
829
874
|
}
|
|
@@ -840,12 +885,14 @@ export function registerRuntimeDataLane(tools) {
|
|
|
840
885
|
const returnedId = typeof performedData.objectId === "string" ? performedData.objectId : typeof performedData.bizObjectId === "string" ? performedData.bizObjectId : args.bizObjectId;
|
|
841
886
|
const readbackData = await loadExistingBizObject(session, args.schemaCode, sheetCode, returnedId);
|
|
842
887
|
const sequenceStatus = extractSequenceStatus(readbackData);
|
|
888
|
+
const updatedRecord = normalizeLoadedRecord(readbackData);
|
|
889
|
+
const updatedEmptyFields = readbackEmptyFields(formData, updatedRecord);
|
|
843
890
|
return {
|
|
844
891
|
performed: { errcode: performed.errcode, bizObjectId: returnedId, sheetCode, resolvedFrom },
|
|
845
892
|
// Flat record contract (ticket 17): live FormLoadVO normalizes to the
|
|
846
893
|
// fake's flat field shape so `authoritative.<fieldCode>` reads work on
|
|
847
894
|
// both faces.
|
|
848
|
-
readback: { authoritative: projectReadbackRecord(
|
|
895
|
+
readback: { authoritative: projectReadbackRecord(updatedRecord, args), sequenceStatus, ...(updatedEmptyFields.length > 0 ? { readbackWarning: readbackWarningFor(updatedEmptyFields, "data.update") } : {}) },
|
|
849
896
|
};
|
|
850
897
|
});
|
|
851
898
|
/**
|
|
@@ -1006,6 +1053,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1006
1053
|
schemaCode: args.schemaCode,
|
|
1007
1054
|
bizObjectId: targetObjectId,
|
|
1008
1055
|
workflowCode: explicitWorkflowCode,
|
|
1056
|
+
nextCommand: "omc describe data.submit --example workflow-backed-data-submit",
|
|
1009
1057
|
message: "8.6.26 has no REST in-place workflow start for an existing no-instance row (submitWorkflowByWorkflowCode -> createAndGetBizObject INSERTs -> 40000 duplicate primary key; the load+update starter is facade-only). Supported shape: create workflow drafts via data.save with workflowCode (returns same-id row + DRAFT instance + start workItemId), then data.submit with that workItemId + workflowInstanceId + bizObjectId",
|
|
1010
1058
|
});
|
|
1011
1059
|
}
|
|
@@ -1021,6 +1069,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1021
1069
|
schemaCode: args.schemaCode,
|
|
1022
1070
|
bizObjectId: targetObjectId,
|
|
1023
1071
|
sheetCode,
|
|
1072
|
+
nextCommand: `omc call data.list --args '${JSON.stringify({ schemaCode: args.schemaCode })}'`,
|
|
1024
1073
|
message: "no live business object with this id; refusing to submit (the platform would otherwise create a new row)",
|
|
1025
1074
|
});
|
|
1026
1075
|
}
|
|
@@ -1163,13 +1212,20 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1163
1212
|
// start path returns only {result, objectId}); the authoritative loaded
|
|
1164
1213
|
// row carries it. Prefer the response, then the row.
|
|
1165
1214
|
const effectiveInstanceId = workflowInstanceId ?? extractWorkflowInstanceId(loaded) ?? (loadedRecord && typeof loadedRecord.workflowInstanceId === "string" && loadedRecord.workflowInstanceId ? loadedRecord.workflowInstanceId : null);
|
|
1215
|
+
// An approval submit returns the status + the fields the caller submitted,
|
|
1216
|
+
// not the whole row: r12's 25 approvals were the largest read face at
|
|
1217
|
+
// 55.6 KB. Explicit summary/fields/full keep their contract.
|
|
1218
|
+
const approvalDelta = hasWorkItem && args.summary !== true && args.full !== true && !(Array.isArray(args.fields) && args.fields.length > 0);
|
|
1166
1219
|
readback = {
|
|
1167
1220
|
...readback,
|
|
1168
1221
|
workflowInstanceId: effectiveInstanceId,
|
|
1169
1222
|
objectId,
|
|
1170
1223
|
workItemId: typeof performedData.workItemId === "string" ? performedData.workItemId : null,
|
|
1171
1224
|
sequenceStatus,
|
|
1172
|
-
submittedData: projectReadbackRecord(loadedRecord, args),
|
|
1225
|
+
submittedData: approvalDelta ? projectApprovalReadback(loadedRecord, Object.keys(formData ?? {})) : projectReadbackRecord(loadedRecord, args),
|
|
1226
|
+
...(approvalDelta
|
|
1227
|
+
? { readbackShape: "approval-delta", readbackHint: "审批提交默认只回 sequenceStatus + 本次提交的字段;要整条记录(含子表/系统字段)加 full:true,要指定字段加 fields:[...]" }
|
|
1228
|
+
: {}),
|
|
1173
1229
|
};
|
|
1174
1230
|
// Multi-level approval: after a submit advances the flow, the caller
|
|
1175
1231
|
// almost always needs the NEXT pending todo — bench52/53 paid an extra
|