@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
|
@@ -128,22 +128,58 @@ async function readRoleTree(session) {
|
|
|
128
128
|
* that `org.role.list` actually shows) to the platform role id. A reference that
|
|
129
129
|
* matches nothing is refused with the list of real roles — never written blind.
|
|
130
130
|
*/
|
|
131
|
+
/**
|
|
132
|
+
* r28h F11: `String(undefined)` → `"undefined"` must never leave a readback.
|
|
133
|
+
* Fail closed when a resolved id is empty / literally the word "undefined".
|
|
134
|
+
*/
|
|
135
|
+
function assertResolvedId(id, tool, label) {
|
|
136
|
+
if (!id || id === "undefined" || id === "null") {
|
|
137
|
+
throw new ToolRefusalError({
|
|
138
|
+
status: "refused",
|
|
139
|
+
blocker: "wire-shape",
|
|
140
|
+
message: `${tool} 未能解析出真实 ${label}(得到 ${JSON.stringify(id)});不要把 undefined/null 序列化进回执`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return id;
|
|
144
|
+
}
|
|
131
145
|
async function resolveRoleRef(session, args, tool) {
|
|
146
|
+
// r28h F10: both given → resolve code and refuse when it disagrees with roleId.
|
|
147
|
+
if (args.roleId !== undefined && args.roleCode !== undefined) {
|
|
148
|
+
const { roles } = await readRoleTree(session);
|
|
149
|
+
const match = roles.find((role) => role.code === args.roleCode) ?? roles.find((role) => role.name === args.roleCode);
|
|
150
|
+
if (!match) {
|
|
151
|
+
throw new ToolInputError({
|
|
152
|
+
status: "error",
|
|
153
|
+
blocker: "tool-input-invalid",
|
|
154
|
+
tool,
|
|
155
|
+
message: `角色 ${args.roleCode} 不存在。可用角色:${roles.slice(0, 30).map((role) => `${role.code}(${role.name})`).join(";")}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (args.roleId !== match.id && args.roleId !== match.code && args.roleId !== match.name) {
|
|
159
|
+
throw new ToolInputError({
|
|
160
|
+
status: "error",
|
|
161
|
+
blocker: "tool-input-invalid",
|
|
162
|
+
tool,
|
|
163
|
+
message: `roleId=${args.roleId} 与 roleCode=${args.roleCode}(→${match.id})不一致;二者其一,都给且不一致则拒绝`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return assertResolvedId(match.id, tool, "roleId");
|
|
167
|
+
}
|
|
132
168
|
const requested = args.roleId ?? args.roleCode;
|
|
133
169
|
if (!requested)
|
|
134
170
|
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool, message: "需要 roleId 或 roleCode(org.role.list 的 roles 码表给的是 code)" });
|
|
135
171
|
// An explicit roleCode must resolve — never forward it as an id.
|
|
136
172
|
if (args.roleId !== undefined && args.roleCode === undefined && /^[0-9a-f]{32}$/i.test(requested))
|
|
137
|
-
return requested;
|
|
173
|
+
return assertResolvedId(requested, tool, "roleId");
|
|
138
174
|
const { roles } = await readRoleTree(session);
|
|
139
175
|
const match = roles.find((role) => role.code === requested) ?? roles.find((role) => role.name === requested);
|
|
140
176
|
if (match)
|
|
141
|
-
return match.id;
|
|
177
|
+
return assertResolvedId(match.id, tool, "roleId");
|
|
142
178
|
// Not a known code/name. `roleId` may still be a valid id the tree does not
|
|
143
179
|
// surface (e.g. a role in another corp); forward it and let the platform
|
|
144
180
|
// judge, rather than breaking callers that hold an id we cannot see.
|
|
145
181
|
if (args.roleId !== undefined)
|
|
146
|
-
return requested;
|
|
182
|
+
return assertResolvedId(requested, tool, "roleId");
|
|
147
183
|
throw new ToolInputError({
|
|
148
184
|
status: "error",
|
|
149
185
|
blocker: "tool-input-invalid",
|
|
@@ -152,6 +188,97 @@ async function resolveRoleRef(session, args, tool) {
|
|
|
152
188
|
});
|
|
153
189
|
}
|
|
154
190
|
const roleUserId = (row) => String(row.id ?? row.userId ?? row.recId ?? "");
|
|
191
|
+
/**
|
|
192
|
+
* Walk `/role/list` and locate a role-group by id, returning the real
|
|
193
|
+
* code/path/parentId the create face itself often leaves null (r28h F9).
|
|
194
|
+
*/
|
|
195
|
+
function locateRoleGroup(rows, roleGroupId) {
|
|
196
|
+
const childKeys = ["children", "roles", "childRoles", "childList", "list"];
|
|
197
|
+
const childrenOf = (row) => {
|
|
198
|
+
for (const key of childKeys) {
|
|
199
|
+
const value = row[key];
|
|
200
|
+
if (Array.isArray(value))
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
return [];
|
|
204
|
+
};
|
|
205
|
+
let found = null;
|
|
206
|
+
const walk = (nodes, parents) => {
|
|
207
|
+
for (const node of nodes) {
|
|
208
|
+
const kids = childrenOf(node);
|
|
209
|
+
const hasGroupId = node.groupId !== undefined && node.groupId !== null;
|
|
210
|
+
const isGroup = node.group === true || (!hasGroupId && kids.length > 0);
|
|
211
|
+
if (!isGroup)
|
|
212
|
+
continue;
|
|
213
|
+
const id = String(node.id ?? "");
|
|
214
|
+
const name = String(node.name ?? "");
|
|
215
|
+
const path = [...parents.map((parent) => parent.name), name].filter((part) => part.length > 0).join(" / ");
|
|
216
|
+
if (id === roleGroupId) {
|
|
217
|
+
const parent = parents.length > 0 ? parents[parents.length - 1] : null;
|
|
218
|
+
const rawParent = node.parentId == null || node.parentId === "" ? null : String(node.parentId);
|
|
219
|
+
found = {
|
|
220
|
+
id,
|
|
221
|
+
code: node.code == null || node.code === "" ? null : String(node.code),
|
|
222
|
+
name: name || null,
|
|
223
|
+
parentId: parent?.id ?? rawParent,
|
|
224
|
+
parentName: parent?.name ?? null,
|
|
225
|
+
path,
|
|
226
|
+
};
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (kids.length > 0)
|
|
230
|
+
walk(kids, [...parents, { id, name }]);
|
|
231
|
+
if (found)
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
walk(rows, []);
|
|
236
|
+
return found;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Locate a role by id after create — platform often leaves code:null on the
|
|
240
|
+
* add_role response and fills SYS… on the subsequent list (r28h F9).
|
|
241
|
+
*/
|
|
242
|
+
function locateRole(rows, roleId) {
|
|
243
|
+
const childKeys = ["children", "roles", "childRoles", "childList", "list"];
|
|
244
|
+
const childrenOf = (row) => {
|
|
245
|
+
for (const key of childKeys) {
|
|
246
|
+
const value = row[key];
|
|
247
|
+
if (Array.isArray(value))
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
return [];
|
|
251
|
+
};
|
|
252
|
+
let found = null;
|
|
253
|
+
const walk = (nodes, group) => {
|
|
254
|
+
for (const node of nodes) {
|
|
255
|
+
const kids = childrenOf(node);
|
|
256
|
+
const hasGroupId = node.groupId !== undefined && node.groupId !== null;
|
|
257
|
+
const isGroup = node.group === true || (!hasGroupId && kids.length > 0);
|
|
258
|
+
if (isGroup) {
|
|
259
|
+
const next = { id: String(node.id ?? ""), code: node.code == null ? null : String(node.code), name: node.name == null ? null : String(node.name) };
|
|
260
|
+
if (kids.length > 0)
|
|
261
|
+
walk(kids, next);
|
|
262
|
+
if (found)
|
|
263
|
+
return;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (String(node.id ?? "") === roleId) {
|
|
267
|
+
found = {
|
|
268
|
+
id: roleId,
|
|
269
|
+
code: node.code == null || node.code === "" ? null : String(node.code),
|
|
270
|
+
name: node.name == null ? null : String(node.name),
|
|
271
|
+
groupId: group?.id ?? (node.groupId == null ? null : String(node.groupId)),
|
|
272
|
+
groupCode: group?.code ?? null,
|
|
273
|
+
groupName: group?.name ?? null,
|
|
274
|
+
};
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
walk(rows, null);
|
|
280
|
+
return found;
|
|
281
|
+
}
|
|
155
282
|
async function memberRows(session, roleId) {
|
|
156
283
|
const data = (await wireCall(session, {
|
|
157
284
|
path: ENDPOINTS.organizationRoleUsers,
|
|
@@ -194,18 +321,20 @@ export function registerOrganizationLane(tools) {
|
|
|
194
321
|
kind: "read",
|
|
195
322
|
lane: LANE,
|
|
196
323
|
scope: SCOPES.roleUsers,
|
|
197
|
-
description: "列出角色成员(`GET organization/role/users`)。返回 members[{id(recId),name,...}];**id 是 recId,不是账号**(addUsers/setUserScope
|
|
324
|
+
description: "列出角色成员(`GET organization/role/users`)。返回 members[{id(recId),name,...}];**id 是 recId,不是账号**(addUsers/setUserScope 都传它)。**roleId 与 roleCode 二者其一**(与写面对齐;都给且不一致则拒绝)。",
|
|
198
325
|
inputSchema: {
|
|
199
326
|
environment: environmentArg,
|
|
200
|
-
roleId: z.string().min(1).describe("角色 id
|
|
327
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
328
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称;与 roleId 二选一"),
|
|
201
329
|
page: z.number().int().min(0).optional(),
|
|
202
330
|
size: z.number().int().min(1).max(500).optional(),
|
|
203
331
|
capability: capabilityArg,
|
|
204
332
|
},
|
|
205
333
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
334
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.users");
|
|
206
335
|
const data = (await wireCall(session, {
|
|
207
336
|
path: ENDPOINTS.organizationRoleUsers,
|
|
208
|
-
query: { ...orgQuery(session), roleId
|
|
337
|
+
query: { ...orgQuery(session), roleId, page: String(args.page ?? 0), size: String(args.size ?? 500) },
|
|
209
338
|
})).data;
|
|
210
339
|
const rows = Array.isArray(data?.content) ? data.content : wireRows(data);
|
|
211
340
|
// The wire row carries `deptId: null` and `departmentId` absent; the
|
|
@@ -241,7 +370,7 @@ export function registerOrganizationLane(tools) {
|
|
|
241
370
|
unitType: row.unitType ?? null,
|
|
242
371
|
}));
|
|
243
372
|
return {
|
|
244
|
-
roleId
|
|
373
|
+
roleId,
|
|
245
374
|
count: members.length,
|
|
246
375
|
total: data?.totalElements ?? members.length,
|
|
247
376
|
members,
|
|
@@ -255,333 +384,387 @@ export function registerOrganizationLane(tools) {
|
|
|
255
384
|
kind: "read",
|
|
256
385
|
lane: LANE,
|
|
257
386
|
scope: SCOPES.userScope,
|
|
258
|
-
description: "读成员的管理范围(`GET organization/role/get_ouscope
|
|
387
|
+
description: "读成员的管理范围(`GET organization/role/get_ouscope`)。**不是枚举档位**(本人/本部门/全部这类不存在):scopes 是一组部门节点 `{id,name,type:\"1\",deptPath}` 的自由列表。`type:\"1\"`=部门节点;update_ouscope 是覆盖语义,改前先读。**roleId 与 roleCode 二者其一**(与写面对齐;都给且不一致则拒绝)。",
|
|
259
388
|
inputSchema: {
|
|
260
389
|
environment: environmentArg,
|
|
261
|
-
roleId: z.string().min(1),
|
|
390
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
391
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称;与 roleId 二选一"),
|
|
262
392
|
userId: z.string().min(1).describe("成员的用户记录 id(recId)"),
|
|
263
393
|
capability: capabilityArg,
|
|
264
394
|
},
|
|
265
395
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
396
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.userScope");
|
|
266
397
|
const data = (await wireCall(session, {
|
|
267
398
|
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
268
|
-
query: { ...orgQuery(session), roleId
|
|
399
|
+
query: { ...orgQuery(session), roleId, userId: String(args.userId) },
|
|
269
400
|
})).data;
|
|
270
401
|
const scopes = wireRows(data).map((row) => ({ id: row.id ?? null, name: row.name ?? null, type: row.type ?? null, deptPath: row.deptPath ?? null }));
|
|
271
|
-
return { roleId
|
|
402
|
+
return { roleId, userId: String(args.userId), count: scopes.length, scopes };
|
|
272
403
|
}),
|
|
273
404
|
}));
|
|
274
|
-
writeTool(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
405
|
+
writeTool({
|
|
406
|
+
tools,
|
|
407
|
+
name: "org.roleGroup.create",
|
|
408
|
+
lane: LANE,
|
|
409
|
+
scope: SCOPES.groupCreate,
|
|
410
|
+
description: "创建角色组(`POST organization/role/add_role_group`)。返回 roleGroupId,角色挂在它下面。**corpId 默认写当前企业**(`session.enterpriseId()`):平台的 `corpId` 只从 body 取,body 缺省时才按 parentId 反查企业设置,查不到就 NPE(被吞成 errcode 0);`corpId=\"other\"` 是平台合法的「非组织分组」语义,只有明确要建非组织分组时才传。**parentId 必填**:空值会 50000;根级组请传 `\"0\"`(真机 1.1.25 实测可建根级,path=组名、不挂在既有组下);挂到既有组下则传该组 id(`org.role.list` 的 groups[].id)。创建回执会再读一次 role/list,补上真实 `code`/`parentId`/`path`(平台 add 瞬间 code 常为 null)。",
|
|
411
|
+
inputSchema: {
|
|
412
|
+
roleGroupName: z.string().min(1).describe("角色组名称(界面显示的分组名)"),
|
|
413
|
+
parentId: z.string().min(1).describe("上级角色组 id。**必填**:空值直接 50000 parentIds cannot be empty。根级组传 \"0\";挂到既有组下传该组 id(如「默认」组)。挂到既有组=给该组添子节点(会改既有组的层级语义)"),
|
|
414
|
+
code: z.string().optional(),
|
|
415
|
+
corpId: z.string().min(1).optional().describe("组织 corpId,默认当前登录企业;传 \"other\" 表示非组织分组(该组在组织管理里不归属任何企业)"),
|
|
416
|
+
},
|
|
417
|
+
execute: async (session, args) => {
|
|
418
|
+
const corpId = args.corpId ?? session.enterpriseId();
|
|
419
|
+
const data = (await wireCall(session, {
|
|
420
|
+
path: ENDPOINTS.organizationRoleAddGroup, method: "POST",
|
|
421
|
+
query: orgQuery(session),
|
|
422
|
+
body: { roleGroupName: args.roleGroupName, corpId, ...(args.parentId ? { parentId: args.parentId } : {}), ...(args.code ? { code: args.code } : {}) },
|
|
423
|
+
})).data;
|
|
424
|
+
const roleGroupId = assertResolvedId(String(data?.roleGroupId ?? data?.id ?? ""), "org.roleGroup.create", "roleGroupId");
|
|
425
|
+
const readbackCorpId = data?.corpId ?? null;
|
|
426
|
+
// The platform takes corpId from the body verbatim (RoleGroupHelper
|
|
427
|
+
// .createRoleGroupModel, 1.1.25): a silent rewrite means the value we sent
|
|
428
|
+
// did not land, and participants built from this group would not resolve
|
|
429
|
+
// in the org tree.
|
|
430
|
+
const corpIdHonoured = readbackCorpId === null || String(readbackCorpId) === corpId;
|
|
431
|
+
// r28h F9: create response leaves code:null; re-read the tree so the
|
|
432
|
+
// write face itself carries the real code/path/parentId.
|
|
433
|
+
const tree = (await wireCall(session, {
|
|
434
|
+
path: ENDPOINTS.organizationRoleList,
|
|
435
|
+
query: { corpId: session.enterpriseId(), expandAll: "true" },
|
|
436
|
+
})).data;
|
|
437
|
+
const located = locateRoleGroup(wireRows(tree), roleGroupId);
|
|
438
|
+
return {
|
|
439
|
+
performed: { errcode: 0 },
|
|
440
|
+
readback: {
|
|
441
|
+
roleGroupId,
|
|
442
|
+
roleGroupName: located?.name ?? data?.roleGroupName ?? args.roleGroupName,
|
|
443
|
+
code: located?.code ?? data?.code ?? null,
|
|
444
|
+
parentId: located?.parentId ?? (args.parentId === "0" ? "0" : args.parentId ?? null),
|
|
445
|
+
parentName: located?.parentName ?? null,
|
|
446
|
+
path: located?.path ?? null,
|
|
447
|
+
corpId: readbackCorpId,
|
|
448
|
+
...(corpIdHonoured ? {} : { warning: `平台回读 corpId=${String(readbackCorpId)},与请求的 ${corpId} 不一致;该组可能不归属当前企业,先 org.role.list 核对` }),
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
},
|
|
305
452
|
});
|
|
306
|
-
writeTool(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
groups
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
453
|
+
writeTool({
|
|
454
|
+
tools,
|
|
455
|
+
name: "org.role.create",
|
|
456
|
+
lane: LANE,
|
|
457
|
+
scope: SCOPES.roleCreate,
|
|
458
|
+
description: "创建角色(界面「分组」;`POST organization/role/add_role`)。返回 roleId;code 创建时 null、随后补 SYS<时间戳>,要稳定编码就传 code。**所属角色组推荐用 `roleGroupCode`(如 SYS_1000000)**:`org.role.list` 的紧凑码表只给 `code|name`、不显示组 id,而平台对错的 roleGroupId **静默接受**——存进去的值不是真实组 id 时角色会变成看不见的孤儿(r70 实测:在库里、role/users 查得到,role/list 永不返回、role/get 50000)。工具会把 code 归一成真实 id,并在创建后回读可见性。",
|
|
459
|
+
inputSchema: {
|
|
460
|
+
roleName: z.string().min(1),
|
|
461
|
+
roleGroupCode: z.string().min(1).optional().describe("所属角色组的 code(org.role.list 的 groups 码表,如 SYS_1000000)或名称。**推荐**;与 roleGroupId 二选一"),
|
|
462
|
+
roleGroupId: z.string().min(1).optional().describe("所属角色组 id(32 位 hex,只能从 artifact get <role.list 的 detailArtifactId> --jq groups 取到)。误传组 code 时工具会归一成真实 id"),
|
|
463
|
+
roleGroupName: z.string().optional(),
|
|
464
|
+
roleType: z.enum(["SYS", "CUSTOMIZE", "OTHER"]).optional().describe("默认 SYS(平台会把角色统一落成 SYS,见 RoleHelper.createRoleModel)"),
|
|
465
|
+
code: z.string().optional(),
|
|
466
|
+
remarks: z.string().optional(),
|
|
467
|
+
corpId: z.string().min(1).optional().describe("组织 corpId,默认当前登录企业;传 \"other\" 表示非组织分组角色"),
|
|
468
|
+
},
|
|
469
|
+
execute: async (session, args) => {
|
|
470
|
+
if (!args.roleGroupCode && !args.roleGroupId) {
|
|
471
|
+
throw new ToolInputError({ message: "必须给 roleGroupCode(推荐)或 roleGroupId;先 org.role.list 取 groups 码表" });
|
|
472
|
+
}
|
|
473
|
+
const { groups } = await readRoleTree(session);
|
|
474
|
+
if (groups.length === 0) {
|
|
475
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: "org.role.list 未解析出任何角色组;先确认组织树可读,不要盲建角色" });
|
|
476
|
+
}
|
|
477
|
+
const requested = args.roleGroupId ?? args.roleGroupCode;
|
|
478
|
+
const match = groups.find((group) => group.id === requested) ??
|
|
479
|
+
groups.find((group) => group.code === requested) ??
|
|
480
|
+
groups.find((group) => group.name === requested);
|
|
481
|
+
// A code/name that resolves to nothing is a definite typo → refuse.
|
|
482
|
+
// A 32-hex id absent from the tree may be a group created moments ago in
|
|
483
|
+
// this same session; let it through and rely on the visibility backstop.
|
|
484
|
+
if (!match && args.roleGroupCode) {
|
|
485
|
+
throw new ToolInputError({
|
|
486
|
+
message: `角色组 ${requested} 不存在。可用组:${groups.map((group) => `${group.code}(id=${group.id},${group.name})`).join(";")}`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const roleGroupId = match?.id ?? String(requested);
|
|
490
|
+
const corpId = args.corpId ?? session.enterpriseId();
|
|
491
|
+
const data = (await wireCall(session, {
|
|
492
|
+
path: ENDPOINTS.organizationRoleAdd, method: "POST",
|
|
493
|
+
query: orgQuery(session),
|
|
494
|
+
body: {
|
|
495
|
+
roleName: args.roleName,
|
|
496
|
+
roleGroupId,
|
|
497
|
+
...(args.roleGroupName ? { roleGroupName: args.roleGroupName } : {}),
|
|
498
|
+
roleType: args.roleType ?? "SYS",
|
|
499
|
+
corpId,
|
|
500
|
+
...(args.code ? { code: args.code } : {}),
|
|
501
|
+
...(args.remarks ? { remarks: args.remarks } : {}),
|
|
502
|
+
},
|
|
503
|
+
})).data;
|
|
504
|
+
const roleId = assertResolvedId(String(data?.roleId ?? data?.id ?? ""), "org.role.create", "roleId");
|
|
505
|
+
// Backstop against the silent-orphan trap: re-read the tree and report the
|
|
506
|
+
// truth instead of reporting success for a role nobody can see.
|
|
507
|
+
// r28h F9: also pull the real code (create often leaves it null).
|
|
508
|
+
const treeData = (await wireCall(session, {
|
|
509
|
+
path: ENDPOINTS.organizationRoleList,
|
|
510
|
+
query: { corpId: session.enterpriseId(), expandAll: "true" },
|
|
511
|
+
})).data;
|
|
512
|
+
const located = locateRole(wireRows(treeData), roleId);
|
|
513
|
+
const visible = located !== null || (await readRoleTree(session)).roles.some((role) => role.id === roleId);
|
|
514
|
+
const readbackCorpId = data?.corpId ?? null;
|
|
515
|
+
const warnings = [];
|
|
516
|
+
if (!visible) {
|
|
517
|
+
warnings.push(`角色 ${roleId} 创建后不在 org.role.list 任何组下(孤儿角色):groupId 很可能没落到真实组 id。请 org.role.delete 后按 roleGroupCode 重建`);
|
|
518
|
+
}
|
|
519
|
+
if (readbackCorpId !== null && String(readbackCorpId) !== corpId) {
|
|
520
|
+
warnings.push(`平台回读 corpId=${String(readbackCorpId)},与请求的 ${corpId} 不一致`);
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
performed: { errcode: 0 },
|
|
524
|
+
readback: {
|
|
525
|
+
roleId,
|
|
526
|
+
roleName: located?.name ?? data?.roleName ?? args.roleName,
|
|
527
|
+
code: located?.code ?? data?.code ?? null,
|
|
528
|
+
roleGroupId: located?.groupId ?? roleGroupId,
|
|
529
|
+
roleGroupCode: located?.groupCode ?? match?.code ?? null,
|
|
530
|
+
roleGroupName: located?.groupName ?? match?.name ?? null,
|
|
531
|
+
visibleInRoleList: visible,
|
|
532
|
+
corpId: readbackCorpId,
|
|
533
|
+
...(warnings.length > 0 ? { warning: warnings.join(";") } : {}),
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
},
|
|
378
537
|
});
|
|
379
538
|
// ── department ────────────────────────────────────────────────────────────
|
|
380
|
-
writeTool(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
parentId
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
539
|
+
writeTool({
|
|
540
|
+
tools,
|
|
541
|
+
name: "org.department.create",
|
|
542
|
+
lane: LANE,
|
|
543
|
+
scope: SCOPES.deptCreate,
|
|
544
|
+
description: "新建部门(`POST organization/department/add`)。**1.1.25 平台强制 parentId**(省略会 200007「部门parentId不能为空!」)——省略时工具自动取部门树的第一个根部门作父级;deptType 传名字或 0..3(平台按 int 枚举反序列化,写 \"dept\" 会 10009)。返回 deptId。",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
name: z.string().min(1).describe("部门名称"),
|
|
547
|
+
parentId: z.string().min(1).optional().describe("上级部门 id;省略则自动挂到部门树的第一个根部门(1.1.25 不接受空 parentId)"),
|
|
548
|
+
deptType: z.union([z.string(), z.number().int()]).optional().describe("默认 0=部门;1=家校部门 2=云枢部门 3=虚拟家校部门"),
|
|
549
|
+
code: z.string().optional(),
|
|
550
|
+
managerId: z.string().optional().describe("部门主管的用户 id"),
|
|
551
|
+
sortKey: z.number().int().optional(),
|
|
552
|
+
remarks: z.string().optional(),
|
|
553
|
+
},
|
|
554
|
+
execute: async (session, args) => {
|
|
555
|
+
// department/add answers 200007 「部门parentId不能为空!」 on an empty
|
|
556
|
+
// parentId (live 1.1.25, r10 F5). Resolve the tree's first root instead of
|
|
557
|
+
// forwarding an omission the platform rejects.
|
|
558
|
+
let parentId = args.parentId;
|
|
559
|
+
if (!parentId) {
|
|
560
|
+
const treeData = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentTree })).data;
|
|
561
|
+
// `/department/tree` answers `{departmentList:[…]}` (the same shape
|
|
562
|
+
// org.department.list handles); wireRows alone would throw
|
|
563
|
+
// "expected row array in envelope data".
|
|
564
|
+
const roots = Array.isArray(treeData?.departmentList) ? treeData.departmentList : wireRows(treeData);
|
|
565
|
+
const root = roots.find((row) => {
|
|
566
|
+
const pid = row.parentId;
|
|
567
|
+
return pid === undefined || pid === null || pid === "";
|
|
568
|
+
});
|
|
569
|
+
parentId = root ? String(root.id ?? "") : "";
|
|
570
|
+
if (!parentId)
|
|
571
|
+
throw new ToolRefusalError({ status: "refused", blocker: "org-department-root-absent", message: "部门树没有任何根部门,无法推断 parentId;平台 1.1.25 不接受空 parentId,请先建根部门或显式传 parentId" });
|
|
572
|
+
}
|
|
573
|
+
const data = (await wireCall(session, {
|
|
574
|
+
path: ENDPOINTS.organizationDepartmentAdd, method: "POST", query: orgQuery(session),
|
|
575
|
+
body: {
|
|
576
|
+
name: args.name, corpId: session.enterpriseId(), unitType: "1", enabled: true, deptType: deptTypeCode(args.deptType),
|
|
577
|
+
parentId,
|
|
578
|
+
...(args.code ? { code: args.code } : {}),
|
|
579
|
+
...(args.managerId ? { managerId: args.managerId } : {}),
|
|
580
|
+
...(args.sortKey !== undefined ? { sortKey: args.sortKey } : {}),
|
|
581
|
+
...(args.remarks ? { remarks: args.remarks } : {}),
|
|
582
|
+
},
|
|
583
|
+
})).data;
|
|
584
|
+
const deptId = String(data?.id ?? "");
|
|
585
|
+
if (!deptId)
|
|
586
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: "平台未返回部门 id;先 org.department.list 回读确认" });
|
|
587
|
+
return { performed: { errcode: 0 }, readback: { deptId, name: data?.name ?? args.name, parentId: data?.parentId ?? args.parentId ?? null, code: data?.code ?? null } };
|
|
588
|
+
},
|
|
422
589
|
});
|
|
423
|
-
writeTool(
|
|
424
|
-
|
|
425
|
-
name:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
590
|
+
writeTool({
|
|
591
|
+
tools,
|
|
592
|
+
name: "org.department.update",
|
|
593
|
+
lane: LANE,
|
|
594
|
+
scope: SCOPES.deptUpdate,
|
|
595
|
+
description: "改部门(`POST organization/department/modefied`)。先 get_dept_info 读现状再合并提交,只传要改的字段(名称/上级/主管/排序/备注/启用)。",
|
|
596
|
+
inputSchema: {
|
|
597
|
+
deptId: z.string().min(1),
|
|
598
|
+
name: z.string().optional(),
|
|
599
|
+
parentId: z.string().optional(),
|
|
600
|
+
managerId: z.string().optional(),
|
|
601
|
+
sortKey: z.number().int().optional(),
|
|
602
|
+
remarks: z.string().optional(),
|
|
603
|
+
enabled: z.boolean().optional(),
|
|
604
|
+
},
|
|
605
|
+
execute: async (session, args) => {
|
|
606
|
+
const current = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentGet, query: { ...orgQuery(session), deptId: args.deptId } })).data;
|
|
607
|
+
if (!current)
|
|
608
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: `get_dept_info 未返回部门 ${args.deptId}` });
|
|
609
|
+
const body = {
|
|
610
|
+
...current,
|
|
611
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
612
|
+
...(args.parentId !== undefined ? { parentId: args.parentId } : {}),
|
|
613
|
+
...(args.managerId !== undefined ? { managerId: args.managerId } : {}),
|
|
614
|
+
...(args.sortKey !== undefined ? { sortKey: args.sortKey } : {}),
|
|
615
|
+
...(args.remarks !== undefined ? { remarks: args.remarks } : {}),
|
|
616
|
+
...(args.enabled !== undefined ? { enabled: args.enabled } : {}),
|
|
617
|
+
};
|
|
618
|
+
await wireCall(session, { path: ENDPOINTS.organizationDepartmentModify, method: "POST", query: orgQuery(session), body });
|
|
619
|
+
const after = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentGet, query: { ...orgQuery(session), deptId: args.deptId } })).data;
|
|
620
|
+
return { performed: { errcode: 0 }, readback: { deptId: args.deptId, name: after?.name ?? null, parentId: after?.parentId ?? null, managerId: after?.managerId ?? null, enabled: after?.enabled ?? null } };
|
|
621
|
+
},
|
|
447
622
|
});
|
|
448
|
-
writeTool(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
623
|
+
writeTool({
|
|
624
|
+
tools,
|
|
625
|
+
name: "org.department.delete",
|
|
626
|
+
lane: LANE,
|
|
627
|
+
scope: SCOPES.deptDelete,
|
|
628
|
+
description: "删除部门(`GET organization/department/strike_out?id=`)。平台会拒绝:有子部门 → 200009「部门下面存在子部门,不能删除!」;有用户 → 200002「部门下面存在用户,不能删除!」。工具先计数并在回读里给出,仍由平台裁决。",
|
|
629
|
+
inputSchema: {
|
|
630
|
+
deptId: z.string().min(1),
|
|
631
|
+
force: z.boolean().optional().describe("true = 先逐个删除子部门(仅当子部门无用户)再删本部门;有用户仍由平台裁决"),
|
|
632
|
+
transferUsersTo: z
|
|
633
|
+
.string()
|
|
634
|
+
.min(1)
|
|
635
|
+
.optional()
|
|
636
|
+
.describe("有员工时的转出目标部门 id:给了它就先把本部门(及 force 时的子树各部门)的员工逐人 transferDepartment 到该部门再删;不给则带人即拒绝。目标必须是现存部门,且不能是被删子树的一部分"),
|
|
637
|
+
},
|
|
638
|
+
execute: async (session, args) => {
|
|
639
|
+
const users = wireRows((await wireCall(session, { path: ENDPOINTS.organizationDepartmentUsers, query: { ...orgQuery(session), deptId: args.deptId } })).data);
|
|
640
|
+
// Pre-check children: the platform's 200009 ("部门下面存在子部门") is
|
|
641
|
+
// accurate but sends the caller hunting for ids. Name them and hand over
|
|
642
|
+
// the child-delete command (found during r15 org cleanup).
|
|
643
|
+
const walked = await collectOrgDepartments(session);
|
|
644
|
+
// transferUsersTo (opt-in member evacuation): the target must be a real
|
|
645
|
+
// department and must NOT be inside the subtree being deleted — moving
|
|
646
|
+
// people into a dept that is about to be deleted would lose them.
|
|
647
|
+
const transferTarget = typeof args.transferUsersTo === "string" ? args.transferUsersTo.trim() : "";
|
|
648
|
+
let transferUsers;
|
|
649
|
+
if (transferTarget) {
|
|
650
|
+
if (!walked.rows.some((row) => row.id === transferTarget))
|
|
651
|
+
throw new ToolRefusalError({
|
|
652
|
+
status: "refused", blocker: "department-transfer-target-absent", deptId: String(args.deptId), transferUsersTo: transferTarget,
|
|
653
|
+
message: `transferUsersTo 的目标部门 ${transferTarget} 不存在(org.department.list 里没有)。先确认部门 id,或先创建目标部门。`,
|
|
654
|
+
});
|
|
655
|
+
const subtreeIds = new Set([String(args.deptId)]);
|
|
656
|
+
let grew = true;
|
|
657
|
+
while (grew) {
|
|
658
|
+
grew = false;
|
|
659
|
+
for (const row of walked.rows)
|
|
660
|
+
if (subtreeIds.has(row.parentId) && !subtreeIds.has(row.id)) {
|
|
661
|
+
subtreeIds.add(row.id);
|
|
662
|
+
grew = true;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (subtreeIds.has(transferTarget))
|
|
666
|
+
throw new ToolRefusalError({
|
|
667
|
+
status: "refused", blocker: "department-transfer-into-subtree", deptId: String(args.deptId), transferUsersTo: transferTarget,
|
|
668
|
+
message: `transferUsersTo=${transferTarget} 在被删除的子树里——把人转进一个即将被删的部门会丢成员。换一个子树外的部门。`,
|
|
669
|
+
});
|
|
670
|
+
transferUsers = async (fromDeptId, deptUsers) => {
|
|
671
|
+
let moved = 0;
|
|
672
|
+
for (const member of deptUsers) {
|
|
673
|
+
const userId = String(member.id ?? member.userId ?? "");
|
|
674
|
+
if (!userId)
|
|
675
|
+
continue;
|
|
676
|
+
await wireCall(session, {
|
|
677
|
+
path: ENDPOINTS.organizationUserDeptTransfer, method: "POST", query: orgQuery(session),
|
|
678
|
+
body: { userIds: [userId], sourceDept: fromDeptId, targetDept: transferTarget },
|
|
679
|
+
});
|
|
680
|
+
moved += 1;
|
|
481
681
|
}
|
|
682
|
+
return moved;
|
|
683
|
+
};
|
|
482
684
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
await wireCall(session, {
|
|
495
|
-
path: ENDPOINTS.organizationUserDeptTransfer, method: "POST", query: orgQuery(session),
|
|
496
|
-
body: { userIds: [userId], sourceDept: fromDeptId, targetDept: transferTarget },
|
|
685
|
+
const children = walked.rows
|
|
686
|
+
.filter((row) => row.parentId === String(args.deptId))
|
|
687
|
+
.map((row) => ({ id: row.id, name: row.name }))
|
|
688
|
+
.filter((row) => row.id.length > 0);
|
|
689
|
+
let deletedChildren = 0;
|
|
690
|
+
if (children.length > 0) {
|
|
691
|
+
if (args.force !== true)
|
|
692
|
+
throw new ToolRefusalError({
|
|
693
|
+
status: "refused", blocker: "department-has-children", deptId: String(args.deptId), children,
|
|
694
|
+
nextCommand: `omc call org.department.delete --args '${JSON.stringify({ deptId: children[0].id })}'`,
|
|
695
|
+
message: `部门 ${args.deptId} 下还有 ${children.length} 个子部门,平台拒绝删除(200009)。先删子部门,或传 force:true 让工具按顺序删除:${children.map((child) => `${child.name}(${child.id})`).join("、")}。`,
|
|
497
696
|
});
|
|
498
|
-
|
|
697
|
+
// force = deepest-first over the WHOLE subtree (a direct child with its
|
|
698
|
+
// own children would fail 200009 otherwise).
|
|
699
|
+
const byParent = new Map();
|
|
700
|
+
for (const row of walked.rows) {
|
|
701
|
+
if (row.id === String(args.deptId))
|
|
702
|
+
continue;
|
|
703
|
+
const list = byParent.get(row.parentId) ?? [];
|
|
704
|
+
list.push({ id: row.id, name: row.name });
|
|
705
|
+
byParent.set(row.parentId, list);
|
|
499
706
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
707
|
+
const ordered = [];
|
|
708
|
+
const walk = (parentId) => {
|
|
709
|
+
for (const child of byParent.get(parentId) ?? []) {
|
|
710
|
+
walk(child.id);
|
|
711
|
+
ordered.push(child);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
walk(String(args.deptId));
|
|
715
|
+
deletedChildren = ordered.length;
|
|
716
|
+
for (const child of ordered) {
|
|
717
|
+
try {
|
|
718
|
+
if (transferUsers) {
|
|
719
|
+
const childUsers = wireRows((await wireCall(session, { path: ENDPOINTS.organizationDepartmentUsers, query: { ...orgQuery(session), deptId: child.id } })).data);
|
|
720
|
+
if (childUsers.length > 0)
|
|
721
|
+
await transferUsers(child.id, childUsers);
|
|
722
|
+
}
|
|
723
|
+
await wireCall(session, { path: ENDPOINTS.organizationDepartmentDelete, query: { ...orgQuery(session), id: child.id } });
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
727
|
+
const usersPending = /200002|用户/.test(message);
|
|
728
|
+
throw new ToolRefusalError({
|
|
729
|
+
status: "refused", blocker: usersPending ? "department-has-users" : "department-delete-failed",
|
|
730
|
+
deptId: String(args.deptId), blockedChild: child,
|
|
731
|
+
nextCommand: `omc call org.department.users --args '${JSON.stringify({ deptId: child.id })}'`,
|
|
732
|
+
message: `force 清理到子部门 ${child.name}(${child.id}) 时失败:${message}。${usersPending ? "该部门下还有员工——先转出再删(或传 transferUsersTo 让工具搬)。" : "平台拒绝删除该子部门。"}`,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
let transferredUsers = 0;
|
|
738
|
+
if (users.length > 0 && transferUsers) {
|
|
739
|
+
transferredUsers = await transferUsers(String(args.deptId), users);
|
|
740
|
+
}
|
|
741
|
+
if (users.length > 0 && !transferUsers) {
|
|
742
|
+
// Name every member and hand over a runnable transfer for the first one:
|
|
743
|
+
// "先看人再转出" left the caller to assemble the transfer args (item 9).
|
|
744
|
+
const members = users.map((row) => ({ id: row.id ?? row.userId ?? null, name: row.name ?? null, username: row.username ?? null }));
|
|
745
|
+
const parentId = walked.rows.find((row) => row.id === String(args.deptId))?.parentId ?? "";
|
|
746
|
+
const first = members.find((member) => typeof member.id === "string" && member.id.length > 0);
|
|
510
747
|
throw new ToolRefusalError({
|
|
511
|
-
status: "refused", blocker: "department-has-
|
|
512
|
-
nextCommand:
|
|
513
|
-
|
|
748
|
+
status: "refused", blocker: "department-has-users", deptId: String(args.deptId), users: users.length, members,
|
|
749
|
+
nextCommand: first && parentId
|
|
750
|
+
? `omc call org.user.transferDepartment --args '${JSON.stringify({ userId: first.id, departmentId: parentId })}'`
|
|
751
|
+
: `omc call org.department.users --args '${JSON.stringify({ deptId: String(args.deptId) })}'`,
|
|
752
|
+
message: `部门 ${args.deptId} 下还有 ${users.length} 名员工,平台拒绝删除(200002)。成员:${members.map((member) => `${member.name ?? "?"}(${member.id ?? "?"})`).join("、")}。` +
|
|
753
|
+
`平台不允许删带人的部门,也不提供"随部门级联转出"。两个选择:` +
|
|
754
|
+
`① 逐个 org.user.transferDepartment 转到 ${parentId ? `父部门 ${parentId}` : "目标部门"} 后再删;` +
|
|
755
|
+
`② 本命令直接带 transferUsersTo:"<目标部门id>",工具会先把人逐人转出(含 force 时的子树各部门)再删。` +
|
|
756
|
+
`(默认不搬人——搬人是组织语义决策,不替你选目标部门。)`,
|
|
514
757
|
});
|
|
515
|
-
// force = deepest-first over the WHOLE subtree (a direct child with its
|
|
516
|
-
// own children would fail 200009 otherwise).
|
|
517
|
-
const byParent = new Map();
|
|
518
|
-
for (const row of walked.rows) {
|
|
519
|
-
if (row.id === String(args.deptId))
|
|
520
|
-
continue;
|
|
521
|
-
const list = byParent.get(row.parentId) ?? [];
|
|
522
|
-
list.push({ id: row.id, name: row.name });
|
|
523
|
-
byParent.set(row.parentId, list);
|
|
524
758
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
759
|
+
await wireCall(session, { path: ENDPOINTS.organizationDepartmentDelete, query: { ...orgQuery(session), id: args.deptId } });
|
|
760
|
+
return {
|
|
761
|
+
performed: { errcode: 0 },
|
|
762
|
+
readback: {
|
|
763
|
+
deptId: args.deptId, deleted: true, usersBeforeDelete: users.length, deletedChildren,
|
|
764
|
+
...(transferredUsers > 0 ? { transferredUsers, transferredTo: transferTarget } : {}),
|
|
765
|
+
},
|
|
531
766
|
};
|
|
532
|
-
|
|
533
|
-
deletedChildren = ordered.length;
|
|
534
|
-
for (const child of ordered) {
|
|
535
|
-
try {
|
|
536
|
-
if (transferUsers) {
|
|
537
|
-
const childUsers = wireRows((await wireCall(session, { path: ENDPOINTS.organizationDepartmentUsers, query: { ...orgQuery(session), deptId: child.id } })).data);
|
|
538
|
-
if (childUsers.length > 0)
|
|
539
|
-
await transferUsers(child.id, childUsers);
|
|
540
|
-
}
|
|
541
|
-
await wireCall(session, { path: ENDPOINTS.organizationDepartmentDelete, query: { ...orgQuery(session), id: child.id } });
|
|
542
|
-
}
|
|
543
|
-
catch (error) {
|
|
544
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
545
|
-
const usersPending = /200002|用户/.test(message);
|
|
546
|
-
throw new ToolRefusalError({
|
|
547
|
-
status: "refused", blocker: usersPending ? "department-has-users" : "department-delete-failed",
|
|
548
|
-
deptId: String(args.deptId), blockedChild: child,
|
|
549
|
-
nextCommand: `omc call org.department.users --args '${JSON.stringify({ deptId: child.id })}'`,
|
|
550
|
-
message: `force 清理到子部门 ${child.name}(${child.id}) 时失败:${message}。${usersPending ? "该部门下还有员工——先转出再删(或传 transferUsersTo 让工具搬)。" : "平台拒绝删除该子部门。"}`,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
let transferredUsers = 0;
|
|
556
|
-
if (users.length > 0 && transferUsers) {
|
|
557
|
-
transferredUsers = await transferUsers(String(args.deptId), users);
|
|
558
|
-
}
|
|
559
|
-
if (users.length > 0 && !transferUsers) {
|
|
560
|
-
// Name every member and hand over a runnable transfer for the first one:
|
|
561
|
-
// "先看人再转出" left the caller to assemble the transfer args (item 9).
|
|
562
|
-
const members = users.map((row) => ({ id: row.id ?? row.userId ?? null, name: row.name ?? null, username: row.username ?? null }));
|
|
563
|
-
const parentId = walked.rows.find((row) => row.id === String(args.deptId))?.parentId ?? "";
|
|
564
|
-
const first = members.find((member) => typeof member.id === "string" && member.id.length > 0);
|
|
565
|
-
throw new ToolRefusalError({
|
|
566
|
-
status: "refused", blocker: "department-has-users", deptId: String(args.deptId), users: users.length, members,
|
|
567
|
-
nextCommand: first && parentId
|
|
568
|
-
? `omc call org.user.transferDepartment --args '${JSON.stringify({ userId: first.id, departmentId: parentId })}'`
|
|
569
|
-
: `omc call org.department.users --args '${JSON.stringify({ deptId: String(args.deptId) })}'`,
|
|
570
|
-
message: `部门 ${args.deptId} 下还有 ${users.length} 名员工,平台拒绝删除(200002)。成员:${members.map((member) => `${member.name ?? "?"}(${member.id ?? "?"})`).join("、")}。` +
|
|
571
|
-
`平台不允许删带人的部门,也不提供"随部门级联转出"。两个选择:` +
|
|
572
|
-
`① 逐个 org.user.transferDepartment 转到 ${parentId ? `父部门 ${parentId}` : "目标部门"} 后再删;` +
|
|
573
|
-
`② 本命令直接带 transferUsersTo:"<目标部门id>",工具会先把人逐人转出(含 force 时的子树各部门)再删。` +
|
|
574
|
-
`(默认不搬人——搬人是组织语义决策,不替你选目标部门。)`,
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
await wireCall(session, { path: ENDPOINTS.organizationDepartmentDelete, query: { ...orgQuery(session), id: args.deptId } });
|
|
578
|
-
return {
|
|
579
|
-
performed: { errcode: 0 },
|
|
580
|
-
readback: {
|
|
581
|
-
deptId: args.deptId, deleted: true, usersBeforeDelete: users.length, deletedChildren,
|
|
582
|
-
...(transferredUsers > 0 ? { transferredUsers, transferredTo: transferTarget } : {}),
|
|
583
|
-
},
|
|
584
|
-
};
|
|
767
|
+
},
|
|
585
768
|
});
|
|
586
769
|
tools.push(defineTool({
|
|
587
770
|
name: "org.department.users",
|
|
@@ -595,114 +778,138 @@ export function registerOrganizationLane(tools) {
|
|
|
595
778
|
}),
|
|
596
779
|
}));
|
|
597
780
|
// ── employee ──────────────────────────────────────────────────────────────
|
|
598
|
-
writeTool(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
:
|
|
642
|
-
|
|
643
|
-
|
|
781
|
+
writeTool({
|
|
782
|
+
tools,
|
|
783
|
+
name: "org.user.create",
|
|
784
|
+
lane: LANE,
|
|
785
|
+
scope: SCOPES.userCreate,
|
|
786
|
+
description: "新建员工账号(`POST organization/user/add`)。必填 name/username/departmentId;工具自动补 `userLabelIds:[]`、`userManagerConfigList:[]`、`roleIds:[]` —— 平台 dealUserLabel 对 null 直接 NPE(50000,真机 1.1.25)。返回 userId(recId);初始密码由平台默认策略生成。",
|
|
787
|
+
inputSchema: {
|
|
788
|
+
name: z.string().min(1),
|
|
789
|
+
username: z.string().min(1).describe("登录账号"),
|
|
790
|
+
departmentId: z.string().min(1).describe("所属部门 id(必须先存在,否则 201011「用户的部门不存在!」)"),
|
|
791
|
+
mobile: z.string().optional(),
|
|
792
|
+
email: z.string().optional(),
|
|
793
|
+
employeeNo: z.string().optional(),
|
|
794
|
+
gender: z.enum(["MALE", "FEMALE", "OTHER"]).optional(),
|
|
795
|
+
roleIds: z.array(z.string()).optional().describe("同时授予的角色 id"),
|
|
796
|
+
partTimeDeptIds: z.array(z.string()).optional().describe("兼职部门 id(主部门仍是 departmentId)"),
|
|
797
|
+
deptIds: z.array(z.string()).optional().describe("`partTimeDeptIds` 的别名"),
|
|
798
|
+
userLabelIds: z.array(z.string()).optional(),
|
|
799
|
+
active: z.boolean().optional(),
|
|
800
|
+
status: z.enum(["ENABLE", "DISABLE", "LOCK"]).optional(),
|
|
801
|
+
},
|
|
802
|
+
execute: async (session, args) => {
|
|
803
|
+
const data = (await wireCall(session, {
|
|
804
|
+
path: ENDPOINTS.organizationUserAdd, method: "POST", query: orgQuery(session),
|
|
805
|
+
body: {
|
|
806
|
+
name: args.name, username: args.username, departmentId: args.departmentId, currentDeptId: args.departmentId,
|
|
807
|
+
corpId: session.enterpriseId(), unitType: "3", enabled: true, active: args.active ?? true,
|
|
808
|
+
status: args.status ?? "ENABLE",
|
|
809
|
+
userLabelIds: args.userLabelIds ?? [], userManagerConfigList: [], roleIds: args.roleIds ?? [], deptIds: args.partTimeDeptIds ?? args.deptIds ?? [],
|
|
810
|
+
...(args.mobile ? { mobile: args.mobile } : {}),
|
|
811
|
+
...(args.email ? { email: args.email } : {}),
|
|
812
|
+
...(args.employeeNo ? { employeeNo: args.employeeNo } : {}),
|
|
813
|
+
...(args.gender ? { gender: args.gender } : {}),
|
|
814
|
+
},
|
|
815
|
+
})).data;
|
|
816
|
+
const userId = String(data?.id ?? "");
|
|
817
|
+
if (!userId)
|
|
818
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: "平台未返回用户 id;先 org.user.search 回读确认" });
|
|
819
|
+
return {
|
|
820
|
+
performed: { errcode: 0 },
|
|
821
|
+
readback: {
|
|
822
|
+
userId,
|
|
823
|
+
name: data?.name ?? args.name,
|
|
824
|
+
username: data?.username ?? args.username,
|
|
825
|
+
departmentId: data?.departmentId ?? args.departmentId,
|
|
826
|
+
// 1.1.25 does not echo `active` on add and does not honour an update to
|
|
827
|
+
// it (r15 P7): the flag looks derived from account activation/binding.
|
|
828
|
+
// Say so, so nobody re-creates the user to "fix" it.
|
|
829
|
+
...(args.active !== undefined
|
|
830
|
+
? { activeWarning: `平台不回读 active(1.1.25 上它疑似由账号激活/绑定派生,不是可直接写的开关)。启停账号用 status=ENABLE/DISABLE/LOCK;不要为改 active 新建用户。` }
|
|
831
|
+
: {}),
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
},
|
|
644
835
|
});
|
|
645
|
-
writeTool(
|
|
646
|
-
|
|
647
|
-
name:
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
836
|
+
writeTool({
|
|
837
|
+
tools,
|
|
838
|
+
name: "org.user.update",
|
|
839
|
+
lane: LANE,
|
|
840
|
+
scope: SCOPES.userUpdate,
|
|
841
|
+
description: "改员工(`POST organization/user/modify`)。先 user/info 读现状再合并;工具强制补 `deptIds:[]` 等四个数组——平台 modified 对 null 的 deptIds 直接 NPE(50000,真机 1.1.25)。可改姓名/部门/手机/邮箱/角色/启停。",
|
|
842
|
+
inputSchema: {
|
|
843
|
+
userId: z.string().min(1),
|
|
844
|
+
name: z.string().optional(),
|
|
845
|
+
departmentId: z.string().optional(),
|
|
846
|
+
mobile: z.string().optional(),
|
|
847
|
+
email: z.string().optional(),
|
|
848
|
+
employeeNo: z.string().optional(),
|
|
849
|
+
gender: z.enum(["MALE", "FEMALE", "OTHER"]).optional(),
|
|
850
|
+
roleIds: z.array(z.string()).optional(),
|
|
851
|
+
partTimeDeptIds: z.array(z.string()).optional().describe("兼职部门 id;**覆盖语义**(传 [] 清空兼职,主部门不变)"),
|
|
852
|
+
active: z.boolean().optional(),
|
|
853
|
+
status: z.enum(["ENABLE", "DISABLE", "LOCK"]).optional(),
|
|
854
|
+
},
|
|
855
|
+
execute: async (session, args) => {
|
|
856
|
+
const current = (await wireCall(session, { path: ENDPOINTS.organizationUserInfo, query: { ...orgQuery(session), id: args.userId } })).data;
|
|
857
|
+
if (!current)
|
|
858
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: `user/info 未返回用户 ${args.userId}` });
|
|
859
|
+
const body = {
|
|
860
|
+
...current,
|
|
861
|
+
id: args.userId,
|
|
862
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
863
|
+
...(args.departmentId !== undefined ? { departmentId: args.departmentId, currentDeptId: args.departmentId } : {}),
|
|
864
|
+
...(args.mobile !== undefined ? { mobile: args.mobile } : {}),
|
|
865
|
+
...(args.email !== undefined ? { email: args.email } : {}),
|
|
866
|
+
...(args.employeeNo !== undefined ? { employeeNo: args.employeeNo } : {}),
|
|
867
|
+
...(args.gender !== undefined ? { gender: args.gender } : {}),
|
|
868
|
+
...(args.active !== undefined ? { active: args.active } : {}),
|
|
869
|
+
...(args.status !== undefined ? { status: args.status } : {}),
|
|
870
|
+
deptIds: args.partTimeDeptIds ?? (Array.isArray(current.deptIds) ? current.deptIds : []),
|
|
871
|
+
userLabelIds: Array.isArray(current.userLabelIds) ? current.userLabelIds : [],
|
|
872
|
+
userManagerConfigList: Array.isArray(current.userManagerConfigList) ? current.userManagerConfigList : [],
|
|
873
|
+
roleIds: args.roleIds ?? (Array.isArray(current.roleIds) ? current.roleIds : []),
|
|
874
|
+
};
|
|
875
|
+
await wireCall(session, { path: ENDPOINTS.organizationUserModify, method: "POST", query: orgQuery(session), body });
|
|
876
|
+
const after = (await wireCall(session, { path: ENDPOINTS.organizationUserInfo, query: { ...orgQuery(session), id: args.userId } })).data;
|
|
877
|
+
const departments = wireRows((await wireCall(session, {
|
|
878
|
+
path: ENDPOINTS.organizationUserDepartments, query: { ...orgQuery(session), id: String(args.userId) },
|
|
879
|
+
})).data).map((row) => ({ deptId: row.deptId ?? null, deptName: row.deptName ?? null, isMain: row.isMain ?? null }));
|
|
880
|
+
return {
|
|
881
|
+
performed: { errcode: 0 },
|
|
882
|
+
readback: {
|
|
883
|
+
userId: args.userId,
|
|
884
|
+
name: after?.name ?? null,
|
|
885
|
+
departmentId: after?.departmentId ?? null,
|
|
886
|
+
mobile: after?.mobile ?? null,
|
|
887
|
+
email: after?.email ?? null,
|
|
888
|
+
active: after?.active ?? null,
|
|
889
|
+
userStatus: after?.status ?? null,
|
|
890
|
+
departments,
|
|
891
|
+
// r15 P7: `active` did not persist on 1.1.25 (passed true, read false).
|
|
892
|
+
// It looks derived from account activation/binding rather than a
|
|
893
|
+
// writable switch — say so instead of letting the caller re-create the
|
|
894
|
+
// user to "fix" it.
|
|
895
|
+
...(args.active !== undefined && after?.active !== args.active
|
|
896
|
+
? { activeWarning: `传了 active=${String(args.active)},平台读回 ${String(after?.active)}:active 在 1.1.25 上不是可直接写的开关(疑似由账号激活/绑定派生)。启停账号用 status=ENABLE/DISABLE/LOCK;不要为改 active 新建用户。` }
|
|
897
|
+
: {}),
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
},
|
|
702
901
|
});
|
|
703
|
-
writeTool(
|
|
704
|
-
|
|
705
|
-
|
|
902
|
+
writeTool({
|
|
903
|
+
tools,
|
|
904
|
+
name: "org.user.delete",
|
|
905
|
+
lane: LANE,
|
|
906
|
+
scope: SCOPES.userDelete,
|
|
907
|
+
description: "删除员工(`GET organization/user/strike_out?id=`)。只传用户 id;返回 deleted。",
|
|
908
|
+
inputSchema: { userId: z.string().min(1) },
|
|
909
|
+
execute: async (session, args) => {
|
|
910
|
+
await wireCall(session, { path: ENDPOINTS.organizationUserDelete, query: { ...orgQuery(session), id: args.userId } });
|
|
911
|
+
return { performed: { errcode: 0 }, readback: { userId: args.userId, deleted: true } };
|
|
912
|
+
},
|
|
706
913
|
});
|
|
707
914
|
tools.push(defineTool({
|
|
708
915
|
name: "org.user.info",
|
|
@@ -746,240 +953,298 @@ export function registerOrganizationLane(tools) {
|
|
|
746
953
|
return { userId: String(args.userId), count: departments.length, departments };
|
|
747
954
|
}),
|
|
748
955
|
}));
|
|
749
|
-
writeTool(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
956
|
+
writeTool({
|
|
957
|
+
tools,
|
|
958
|
+
name: "org.role.delete",
|
|
959
|
+
lane: LANE,
|
|
960
|
+
scope: SCOPES.roleDelete,
|
|
961
|
+
description: "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25):默认先读成员数、非空即拒绝并给出人数;`force:true` 才会先逐人移出再删。**可用 roleCode 代替 roleId**(org.role.list 的码表只有 code)。",
|
|
962
|
+
inputSchema: {
|
|
963
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
964
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表,如 qc_manager)或名称"),
|
|
965
|
+
force: z.boolean().optional().describe("true = 先移出全部成员再删角色"),
|
|
966
|
+
},
|
|
967
|
+
execute: async (session, args) => {
|
|
968
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.delete");
|
|
969
|
+
const rows = await memberRows(session, roleId);
|
|
970
|
+
if (rows.length > 0 && args.force !== true)
|
|
971
|
+
throw new ToolRefusalError({
|
|
972
|
+
status: "refused", blocker: "role-has-members", roleId, members: rows.length,
|
|
973
|
+
nextCommand: `omc call org.role.delete --args '${JSON.stringify({ roleId, force: true })}'`,
|
|
974
|
+
message: `角色下还有 ${rows.length} 名成员,平台拒绝删除(errmsg:角色下已经包含用户数据)。先 org.role.removeUsers,或传 force:true 让工具移出后再删。`,
|
|
975
|
+
});
|
|
976
|
+
if (rows.length > 0)
|
|
977
|
+
await removeMembers(session, roleId, rows.map(roleUserId));
|
|
978
|
+
await wireCall(session, { path: ENDPOINTS.organizationRoleDelete, query: { ...orgQuery(session), ids: roleId } });
|
|
979
|
+
return { performed: { errcode: 0 }, readback: { roleId, deleted: true, removedMembers: rows.length } };
|
|
980
|
+
},
|
|
766
981
|
});
|
|
767
|
-
writeTool(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
982
|
+
writeTool({
|
|
983
|
+
tools,
|
|
984
|
+
name: "org.roleGroup.delete",
|
|
985
|
+
lane: LANE,
|
|
986
|
+
scope: SCOPES.roleGroupDelete,
|
|
987
|
+
description: "删除角色组(`GET organization/role/strike_out_role_group?ids=`)。平台**拒绝删除仍含角色的组**(`-1 角色组下已经包含角色数据!`):默认先数角色、非空即拒绝;`force:true` 会逐个删角色(含其成员)再删组。",
|
|
988
|
+
inputSchema: {
|
|
989
|
+
roleGroupId: z.string().min(1),
|
|
990
|
+
force: z.boolean().optional(),
|
|
991
|
+
},
|
|
992
|
+
execute: async (session, args) => {
|
|
993
|
+
const roles = await rolesInGroup(session, String(args.roleGroupId));
|
|
994
|
+
if (roles.length > 0 && args.force !== true)
|
|
995
|
+
throw new ToolRefusalError({
|
|
996
|
+
status: "refused", blocker: "role-group-has-roles", roleGroupId: String(args.roleGroupId), roles: roles.length,
|
|
997
|
+
nextCommand: `omc call org.roleGroup.delete --args '${JSON.stringify({ roleGroupId: String(args.roleGroupId), force: true })}'`,
|
|
998
|
+
message: `角色组下还有 ${roles.length} 个角色,平台拒绝删除(errmsg:角色组下已经包含角色数据)。先删角色,或传 force:true。`,
|
|
999
|
+
});
|
|
1000
|
+
for (const role of roles) {
|
|
1001
|
+
const members = await memberRows(session, role.id);
|
|
1002
|
+
if (members.length > 0)
|
|
1003
|
+
await removeMembers(session, role.id, members.map(roleUserId));
|
|
1004
|
+
await wireCall(session, { path: ENDPOINTS.organizationRoleDelete, query: { ...orgQuery(session), ids: role.id } });
|
|
1005
|
+
}
|
|
1006
|
+
await wireCall(session, { path: ENDPOINTS.organizationRoleGroupDelete, query: { ...orgQuery(session), ids: String(args.roleGroupId) } });
|
|
1007
|
+
return { performed: { errcode: 0 }, readback: { roleGroupId: String(args.roleGroupId), deleted: true, removedRoles: roles.map((role) => role.id) } };
|
|
1008
|
+
},
|
|
786
1009
|
});
|
|
787
|
-
writeTool(
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1010
|
+
writeTool({
|
|
1011
|
+
tools,
|
|
1012
|
+
name: "org.user.transferDepartment",
|
|
1013
|
+
lane: LANE,
|
|
1014
|
+
scope: SCOPES.userTransfer,
|
|
1015
|
+
description: "把员工从一个部门移到另一个部门(`POST organization/user/dept_transfer`,body `{userIds,sourceDept,targetDept}`)。sourceDept 必填(缺它 50000);移动后原部门不再包含该员工,主部门保持。",
|
|
1016
|
+
inputSchema: {
|
|
1017
|
+
userId: z.string().min(1),
|
|
1018
|
+
fromDeptId: z.string().min(1).describe("原部门 id(必填)"),
|
|
1019
|
+
toDeptId: z.string().min(1).describe("目标部门 id"),
|
|
1020
|
+
},
|
|
1021
|
+
execute: async (session, args) => {
|
|
1022
|
+
await wireCall(session, {
|
|
1023
|
+
path: ENDPOINTS.organizationUserDeptTransfer, method: "POST", query: orgQuery(session),
|
|
1024
|
+
body: { userIds: [String(args.userId)], sourceDept: String(args.fromDeptId), targetDept: String(args.toDeptId) },
|
|
1025
|
+
});
|
|
1026
|
+
const departments = wireRows((await wireCall(session, {
|
|
1027
|
+
path: ENDPOINTS.organizationUserDepartments, query: { ...orgQuery(session), id: String(args.userId) },
|
|
1028
|
+
})).data).map((row) => ({ deptId: row.deptId ?? null, deptName: row.deptName ?? null, isMain: row.isMain ?? null }));
|
|
1029
|
+
return { performed: { errcode: 0 }, readback: { userId: String(args.userId), fromDeptId: String(args.fromDeptId), toDeptId: String(args.toDeptId), departments } };
|
|
1030
|
+
},
|
|
800
1031
|
});
|
|
801
|
-
writeTool(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
roleGroupId
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
};
|
|
831
|
-
await wireCall(session, { path: ENDPOINTS.organizationRoleModify, method: "POST", query: orgQuery(session), body });
|
|
832
|
-
const after = (await wireCall(session, { path: ENDPOINTS.organizationRoleGet, query: { ...orgQuery(session), roleId: String(args.roleId) } })).data;
|
|
833
|
-
// role/get answers RoleModel (name/groupId), not RoleVO (roleName/
|
|
834
|
-
// roleGroupId) — read both spellings so the readback is never null.
|
|
835
|
-
return {
|
|
836
|
-
performed: { errcode: 0 },
|
|
837
|
-
readback: {
|
|
1032
|
+
writeTool({
|
|
1033
|
+
tools,
|
|
1034
|
+
name: "org.role.update",
|
|
1035
|
+
lane: LANE,
|
|
1036
|
+
scope: SCOPES.roleUpdate,
|
|
1037
|
+
description: "改角色名称/编码/备注/所属角色组(`POST organization/role/modify_role`)。先读现状再提交,只传要改的字段。**平台要求 roleGroupId 必填**(缺则 202012「角色组主键Id不能为空」),工具默认回填当前组;**corpId 不可改**(RoleHelper.update 不读该字段),要换企业只能在目标企业下重建角色。",
|
|
1038
|
+
inputSchema: {
|
|
1039
|
+
roleId: z.string().min(1),
|
|
1040
|
+
roleName: z.string().optional(),
|
|
1041
|
+
roleGroupId: z.string().min(1).optional().describe("移到别的角色组(默认保持当前组)。平台 update 必填该字段,且目标组必须存在、roleType=SYS"),
|
|
1042
|
+
code: z.string().optional(),
|
|
1043
|
+
remarks: z.string().optional(),
|
|
1044
|
+
},
|
|
1045
|
+
execute: async (session, args) => {
|
|
1046
|
+
const current = (await wireCall(session, { path: ENDPOINTS.organizationRoleGet, query: { ...orgQuery(session), roleId: String(args.roleId) } })).data;
|
|
1047
|
+
if (!current)
|
|
1048
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: `role/get 未返回角色 ${String(args.roleId)};不要盲目 modify` });
|
|
1049
|
+
// RoleHelper.update (1.1.25) validates roleName AND roleGroupId as
|
|
1050
|
+
// REQUIRED before touching the row (validateRoleParam): a delta that omits
|
|
1051
|
+
// either answers 202001 「角色名称不能为空」 / 202012 「角色组主键Id不能为空」.
|
|
1052
|
+
// Both are backfilled from role/get, so a remarks-only edit works.
|
|
1053
|
+
const roleName = String(args.roleName ?? current.roleName ?? current.name ?? "");
|
|
1054
|
+
if (roleName === "")
|
|
1055
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: `角色 ${String(args.roleId)} 读不到 roleName;平台 modify_role 要求该字段,请显式传 roleName` });
|
|
1056
|
+
const roleGroupId = String(args.roleGroupId ?? current.roleGroupId ?? current.groupId ?? "");
|
|
1057
|
+
if (roleGroupId === "")
|
|
1058
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: `角色 ${String(args.roleId)} 读不到 roleGroupId;平台 modify_role 要求该字段,请显式传 roleGroupId` });
|
|
1059
|
+
const body = {
|
|
1060
|
+
...current,
|
|
838
1061
|
roleId: String(args.roleId),
|
|
839
|
-
roleName
|
|
840
|
-
roleGroupId
|
|
841
|
-
code:
|
|
842
|
-
remarks:
|
|
843
|
-
|
|
844
|
-
|
|
1062
|
+
roleName,
|
|
1063
|
+
roleGroupId,
|
|
1064
|
+
...(args.code !== undefined ? { code: args.code } : {}),
|
|
1065
|
+
...(args.remarks !== undefined ? { remarks: args.remarks } : {}),
|
|
1066
|
+
corpId: current.corpId ?? "other",
|
|
1067
|
+
roleType: current.roleType ?? "SYS",
|
|
1068
|
+
};
|
|
1069
|
+
await wireCall(session, { path: ENDPOINTS.organizationRoleModify, method: "POST", query: orgQuery(session), body });
|
|
1070
|
+
const after = (await wireCall(session, { path: ENDPOINTS.organizationRoleGet, query: { ...orgQuery(session), roleId: String(args.roleId) } })).data;
|
|
1071
|
+
// role/get answers RoleModel (name/groupId), not RoleVO (roleName/
|
|
1072
|
+
// roleGroupId) — read both spellings so the readback is never null.
|
|
1073
|
+
return {
|
|
1074
|
+
performed: { errcode: 0 },
|
|
1075
|
+
readback: {
|
|
1076
|
+
roleId: String(args.roleId),
|
|
1077
|
+
roleName: after?.roleName ?? after?.name ?? null,
|
|
1078
|
+
roleGroupId: after?.roleGroupId ?? after?.groupId ?? null,
|
|
1079
|
+
code: after?.code ?? null,
|
|
1080
|
+
remarks: after?.remarks ?? null,
|
|
1081
|
+
},
|
|
1082
|
+
};
|
|
1083
|
+
},
|
|
845
1084
|
});
|
|
846
|
-
writeTool(
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1085
|
+
writeTool({
|
|
1086
|
+
tools,
|
|
1087
|
+
name: "org.role.addUsers",
|
|
1088
|
+
lane: LANE,
|
|
1089
|
+
scope: SCOPES.addUsers,
|
|
1090
|
+
description: "加成员并(可选)一次设好管理范围(`add_role_user` + 逐人 `update_ouscope`,一个工具调用)。users[].id 必须是 recId(org.user.search 的 id);传 `scopes` 会应用到本次新增的每个人。**平台的 add_role_user 虽然收 `ouScope` 但会忽略它**(实测 1.1.25,回读仍为空),所以范围必须单独写——工具内部已按顺序完成。加完回读成员与范围。",
|
|
1091
|
+
inputSchema: {
|
|
1092
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
1093
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称——省掉取 artifact 找 id"),
|
|
1094
|
+
users: z.array(z.object({ id: z.string().min(1).describe("用户记录 id(recId)"), name: z.string().min(1) })).min(1),
|
|
1095
|
+
scopes: z.array(z.object({
|
|
1096
|
+
id: z.string().min(1).describe("部门 id"),
|
|
1097
|
+
name: z.string().min(1),
|
|
1098
|
+
deptPath: z.string().optional(),
|
|
1099
|
+
type: z.string().optional().describe("默认 \"1\"(部门节点)"),
|
|
1100
|
+
})).optional().describe("应用到本次新增的每个成员的管理范围;省略则不设(成员 ouScope 为空)"),
|
|
1101
|
+
sortKey: z.number().int().optional(),
|
|
1102
|
+
userIds: z.array(z.object({ id: z.string().min(1), name: z.string().min(1) })).optional().describe("`users` 的别名"),
|
|
1103
|
+
},
|
|
1104
|
+
execute: async (session, args) => {
|
|
1105
|
+
const users = args.users ?? args.userIds ?? [];
|
|
1106
|
+
if (users.length === 0)
|
|
1107
|
+
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "org.role.addUsers", message: "需要 users(或 userIds)至少一人" });
|
|
1108
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.addUsers");
|
|
1109
|
+
await wireCall(session, {
|
|
1110
|
+
path: ENDPOINTS.organizationRoleAddUser, method: "POST",
|
|
1111
|
+
query: orgQuery(session),
|
|
1112
|
+
body: { roleId, sortKey: args.sortKey ?? 999, userIds: users.map((user) => ({ id: user.id, name: user.name, type: 3, unitType: "3" })) },
|
|
1113
|
+
});
|
|
1114
|
+
const readback = async () => {
|
|
1115
|
+
const data = (await wireCall(session, {
|
|
1116
|
+
path: ENDPOINTS.organizationRoleUsers,
|
|
1117
|
+
query: { ...orgQuery(session), roleId: roleId, page: "0", size: "500" },
|
|
1118
|
+
})).data;
|
|
1119
|
+
return Array.isArray(data?.content) ? data.content : wireRows(data);
|
|
1120
|
+
};
|
|
1121
|
+
const rows = await readback();
|
|
1122
|
+
const present = new Set(rows.map(roleUserId));
|
|
1123
|
+
const added = users.filter((user) => present.has(user.id)).map((user) => user.id);
|
|
1124
|
+
const missing = users.filter((user) => !present.has(user.id)).map((user) => user.id);
|
|
1125
|
+
const scopeResults = [];
|
|
1126
|
+
if (args.scopes !== undefined && args.scopes.length > 0) {
|
|
1127
|
+
const ouScopeList = args.scopes.map((scope) => ({ id: scope.id, name: scope.name, type: scope.type ?? "1", ...(scope.deptPath ? { deptPath: scope.deptPath } : {}) }));
|
|
1128
|
+
for (const id of added) {
|
|
1129
|
+
await wireCall(session, {
|
|
1130
|
+
path: ENDPOINTS.organizationRoleUpdateOuScope, method: "POST", query: orgQuery(session),
|
|
1131
|
+
body: { roleId: roleId, userId: id, unitType: 3, sortKey: args.sortKey ?? 999, ouScopeList },
|
|
1132
|
+
});
|
|
1133
|
+
const scope = wireRows((await wireCall(session, {
|
|
1134
|
+
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
1135
|
+
query: { ...orgQuery(session), roleId: roleId, userId: id },
|
|
1136
|
+
})).data);
|
|
1137
|
+
scopeResults.push({ userId: id, scopes: scope.length });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return {
|
|
1141
|
+
performed: { errcode: 0 },
|
|
1142
|
+
readback: {
|
|
1143
|
+
roleId: roleId, added,
|
|
1144
|
+
...(missing.length > 0 ? { missing, hint: "这些 recId 未出现在成员列表:确认 id 是 recId、用户未离职/禁用" } : {}),
|
|
1145
|
+
...(scopeResults.length > 0 ? { scopeResults } : {}),
|
|
1146
|
+
// ouScope 是流程角色参与人解析的过滤维度:平台
|
|
1147
|
+
// ParticipantService.listManagerIdsByUserIdsAndRoleCode 用
|
|
1148
|
+
// `h_org_role_user.ouScope ∩ 发起人部门链`,ouScope 为空 = 该成员
|
|
1149
|
+
// 永远解析不到。没传 scopes 时把这条代价显式说出来(r9+r10 两次
|
|
1150
|
+
// 真机坐实;不是 corpId 问题)。
|
|
1151
|
+
...(args.scopes === undefined || args.scopes.length === 0
|
|
1152
|
+
? {
|
|
1153
|
+
warnings: [
|
|
1154
|
+
{
|
|
1155
|
+
code: "org-role-member-scope-empty",
|
|
1156
|
+
severity: "warning",
|
|
1157
|
+
message: "本次未传 scopes:新增成员的 ouScope 为空,**不能作为流程角色参与人被解析**(平台按 ouScope ∩ 发起人部门链过滤,r9/r10 真机坐实)。如果这个角色要用于审批/参与人,请补 scopes(部门节点)或随后调用 org.role.setUserScope;只用于权限分配可忽略。",
|
|
1158
|
+
},
|
|
1159
|
+
],
|
|
1160
|
+
}
|
|
1161
|
+
: {}),
|
|
1162
|
+
total: rows.length,
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
writeTool({
|
|
1168
|
+
tools,
|
|
1169
|
+
name: "org.role.removeUsers",
|
|
1170
|
+
lane: LANE,
|
|
1171
|
+
scope: SCOPES.removeUsers,
|
|
1172
|
+
description: "把成员移出角色(`POST organization/role/strike_out_role_user`)。**平台要求 JSON body `{roleId,userIds:[...]}`**——用 query 或 `ids` 会报 10009 `Required request body is missing`(实测 1.1.25)。只移出角色,不删用户。",
|
|
1173
|
+
inputSchema: {
|
|
1174
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
1175
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称"),
|
|
1176
|
+
ids: z.array(z.string().min(1)).min(1).describe("要移出的成员 recId 列表"),
|
|
1177
|
+
},
|
|
1178
|
+
execute: async (session, args) => {
|
|
1179
|
+
// r28h F11: always echo the *resolved* roleId — never String(args.roleId)
|
|
1180
|
+
// (that becomes the literal "undefined" when the caller passed roleCode).
|
|
1181
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.removeUsers");
|
|
1182
|
+
await wireCall(session, {
|
|
1183
|
+
path: ENDPOINTS.organizationRoleRemoveUser, method: "POST", query: orgQuery(session),
|
|
1184
|
+
body: { roleId, userIds: args.ids },
|
|
1185
|
+
});
|
|
869
1186
|
const data = (await wireCall(session, {
|
|
870
1187
|
path: ENDPOINTS.organizationRoleUsers,
|
|
871
|
-
query: { ...orgQuery(session), roleId
|
|
1188
|
+
query: { ...orgQuery(session), roleId, page: "0", size: "500" },
|
|
872
1189
|
})).data;
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const added = users.filter((user) => present.has(user.id)).map((user) => user.id);
|
|
878
|
-
const missing = users.filter((user) => !present.has(user.id)).map((user) => user.id);
|
|
879
|
-
const scopeResults = [];
|
|
880
|
-
if (args.scopes !== undefined && args.scopes.length > 0) {
|
|
881
|
-
const ouScopeList = args.scopes.map((scope) => ({ id: scope.id, name: scope.name, type: scope.type ?? "1", ...(scope.deptPath ? { deptPath: scope.deptPath } : {}) }));
|
|
882
|
-
for (const id of added) {
|
|
883
|
-
await wireCall(session, {
|
|
884
|
-
path: ENDPOINTS.organizationRoleUpdateOuScope, method: "POST", query: orgQuery(session),
|
|
885
|
-
body: { roleId: roleId, userId: id, unitType: 3, sortKey: args.sortKey ?? 999, ouScopeList },
|
|
886
|
-
});
|
|
887
|
-
const scope = wireRows((await wireCall(session, {
|
|
888
|
-
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
889
|
-
query: { ...orgQuery(session), roleId: roleId, userId: id },
|
|
890
|
-
})).data);
|
|
891
|
-
scopeResults.push({ userId: id, scopes: scope.length });
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
return {
|
|
895
|
-
performed: { errcode: 0 },
|
|
896
|
-
readback: {
|
|
897
|
-
roleId: roleId, added,
|
|
898
|
-
...(missing.length > 0 ? { missing, hint: "这些 recId 未出现在成员列表:确认 id 是 recId、用户未离职/禁用" } : {}),
|
|
899
|
-
...(scopeResults.length > 0 ? { scopeResults } : {}),
|
|
900
|
-
// ouScope 是流程角色参与人解析的过滤维度:平台
|
|
901
|
-
// ParticipantService.listManagerIdsByUserIdsAndRoleCode 用
|
|
902
|
-
// `h_org_role_user.ouScope ∩ 发起人部门链`,ouScope 为空 = 该成员
|
|
903
|
-
// 永远解析不到。没传 scopes 时把这条代价显式说出来(r9+r10 两次
|
|
904
|
-
// 真机坐实;不是 corpId 问题)。
|
|
905
|
-
...(args.scopes === undefined || args.scopes.length === 0
|
|
906
|
-
? {
|
|
907
|
-
warnings: [
|
|
908
|
-
{
|
|
909
|
-
code: "org-role-member-scope-empty",
|
|
910
|
-
severity: "warning",
|
|
911
|
-
message: "本次未传 scopes:新增成员的 ouScope 为空,**不能作为流程角色参与人被解析**(平台按 ouScope ∩ 发起人部门链过滤,r9/r10 真机坐实)。如果这个角色要用于审批/参与人,请补 scopes(部门节点)或随后调用 org.role.setUserScope;只用于权限分配可忽略。",
|
|
912
|
-
},
|
|
913
|
-
],
|
|
914
|
-
}
|
|
915
|
-
: {}),
|
|
916
|
-
total: rows.length,
|
|
917
|
-
},
|
|
918
|
-
};
|
|
919
|
-
});
|
|
920
|
-
writeTool(tools, "org.role.removeUsers", LANE, SCOPES.removeUsers, "把成员移出角色(`POST organization/role/strike_out_role_user`)。**平台要求 JSON body `{roleId,userIds:[...]}`**——用 query 或 `ids` 会报 10009 `Required request body is missing`(实测 1.1.25)。只移出角色,不删用户。", {
|
|
921
|
-
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
922
|
-
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称"),
|
|
923
|
-
ids: z.array(z.string().min(1)).min(1).describe("要移出的成员 recId 列表"),
|
|
924
|
-
}, async (session, args) => {
|
|
925
|
-
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.removeUsers");
|
|
926
|
-
await wireCall(session, {
|
|
927
|
-
path: ENDPOINTS.organizationRoleRemoveUser, method: "POST", query: orgQuery(session),
|
|
928
|
-
body: { roleId, userIds: args.ids },
|
|
929
|
-
});
|
|
930
|
-
const data = (await wireCall(session, {
|
|
931
|
-
path: ENDPOINTS.organizationRoleUsers,
|
|
932
|
-
query: { ...orgQuery(session), roleId: String(args.roleId), page: "0", size: "500" },
|
|
933
|
-
})).data;
|
|
934
|
-
const rows = Array.isArray(data?.content) ? data.content : wireRows(data);
|
|
935
|
-
const present = new Set(rows.map(roleUserId));
|
|
936
|
-
return { performed: { errcode: 0 }, readback: { roleId: String(args.roleId), removed: args.ids.filter((id) => !present.has(id)), stillPresent: args.ids.filter((id) => present.has(id)), total: data?.totalElements ?? rows.length } };
|
|
1190
|
+
const rows = Array.isArray(data?.content) ? data.content : wireRows(data);
|
|
1191
|
+
const present = new Set(rows.map(roleUserId));
|
|
1192
|
+
return { performed: { errcode: 0 }, readback: { roleId, removed: args.ids.filter((id) => !present.has(id)), stillPresent: args.ids.filter((id) => present.has(id)), total: data?.totalElements ?? rows.length } };
|
|
1193
|
+
},
|
|
937
1194
|
});
|
|
938
|
-
writeTool(
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
.
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1195
|
+
writeTool({
|
|
1196
|
+
tools,
|
|
1197
|
+
name: "org.role.setUserScope",
|
|
1198
|
+
lane: LANE,
|
|
1199
|
+
scope: SCOPES.setScope,
|
|
1200
|
+
description: "设置成员管理范围(`POST organization/role/update_ouscope`)。**不是枚举档位**(本人/本部门/本部门及下级/全部这类平台没有):scopes 是一组部门节点的自由列表。平台覆盖语义、无回滚:默认读旧值合并(mode:\"merge\"),mode:\"replace\" 才整体替换。**类型对照表**:setUserScope 的 unitType 传数字 3;addUsers 的 unitType 传字符串 \"3\";节点 type 传字符串 \"1\"(部门节点)。最小可跑示例:`{\"roleCode\":\"X\",\"userId\":\"<recId>\",\"scopes\":[{\"id\":\"<deptId>\",\"name\":\"<deptName>\",\"type\":\"1\"}]}`。",
|
|
1201
|
+
inputSchema: {
|
|
1202
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
1203
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称"),
|
|
1204
|
+
userId: z.string().min(1).describe("成员 recId"),
|
|
1205
|
+
scopes: z.array(z.object({
|
|
1206
|
+
id: z.string().min(1).describe("部门 id(org.department.list 的 id)"),
|
|
1207
|
+
name: z.string().min(1).describe("部门名(与 id 对应)"),
|
|
1208
|
+
deptPath: z.string().optional().describe("完整路径,如 根组织/广州总所"),
|
|
1209
|
+
// The wire wants the enum as a STRING ("1"); a number used to die in
|
|
1210
|
+
// input validation with a generic tool-input-invalid (r10). Accept both
|
|
1211
|
+
// and normalize, so the mistake cannot cost a round.
|
|
1212
|
+
type: z.union([z.string(), z.number()]).optional().describe("部门节点固定 \"1\"(字符串;也接受数字 1,工具会归一化)"),
|
|
1213
|
+
})).describe("要设置的管理范围节点列表(自由列表,不是枚举档位);每项 {id,name,type:\"1\"}"),
|
|
1214
|
+
mode: z.enum(["merge", "replace"]).optional().describe("默认 merge(读旧值合并);replace = 覆盖整个范围"),
|
|
1215
|
+
dryRun: z.boolean().optional(),
|
|
1216
|
+
},
|
|
1217
|
+
execute: async (session, args) => {
|
|
1218
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.setUserScope");
|
|
1219
|
+
const before = wireRows((await wireCall(session, {
|
|
1220
|
+
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
1221
|
+
query: { ...orgQuery(session), roleId, userId: args.userId },
|
|
1222
|
+
})).data);
|
|
1223
|
+
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 } : {}) }));
|
|
1224
|
+
const merged = args.mode === "replace"
|
|
1225
|
+
? wanted
|
|
1226
|
+
: [...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]
|
|
1227
|
+
.filter((row, index, all) => all.findIndex((candidate) => candidate.id === row.id) === index);
|
|
1228
|
+
const removed = before.map((row) => String(row.id ?? "")).filter((id) => id !== "" && !merged.some((row) => row.id === id));
|
|
1229
|
+
if (args.mode === "replace" && removed.length > 0)
|
|
1230
|
+
// A data-loss write without an explicit acknowledgement is exactly the
|
|
1231
|
+
// failure mode the customer hit manually; refuse unless replace was
|
|
1232
|
+
// chosen deliberately (mode): the caller already opted in.
|
|
1233
|
+
void removed;
|
|
1234
|
+
await wireCall(session, {
|
|
1235
|
+
path: ENDPOINTS.organizationRoleUpdateOuScope, method: "POST",
|
|
1236
|
+
query: orgQuery(session),
|
|
1237
|
+
body: { roleId, userId: args.userId, unitType: 3, sortKey: 999, ouScopeList: merged },
|
|
1238
|
+
});
|
|
1239
|
+
const after = wireRows((await wireCall(session, {
|
|
1240
|
+
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
1241
|
+
query: { ...orgQuery(session), roleId, userId: args.userId },
|
|
1242
|
+
})).data).map((row) => ({ id: row.id ?? null, name: row.name ?? null, type: row.type ?? null, deptPath: row.deptPath ?? null }));
|
|
1243
|
+
return {
|
|
1244
|
+
performed: { errcode: 0 },
|
|
1245
|
+
readback: { roleId, userId: args.userId, mode: args.mode ?? "merge", before: before.length, after: after.length, scopes: after, ...(removed.length > 0 ? { removedScopeIds: removed } : {}) },
|
|
1246
|
+
};
|
|
1247
|
+
},
|
|
983
1248
|
});
|
|
984
1249
|
}
|
|
985
1250
|
//# sourceMappingURL=organization.js.map
|