@stalfh233/omc-cli 0.6.0 → 0.6.2
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/dist/cli.js +28 -2
- package/dist/lanes/organization.js +145 -25
- package/package.json +3 -3
- package/skills/omc-backend-online-coding.md +77 -3
package/dist/cli.js
CHANGED
|
@@ -749,8 +749,34 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
749
749
|
}, transport);
|
|
750
750
|
}
|
|
751
751
|
const result = { status: "ok", artifactId: id, sha256: artifact.sha256, content };
|
|
752
|
-
if (Buffer.byteLength(JSON.stringify(result), "utf8") > transport.maxOutputBytes)
|
|
753
|
-
|
|
752
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") > transport.maxOutputBytes) {
|
|
753
|
+
// 2026-11-04 r70: `--full` used to fall back to the same bare `fetch`
|
|
754
|
+
// pointer it had just been asked to bypass — re-running `--full` re-hits
|
|
755
|
+
// the same ceiling, so a caller that genuinely needs the body loops
|
|
756
|
+
// forever (observed live: three consecutive `--full` calls returned the
|
|
757
|
+
// same id with bytes 90K→151K→227K of pointer metadata). Say what
|
|
758
|
+
// actually happened and give the two projections that DO work.
|
|
759
|
+
const record = content && typeof content === "object" && !Array.isArray(content) ? content : undefined;
|
|
760
|
+
const keys = record
|
|
761
|
+
? Object.entries(record)
|
|
762
|
+
.map(([key, value]) => ({ key, bytes: Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8") }))
|
|
763
|
+
.sort((left, right) => right.bytes - left.bytes)
|
|
764
|
+
.slice(0, 3)
|
|
765
|
+
.map((entry) => entry.key)
|
|
766
|
+
: [];
|
|
767
|
+
return emitTransportResult({
|
|
768
|
+
status: "ok",
|
|
769
|
+
artifactId: id,
|
|
770
|
+
sha256: artifact.sha256,
|
|
771
|
+
bytes: artifact.content.length,
|
|
772
|
+
fullRejected: true,
|
|
773
|
+
reason: `整包 ${Math.round(artifact.content.length / 1024)}KB 超过本通道输出上限 ${transport.maxOutputBytes}B;--full 无法一次返回,再跑一次结果相同`,
|
|
774
|
+
...(keys.length > 0 ? { keysCommand: `omc artifact get ${id} --keys ${keys.join(",")}`, sampledKeys: keys } : {}),
|
|
775
|
+
jqCommand: `omc artifact get ${id} --jq '<path>'`,
|
|
776
|
+
raiseCeilingCommand: `omc artifact get ${id} --full --max-output-bytes ${Math.max(artifact.content.length + 1024, transport.maxOutputBytes * 2)}`,
|
|
777
|
+
hint: "二选一:用 --jq/--keys 取你真正需要的那部分(推荐),或显式抬高 --max-output-bytes",
|
|
778
|
+
}, transport);
|
|
779
|
+
}
|
|
754
780
|
return emitTransportResult(result, transport);
|
|
755
781
|
}
|
|
756
782
|
catch (error) {
|
|
@@ -74,6 +74,83 @@ function deptTypeCode(value) {
|
|
|
74
74
|
return code;
|
|
75
75
|
}
|
|
76
76
|
const orgQuery = (session) => ({ corpId: session.enterpriseId() });
|
|
77
|
+
/**
|
|
78
|
+
* Read the role tree (`/role/list?expandAll=true`) and flatten it, keeping the
|
|
79
|
+
* **id** alongside code/name.
|
|
80
|
+
*
|
|
81
|
+
* Why this exists (r70, live 1.1.25): `org.role.list`'s compact projection emits
|
|
82
|
+
* `code|name` pairs and drops the id, while `add_role` / `add_role_user` /
|
|
83
|
+
* `update_ouscope` all want the id. Worse, the platform stores a wrong
|
|
84
|
+
* `roleGroupId` verbatim (`h_org_role.groupId` is not an FK): passing the group
|
|
85
|
+
* *code* that the read face shows yields errcode 0 and an orphan role — the row
|
|
86
|
+
* exists, `role/users` finds it, `role/list` never returns it, `role/get`
|
|
87
|
+
* answers 50000. Resolving code→id here removes the trap for every caller.
|
|
88
|
+
*/
|
|
89
|
+
async function readRoleTree(session) {
|
|
90
|
+
const data = (await wireCall(session, {
|
|
91
|
+
path: ENDPOINTS.organizationRoleList,
|
|
92
|
+
query: { corpId: session.enterpriseId(), expandAll: "true" },
|
|
93
|
+
})).data;
|
|
94
|
+
const groups = [];
|
|
95
|
+
const roles = [];
|
|
96
|
+
const childKeys = ["children", "roles", "childRoles", "childList", "list"];
|
|
97
|
+
const childrenOf = (row) => {
|
|
98
|
+
for (const key of childKeys) {
|
|
99
|
+
const value = row[key];
|
|
100
|
+
if (Array.isArray(value))
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
};
|
|
105
|
+
const walk = (rows) => {
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
const kids = childrenOf(row);
|
|
108
|
+
// A container carries group===true, or has children and no groupId; a leaf
|
|
109
|
+
// role always carries groupId (mirrors flattenRoleTree's rule).
|
|
110
|
+
const hasGroupId = row.groupId !== undefined && row.groupId !== null;
|
|
111
|
+
const isGroup = row.group === true || (!hasGroupId && kids.length > 0);
|
|
112
|
+
if (!isGroup) {
|
|
113
|
+
const id = String(row.id ?? "");
|
|
114
|
+
if (id !== "")
|
|
115
|
+
roles.push({ id, code: String(row.code ?? ""), name: String(row.name ?? "") });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
groups.push({ id: String(row.id ?? ""), code: String(row.code ?? ""), name: String(row.name ?? "") });
|
|
119
|
+
if (kids.length > 0)
|
|
120
|
+
walk(kids);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
walk(wireRows(data));
|
|
124
|
+
return { groups, roles };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve a caller-supplied role reference (32-hex id, or the `code` / `name`
|
|
128
|
+
* that `org.role.list` actually shows) to the platform role id. A reference that
|
|
129
|
+
* matches nothing is refused with the list of real roles — never written blind.
|
|
130
|
+
*/
|
|
131
|
+
async function resolveRoleRef(session, args, tool) {
|
|
132
|
+
const requested = args.roleId ?? args.roleCode;
|
|
133
|
+
if (!requested)
|
|
134
|
+
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool, message: "需要 roleId 或 roleCode(org.role.list 的 roles 码表给的是 code)" });
|
|
135
|
+
// An explicit roleCode must resolve — never forward it as an id.
|
|
136
|
+
if (args.roleId !== undefined && args.roleCode === undefined && /^[0-9a-f]{32}$/i.test(requested))
|
|
137
|
+
return requested;
|
|
138
|
+
const { roles } = await readRoleTree(session);
|
|
139
|
+
const match = roles.find((role) => role.code === requested) ?? roles.find((role) => role.name === requested);
|
|
140
|
+
if (match)
|
|
141
|
+
return match.id;
|
|
142
|
+
// Not a known code/name. `roleId` may still be a valid id the tree does not
|
|
143
|
+
// surface (e.g. a role in another corp); forward it and let the platform
|
|
144
|
+
// judge, rather than breaking callers that hold an id we cannot see.
|
|
145
|
+
if (args.roleId !== undefined)
|
|
146
|
+
return requested;
|
|
147
|
+
throw new ToolInputError({
|
|
148
|
+
status: "error",
|
|
149
|
+
blocker: "tool-input-invalid",
|
|
150
|
+
tool,
|
|
151
|
+
message: `角色 ${requested} 不存在。可用角色:${roles.slice(0, 30).map((role) => `${role.code}(${role.name})`).join(";")}`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
77
154
|
const roleUserId = (row) => String(row.id ?? row.userId ?? row.recId ?? "");
|
|
78
155
|
async function memberRows(session, roleId) {
|
|
79
156
|
const data = (await wireCall(session, {
|
|
@@ -226,22 +303,43 @@ export function registerOrganizationLane(tools) {
|
|
|
226
303
|
},
|
|
227
304
|
};
|
|
228
305
|
});
|
|
229
|
-
writeTool(tools, "org.role.create", LANE, SCOPES.roleCreate, "创建角色(界面「分组」;`POST organization/role/add_role`)。返回 roleId;code 创建时 null、随后补 SYS<时间戳>,要稳定编码就传 code
|
|
306
|
+
writeTool(tools, "org.role.create", LANE, SCOPES.roleCreate, "创建角色(界面「分组」;`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,并在创建后回读可见性。", {
|
|
230
307
|
roleName: z.string().min(1),
|
|
231
|
-
|
|
308
|
+
roleGroupCode: z.string().min(1).optional().describe("所属角色组的 code(org.role.list 的 groups 码表,如 SYS_1000000)或名称。**推荐**;与 roleGroupId 二选一"),
|
|
309
|
+
roleGroupId: z.string().min(1).optional().describe("所属角色组 id(32 位 hex,只能从 artifact get <role.list 的 detailArtifactId> --jq groups 取到)。误传组 code 时工具会归一成真实 id"),
|
|
232
310
|
roleGroupName: z.string().optional(),
|
|
233
311
|
roleType: z.enum(["SYS", "CUSTOMIZE", "OTHER"]).optional().describe("默认 SYS(平台会把角色统一落成 SYS,见 RoleHelper.createRoleModel)"),
|
|
234
312
|
code: z.string().optional(),
|
|
235
313
|
remarks: z.string().optional(),
|
|
236
314
|
corpId: z.string().min(1).optional().describe("组织 corpId,默认当前登录企业;传 \"other\" 表示非组织分组角色"),
|
|
237
315
|
}, async (session, args) => {
|
|
316
|
+
if (!args.roleGroupCode && !args.roleGroupId) {
|
|
317
|
+
throw new ToolInputError({ message: "必须给 roleGroupCode(推荐)或 roleGroupId;先 org.role.list 取 groups 码表" });
|
|
318
|
+
}
|
|
319
|
+
const { groups } = await readRoleTree(session);
|
|
320
|
+
if (groups.length === 0) {
|
|
321
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: "org.role.list 未解析出任何角色组;先确认组织树可读,不要盲建角色" });
|
|
322
|
+
}
|
|
323
|
+
const requested = args.roleGroupId ?? args.roleGroupCode;
|
|
324
|
+
const match = groups.find((group) => group.id === requested) ??
|
|
325
|
+
groups.find((group) => group.code === requested) ??
|
|
326
|
+
groups.find((group) => group.name === requested);
|
|
327
|
+
// A code/name that resolves to nothing is a definite typo → refuse.
|
|
328
|
+
// A 32-hex id absent from the tree may be a group created moments ago in
|
|
329
|
+
// this same session; let it through and rely on the visibility backstop.
|
|
330
|
+
if (!match && args.roleGroupCode) {
|
|
331
|
+
throw new ToolInputError({
|
|
332
|
+
message: `角色组 ${requested} 不存在。可用组:${groups.map((group) => `${group.code}(id=${group.id},${group.name})`).join(";")}`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const roleGroupId = match?.id ?? String(requested);
|
|
238
336
|
const corpId = args.corpId ?? session.enterpriseId();
|
|
239
337
|
const data = (await wireCall(session, {
|
|
240
338
|
path: ENDPOINTS.organizationRoleAdd, method: "POST",
|
|
241
339
|
query: orgQuery(session),
|
|
242
340
|
body: {
|
|
243
341
|
roleName: args.roleName,
|
|
244
|
-
roleGroupId
|
|
342
|
+
roleGroupId,
|
|
245
343
|
...(args.roleGroupName ? { roleGroupName: args.roleGroupName } : {}),
|
|
246
344
|
roleType: args.roleType ?? "SYS",
|
|
247
345
|
corpId,
|
|
@@ -252,15 +350,29 @@ export function registerOrganizationLane(tools) {
|
|
|
252
350
|
const roleId = String(data?.roleId ?? data?.id ?? "");
|
|
253
351
|
if (!roleId)
|
|
254
352
|
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: "平台未返回 roleId;先 org.role.list 回读确认" });
|
|
353
|
+
// Backstop against the silent-orphan trap: re-read the tree and report the
|
|
354
|
+
// truth instead of reporting success for a role nobody can see.
|
|
355
|
+
const visible = (await readRoleTree(session)).roles.some((role) => role.id === roleId);
|
|
255
356
|
const readbackCorpId = data?.corpId ?? null;
|
|
357
|
+
const warnings = [];
|
|
358
|
+
if (!visible) {
|
|
359
|
+
warnings.push(`角色 ${roleId} 创建后不在 org.role.list 任何组下(孤儿角色):groupId 很可能没落到真实组 id。请 org.role.delete 后按 roleGroupCode 重建`);
|
|
360
|
+
}
|
|
361
|
+
if (readbackCorpId !== null && String(readbackCorpId) !== corpId) {
|
|
362
|
+
warnings.push(`平台回读 corpId=${String(readbackCorpId)},与请求的 ${corpId} 不一致`);
|
|
363
|
+
}
|
|
256
364
|
return {
|
|
257
365
|
performed: { errcode: 0 },
|
|
258
366
|
readback: {
|
|
259
367
|
roleId,
|
|
260
368
|
roleName: data?.roleName ?? args.roleName,
|
|
261
369
|
code: data?.code ?? null,
|
|
370
|
+
roleGroupId,
|
|
371
|
+
roleGroupCode: match?.code ?? null,
|
|
372
|
+
roleGroupName: match?.name ?? null,
|
|
373
|
+
visibleInRoleList: visible,
|
|
262
374
|
corpId: readbackCorpId,
|
|
263
|
-
...(
|
|
375
|
+
...(warnings.length > 0 ? { warning: warnings.join(";") } : {}),
|
|
264
376
|
},
|
|
265
377
|
};
|
|
266
378
|
});
|
|
@@ -634,21 +746,23 @@ export function registerOrganizationLane(tools) {
|
|
|
634
746
|
return { userId: String(args.userId), count: departments.length, departments };
|
|
635
747
|
}),
|
|
636
748
|
}));
|
|
637
|
-
writeTool(tools, "org.role.delete", LANE, SCOPES.roleDelete, "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25):默认先读成员数、非空即拒绝并给出人数;`force:true`
|
|
638
|
-
roleId: z.string().min(1),
|
|
749
|
+
writeTool(tools, "org.role.delete", LANE, SCOPES.roleDelete, "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25):默认先读成员数、非空即拒绝并给出人数;`force:true` 才会先逐人移出再删。**可用 roleCode 代替 roleId**(org.role.list 的码表只有 code)。", {
|
|
750
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
751
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表,如 qc_manager)或名称"),
|
|
639
752
|
force: z.boolean().optional().describe("true = 先移出全部成员再删角色"),
|
|
640
753
|
}, async (session, args) => {
|
|
641
|
-
const
|
|
754
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.delete");
|
|
755
|
+
const rows = await memberRows(session, roleId);
|
|
642
756
|
if (rows.length > 0 && args.force !== true)
|
|
643
757
|
throw new ToolRefusalError({
|
|
644
|
-
status: "refused", blocker: "role-has-members", roleId
|
|
645
|
-
nextCommand: `omc call org.role.delete --args '${JSON.stringify({ roleId
|
|
758
|
+
status: "refused", blocker: "role-has-members", roleId, members: rows.length,
|
|
759
|
+
nextCommand: `omc call org.role.delete --args '${JSON.stringify({ roleId, force: true })}'`,
|
|
646
760
|
message: `角色下还有 ${rows.length} 名成员,平台拒绝删除(errmsg:角色下已经包含用户数据)。先 org.role.removeUsers,或传 force:true 让工具移出后再删。`,
|
|
647
761
|
});
|
|
648
762
|
if (rows.length > 0)
|
|
649
|
-
await removeMembers(session,
|
|
650
|
-
await wireCall(session, { path: ENDPOINTS.organizationRoleDelete, query: { ...orgQuery(session), ids:
|
|
651
|
-
return { performed: { errcode: 0 }, readback: { roleId
|
|
763
|
+
await removeMembers(session, roleId, rows.map(roleUserId));
|
|
764
|
+
await wireCall(session, { path: ENDPOINTS.organizationRoleDelete, query: { ...orgQuery(session), ids: roleId } });
|
|
765
|
+
return { performed: { errcode: 0 }, readback: { roleId, deleted: true, removedMembers: rows.length } };
|
|
652
766
|
});
|
|
653
767
|
writeTool(tools, "org.roleGroup.delete", LANE, SCOPES.roleGroupDelete, "删除角色组(`GET organization/role/strike_out_role_group?ids=`)。平台**拒绝删除仍含角色的组**(`-1 角色组下已经包含角色数据!`):默认先数角色、非空即拒绝;`force:true` 会逐个删角色(含其成员)再删组。", {
|
|
654
768
|
roleGroupId: z.string().min(1),
|
|
@@ -730,7 +844,8 @@ export function registerOrganizationLane(tools) {
|
|
|
730
844
|
};
|
|
731
845
|
});
|
|
732
846
|
writeTool(tools, "org.role.addUsers", LANE, SCOPES.addUsers, "加成员并(可选)一次设好管理范围(`add_role_user` + 逐人 `update_ouscope`,一个工具调用)。users[].id 必须是 recId(org.user.search 的 id);传 `scopes` 会应用到本次新增的每个人。**平台的 add_role_user 虽然收 `ouScope` 但会忽略它**(实测 1.1.25,回读仍为空),所以范围必须单独写——工具内部已按顺序完成。加完回读成员与范围。", {
|
|
733
|
-
roleId: z.string().min(1),
|
|
847
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
848
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称——省掉取 artifact 找 id"),
|
|
734
849
|
users: z.array(z.object({ id: z.string().min(1).describe("用户记录 id(recId)"), name: z.string().min(1) })).min(1),
|
|
735
850
|
scopes: z.array(z.object({
|
|
736
851
|
id: z.string().min(1).describe("部门 id"),
|
|
@@ -744,15 +859,16 @@ export function registerOrganizationLane(tools) {
|
|
|
744
859
|
const users = args.users ?? args.userIds ?? [];
|
|
745
860
|
if (users.length === 0)
|
|
746
861
|
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "org.role.addUsers", message: "需要 users(或 userIds)至少一人" });
|
|
862
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.addUsers");
|
|
747
863
|
await wireCall(session, {
|
|
748
864
|
path: ENDPOINTS.organizationRoleAddUser, method: "POST",
|
|
749
865
|
query: orgQuery(session),
|
|
750
|
-
body: { roleId
|
|
866
|
+
body: { roleId, sortKey: args.sortKey ?? 999, userIds: users.map((user) => ({ id: user.id, name: user.name, type: 3, unitType: "3" })) },
|
|
751
867
|
});
|
|
752
868
|
const readback = async () => {
|
|
753
869
|
const data = (await wireCall(session, {
|
|
754
870
|
path: ENDPOINTS.organizationRoleUsers,
|
|
755
|
-
query: { ...orgQuery(session), roleId:
|
|
871
|
+
query: { ...orgQuery(session), roleId: roleId, page: "0", size: "500" },
|
|
756
872
|
})).data;
|
|
757
873
|
return Array.isArray(data?.content) ? data.content : wireRows(data);
|
|
758
874
|
};
|
|
@@ -766,11 +882,11 @@ export function registerOrganizationLane(tools) {
|
|
|
766
882
|
for (const id of added) {
|
|
767
883
|
await wireCall(session, {
|
|
768
884
|
path: ENDPOINTS.organizationRoleUpdateOuScope, method: "POST", query: orgQuery(session),
|
|
769
|
-
body: { roleId:
|
|
885
|
+
body: { roleId: roleId, userId: id, unitType: 3, sortKey: args.sortKey ?? 999, ouScopeList },
|
|
770
886
|
});
|
|
771
887
|
const scope = wireRows((await wireCall(session, {
|
|
772
888
|
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
773
|
-
query: { ...orgQuery(session), roleId:
|
|
889
|
+
query: { ...orgQuery(session), roleId: roleId, userId: id },
|
|
774
890
|
})).data);
|
|
775
891
|
scopeResults.push({ userId: id, scopes: scope.length });
|
|
776
892
|
}
|
|
@@ -778,7 +894,7 @@ export function registerOrganizationLane(tools) {
|
|
|
778
894
|
return {
|
|
779
895
|
performed: { errcode: 0 },
|
|
780
896
|
readback: {
|
|
781
|
-
roleId:
|
|
897
|
+
roleId: roleId, added,
|
|
782
898
|
...(missing.length > 0 ? { missing, hint: "这些 recId 未出现在成员列表:确认 id 是 recId、用户未离职/禁用" } : {}),
|
|
783
899
|
...(scopeResults.length > 0 ? { scopeResults } : {}),
|
|
784
900
|
// ouScope 是流程角色参与人解析的过滤维度:平台
|
|
@@ -802,12 +918,14 @@ export function registerOrganizationLane(tools) {
|
|
|
802
918
|
};
|
|
803
919
|
});
|
|
804
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)。只移出角色,不删用户。", {
|
|
805
|
-
roleId: z.string().min(1),
|
|
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 码表)或名称"),
|
|
806
923
|
ids: z.array(z.string().min(1)).min(1).describe("要移出的成员 recId 列表"),
|
|
807
924
|
}, async (session, args) => {
|
|
925
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.removeUsers");
|
|
808
926
|
await wireCall(session, {
|
|
809
927
|
path: ENDPOINTS.organizationRoleRemoveUser, method: "POST", query: orgQuery(session),
|
|
810
|
-
body: { roleId
|
|
928
|
+
body: { roleId, userIds: args.ids },
|
|
811
929
|
});
|
|
812
930
|
const data = (await wireCall(session, {
|
|
813
931
|
path: ENDPOINTS.organizationRoleUsers,
|
|
@@ -818,7 +936,8 @@ export function registerOrganizationLane(tools) {
|
|
|
818
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 } };
|
|
819
937
|
});
|
|
820
938
|
writeTool(tools, "org.role.setUserScope", LANE, SCOPES.setScope, "设置成员管理范围(`POST organization/role/update_ouscope`)。平台覆盖语义、无回滚:默认读旧值合并(mode:\"merge\"),mode:\"replace\" 才整体替换。unitType 传数字 3(addUsers 里是字符串 \"3\")。节点 {id,name,type:\"1\",deptPath}。", {
|
|
821
|
-
roleId: z.string().min(1),
|
|
939
|
+
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
940
|
+
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称"),
|
|
822
941
|
userId: z.string().min(1).describe("成员 recId"),
|
|
823
942
|
scopes: z.array(z.object({
|
|
824
943
|
id: z.string().min(1).describe("部门 id"),
|
|
@@ -832,9 +951,10 @@ export function registerOrganizationLane(tools) {
|
|
|
832
951
|
mode: z.enum(["merge", "replace"]).optional().describe("默认 merge(读旧值合并);replace = 覆盖整个范围"),
|
|
833
952
|
dryRun: z.boolean().optional(),
|
|
834
953
|
}, async (session, args) => {
|
|
954
|
+
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.setUserScope");
|
|
835
955
|
const before = wireRows((await wireCall(session, {
|
|
836
956
|
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
837
|
-
query: { ...orgQuery(session), roleId
|
|
957
|
+
query: { ...orgQuery(session), roleId, userId: args.userId },
|
|
838
958
|
})).data);
|
|
839
959
|
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 } : {}) }));
|
|
840
960
|
const merged = args.mode === "replace"
|
|
@@ -850,15 +970,15 @@ export function registerOrganizationLane(tools) {
|
|
|
850
970
|
await wireCall(session, {
|
|
851
971
|
path: ENDPOINTS.organizationRoleUpdateOuScope, method: "POST",
|
|
852
972
|
query: orgQuery(session),
|
|
853
|
-
body: { roleId
|
|
973
|
+
body: { roleId, userId: args.userId, unitType: 3, sortKey: 999, ouScopeList: merged },
|
|
854
974
|
});
|
|
855
975
|
const after = wireRows((await wireCall(session, {
|
|
856
976
|
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
857
|
-
query: { ...orgQuery(session), roleId
|
|
977
|
+
query: { ...orgQuery(session), roleId, userId: args.userId },
|
|
858
978
|
})).data).map((row) => ({ id: row.id ?? null, name: row.name ?? null, type: row.type ?? null, deptPath: row.deptPath ?? null }));
|
|
859
979
|
return {
|
|
860
980
|
performed: { errcode: 0 },
|
|
861
|
-
readback: { roleId
|
|
981
|
+
readback: { roleId, userId: args.userId, mode: args.mode ?? "merge", before: before.length, after: after.length, scopes: after, ...(removed.length > 0 ? { removedScopeIds: removed } : {}) },
|
|
862
982
|
};
|
|
863
983
|
});
|
|
864
984
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
55
|
+
"@stalfh233/omc-fake-platform": "0.6.2",
|
|
56
56
|
"commander": "14.0.3",
|
|
57
|
-
"omc-core": "0.6.
|
|
57
|
+
"omc-core": "0.6.2",
|
|
58
58
|
"proper-lockfile": "4.1.2",
|
|
59
59
|
"write-file-atomic": "6.0.0",
|
|
60
60
|
"zod": "^3.23.8",
|
|
@@ -35,6 +35,81 @@ AI-Platform 1.1.25(8.7 时代)已实测(2026-11-03)。
|
|
|
35
35
|
|
|
36
36
|
先按 cpdocs 的落点判据定「配置 / 二开」;判到二开时,再用上表在两种 Java 落点间选。
|
|
37
37
|
|
|
38
|
+
## 0.1 在线编码的能力边界(实测,1.1.25)
|
|
39
|
+
|
|
40
|
+
在线编码**不是一个工程,是单文件 Handler**。平台契约就是「一条规则 = 一个 `content`
|
|
41
|
+
字符串 = 一个 `.java` 文件」,没有多文件、没有模块、看不见你的工程代码。这个边界
|
|
42
|
+
决定了很多事能不能做。
|
|
43
|
+
|
|
44
|
+
### 源码形态
|
|
45
|
+
|
|
46
|
+
| 能做 | 不能做 |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **1 个 public 类**(必须 `schemaCode+ruleCode+Handler`) | 第 2 个 public 类(`should be declared in a file named X.java`) |
|
|
49
|
+
| 多个 package-private 顶层类(实测 5 个无问题) | — |
|
|
50
|
+
| 内部类 / 静态嵌套 / 匿名类 / lambda / stream | 顶层 `static class` |
|
|
51
|
+
| 同文件 `enum` / `interface` / `@interface`(非 public) | — |
|
|
52
|
+
| `import a.b.*` 通配符、`import static` | — |
|
|
53
|
+
|
|
54
|
+
### 语言级别:Java 8
|
|
55
|
+
|
|
56
|
+
| 语法 | 结果 |
|
|
57
|
+
|---|---|
|
|
58
|
+
| lambda / stream / 匿名类 | ✅ |
|
|
59
|
+
| `var` | ❌ `cannot find symbol` |
|
|
60
|
+
| `record` | ❌ `class, interface, or enum expected` |
|
|
61
|
+
| 文本块 `"""` | ❌ `';' expected` |
|
|
62
|
+
| switch 表达式 `case 1 ->` | ❌ |
|
|
63
|
+
|
|
64
|
+
**坑**:平台上挂着 Lombok 注解处理器但**它初始化失败**
|
|
65
|
+
(`NoClassDefFoundError: com/sun/tools/javac/processing/JavacProcessingEnvironment`),
|
|
66
|
+
所以用 Java 8 之后的语法时,错误里会先出现一坨 Lombok 异常,真正的
|
|
67
|
+
`cannot find symbol` 排在后面。**看错误先看最后一条**,别被 Lombok 噪音带偏。
|
|
68
|
+
Lombok 的代码生成也**不可用**(`@Data` 生成的方法找不到),老老实实手写 getter/setter
|
|
69
|
+
和 `LoggerFactory.getLogger(...)`。
|
|
70
|
+
|
|
71
|
+
### 可用依赖
|
|
72
|
+
|
|
73
|
+
| 可用 | 不可用 |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `java.lang/math/util/io` + wildcard / static import | Jakarta EE(`jakarta.*`) |
|
|
76
|
+
| `engine.open.*`(OpenEngine / BizObject / BizObjectQuery / FilterExpression) | **大部分 `engine.api.facade/model.*`**(包不存在) |
|
|
77
|
+
| `engine.ErrorCode`、`engine.api.historical.*` | **`engine.service.*`**(平台内部 Service 不可见) |
|
|
78
|
+
| commons-lang3 / fastjson / guava / hutool / slf4j | **工程自有包**(`app.modules.*` 编不过) |
|
|
79
|
+
| `javax.annotation` / `javax.servlet` | 额外第三方 jar(不在 launcher 类路径上) |
|
|
80
|
+
| Spring 注解(能编译) | **Spring 运行时**(规则每次重新实例化,无容器上下文;不要注册 Bean) |
|
|
81
|
+
|
|
82
|
+
**别按平台的「补全白名单」写代码**:白名单列了 `engine.api.*` / `engine.domain.*` /
|
|
83
|
+
`engine.service.*`,但**白名单 ≠ javac 类路径**——白名单里很多类实际编不过。以
|
|
84
|
+
`engine.open.*` 为准,拿不准就先 `programming.compile` 试一次(只读,不落库)。
|
|
85
|
+
|
|
86
|
+
### 运行时约束
|
|
87
|
+
|
|
88
|
+
| 项 | 值 | 说明 |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| 执行超时 | **600s**(默认) | `cloudpivot.online-develop.thread-overtime` |
|
|
91
|
+
| 最大栈深 | 50 | 递归要小心 |
|
|
92
|
+
| 字符串常量 | ≤ 64KB | 超过报 `constant string too long`;文件本身几十 KB 没问题 |
|
|
93
|
+
| 编译产物目录 | `<launcher>/inject_code_dir` | 无写权限 → publish 成功但执行 50000 |
|
|
94
|
+
| 生命周期 | 每次执行重新实例化 | 无状态、不能常驻、不能注册 Bean |
|
|
95
|
+
| 并发/事务 | 跑在平台保存调用内 | 别在里面起线程或长事务 |
|
|
96
|
+
|
|
97
|
+
### 必须退回本地扩展包的场景
|
|
98
|
+
|
|
99
|
+
出现任一条就**不是**在线编码能解决的,别硬塞:
|
|
100
|
+
|
|
101
|
+
1. 要对外开 **REST 接口**
|
|
102
|
+
2. 要接 **SPI / 平台扩展点**(单据号、消息、流程监听、适配器、模型校验…)
|
|
103
|
+
3. 要 **Spring 托管 / 拦截器 / 全局异常 / 多 Bean 协作**
|
|
104
|
+
4. 要**复用工程内自己的类或工具库**(在线编码看不见它们)
|
|
105
|
+
5. 要<strong>引额外第三方 jar</strong>(launcher 类路径上没有)
|
|
106
|
+
6. 要 **Java > 8 语法 / Lombok 代码生成**
|
|
107
|
+
7. 要**多条规则共享同一套工具方法**——在线编码没有跨规则复用,只能各文件复制;
|
|
108
|
+
一旦出现「三处以上重复」,转本地扩展包
|
|
109
|
+
|
|
110
|
+
反过来,**留在在线编码**:某个模型 Create/Update/Delete/Load/GetList/自定义规则时,
|
|
111
|
+
用 OpenEngine 做字段改写、补数、简单联动——publish 即生效。
|
|
112
|
+
|
|
38
113
|
## 1. 两种用法:单条 vs 工作区
|
|
39
114
|
|
|
40
115
|
### 单条(快速改一条规则)
|
|
@@ -136,9 +211,8 @@ cpdocs 提供**平台知识与规范**,本 CLI 提供**执行面**。已安装
|
|
|
136
211
|
推荐循环:**需求 → cpdocs 定落点与 API → `programming.get` 取模板 → 按 cpdocs 规范写 →
|
|
137
212
|
`compile` → `apply --publish` → `programming test` 断言 → (跨模型/对外接口才升到二开扩展包)**。
|
|
138
213
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
(规则每次执行重新实例化,无 Spring 上下文),也别把规则类注册成 Bean(内存泄漏)。
|
|
214
|
+
在线编码里能用什么依赖、什么语法、什么必须退回扩展包——见 [§0.1 能力边界](#01-在线编码的能力边界实测-1125)。
|
|
215
|
+
拿不准某个类能不能用,先 `programming.compile` 试一次(只读不落库),比猜快。
|
|
142
216
|
|
|
143
217
|
## 5. 典型 handler 骨架(已验证可编译可运行)
|
|
144
218
|
|