WAPI 固化企微 API 能力目录与读取口径
这个提交包含在:
@@ -182,6 +182,7 @@ function parseDateOnly(value) {
|
||||
function sanitizeWecomPayload(payload) {
|
||||
const base = sanitizeWecomResponse(payload) ?? {};
|
||||
if (Array.isArray(payload?.userlist)) base.userCount = payload.userlist.length;
|
||||
if (Array.isArray(payload?.dept_user)) base.deptUserCount = payload.dept_user.length;
|
||||
if (Array.isArray(payload?.department)) base.departmentCount = payload.department.length;
|
||||
if (payload?.userid != null) base.useridPrefix = maskExternalUserId(payload.userid);
|
||||
if (payload?.enable != null) base.enable = payload.enable;
|
||||
@@ -289,21 +290,15 @@ async function getTokenFromCandidates({ config, candidates, timeoutMs, fetchImpl
|
||||
function contactTokenCandidates(config) {
|
||||
return [
|
||||
{ source: "WECOM_CONTACT_SECRET", secret: config?.wecomContactSecret },
|
||||
{ source: "WECOM_AGENT_SECRET_FALLBACK", secret: config?.wecomAgentSecret },
|
||||
];
|
||||
}
|
||||
|
||||
function hrTokenCandidates(config) {
|
||||
return [
|
||||
{ source: "WECOM_HR_ASSISTANT_SECRET", secret: config?.wecomHrAssistantSecret },
|
||||
{ source: "WECOM_AGENT_SECRET_FALLBACK", secret: config?.wecomAgentSecret },
|
||||
{ source: "WECOM_AGENT_SECRET", secret: config?.wecomAgentSecret },
|
||||
];
|
||||
}
|
||||
|
||||
function canFallbackToAgentSecret(error) {
|
||||
return error instanceof WecomHrMasterSyncError && error.code === "wecom_errcode_48009";
|
||||
}
|
||||
|
||||
function normalizeDepartmentMap(payload) {
|
||||
const map = new Map();
|
||||
const departments = Array.isArray(payload?.department) ? payload.department : [];
|
||||
@@ -332,17 +327,65 @@ async function fetchDepartmentMap({ accessToken, fetchImpl, timeoutMs }) {
|
||||
return normalizeDepartmentMap(result.payload);
|
||||
}
|
||||
|
||||
async function listContactMembers({ accessToken, departmentId, limit, fetchImpl, timeoutMs }) {
|
||||
const result = await fetchWecomJson("/cgi-bin/user/list", accessToken, {
|
||||
query: { department_id: departmentId, fetch_child: 1 },
|
||||
fetchImpl,
|
||||
timeoutMs,
|
||||
});
|
||||
if (!result.ok) {
|
||||
const error = classifyWecomResult(result, "wecom_user_list_failed");
|
||||
throw new WecomHrMasterSyncError(error.code, error.message, { responseSummary: error.responseSummary });
|
||||
async function listContactMembers({ accessToken, limit, fetchImpl, timeoutMs }) {
|
||||
const membersByUserid = new Map();
|
||||
let cursor = "";
|
||||
|
||||
do {
|
||||
const result = await fetchWecomJson("/cgi-bin/user/list_id", accessToken, {
|
||||
body: {
|
||||
cursor,
|
||||
limit: Math.min(10000, Math.max(1, limit - membersByUserid.size)),
|
||||
},
|
||||
fetchImpl,
|
||||
timeoutMs,
|
||||
});
|
||||
if (!result.ok) {
|
||||
const error = classifyWecomResult(result, "wecom_user_list_id_failed");
|
||||
throw new WecomHrMasterSyncError(error.code, error.message, { responseSummary: error.responseSummary });
|
||||
}
|
||||
|
||||
const batch = Array.isArray(result.payload?.dept_user) ? result.payload.dept_user : [];
|
||||
for (const item of batch) {
|
||||
const userid = toSafeString(item?.userid, 128);
|
||||
if (!userid) continue;
|
||||
const member = membersByUserid.get(userid) || { userid, department: [] };
|
||||
if (item?.department != null && !member.department.includes(item.department)) {
|
||||
member.department.push(item.department);
|
||||
}
|
||||
membersByUserid.set(userid, member);
|
||||
if (membersByUserid.size >= limit) break;
|
||||
}
|
||||
|
||||
cursor = toSafeString(result.payload?.next_cursor, 256);
|
||||
} while (cursor && membersByUserid.size < limit);
|
||||
|
||||
const members = Array.from(membersByUserid.values()).slice(0, limit);
|
||||
const contacts = await Promise.all(members.map(async (member) => {
|
||||
const detail = await fetchOptionalContactMember({
|
||||
accessToken,
|
||||
userid: member.userid,
|
||||
fetchImpl,
|
||||
timeoutMs,
|
||||
});
|
||||
return {
|
||||
...detail,
|
||||
userid: detail.userid ?? member.userid,
|
||||
department: Array.isArray(detail.department) && detail.department.length ? detail.department : member.department,
|
||||
};
|
||||
}));
|
||||
return contacts;
|
||||
}
|
||||
|
||||
async function fetchOptionalDepartmentMap({ accessToken, fetchImpl, timeoutMs }) {
|
||||
try {
|
||||
return await fetchDepartmentMap({ accessToken, fetchImpl, timeoutMs });
|
||||
} catch (error) {
|
||||
if (error instanceof WecomHrMasterSyncError) {
|
||||
return new Map();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return (Array.isArray(result.payload?.userlist) ? result.payload.userlist : []).slice(0, limit);
|
||||
}
|
||||
|
||||
async function fetchContactMember({ accessToken, userid, fetchImpl, timeoutMs }) {
|
||||
@@ -358,6 +401,17 @@ async function fetchContactMember({ accessToken, userid, fetchImpl, timeoutMs })
|
||||
return result.payload;
|
||||
}
|
||||
|
||||
async function fetchOptionalContactMember({ accessToken, userid, fetchImpl, timeoutMs }) {
|
||||
try {
|
||||
return await fetchContactMember({ accessToken, userid, fetchImpl, timeoutMs });
|
||||
} catch (error) {
|
||||
if (error instanceof WecomHrMasterSyncError) {
|
||||
return { userid };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHrStaffInfo({ accessToken, userid, fetchImpl, timeoutMs }) {
|
||||
const result = await fetchWecomJson("/cgi-bin/hr/get_staff_info", accessToken, {
|
||||
body: { userid, get_all: true },
|
||||
@@ -424,14 +478,13 @@ function normalizeWecomHrProfile({ contact = {}, staff = {}, departmentMap = new
|
||||
}
|
||||
|
||||
async function fetchContactProfilesInput({ config, token, userids, limit, fetchImpl, timeoutMs }) {
|
||||
const departmentMap = await fetchDepartmentMap({
|
||||
const departmentMap = await fetchOptionalDepartmentMap({
|
||||
accessToken: token.accessToken,
|
||||
fetchImpl,
|
||||
timeoutMs,
|
||||
});
|
||||
const departmentId = toSafeString(config.wecomContactDefaultDepartmentId, 32) || "1";
|
||||
const contacts = userids.length
|
||||
? await Promise.all(userids.map((userid) => fetchContactMember({
|
||||
? await Promise.all(userids.map((userid) => fetchOptionalContactMember({
|
||||
accessToken: token.accessToken,
|
||||
userid,
|
||||
fetchImpl,
|
||||
@@ -439,7 +492,6 @@ async function fetchContactProfilesInput({ config, token, userids, limit, fetchI
|
||||
})))
|
||||
: await listContactMembers({
|
||||
accessToken: token.accessToken,
|
||||
departmentId,
|
||||
limit,
|
||||
fetchImpl,
|
||||
timeoutMs,
|
||||
@@ -448,55 +500,9 @@ async function fetchContactProfilesInput({ config, token, userids, limit, fetchI
|
||||
return { departmentMap, contacts, tokenSource: token.source };
|
||||
}
|
||||
|
||||
async function fetchContactProfilesInputWithFallback({ config, contactToken, userids, limit, fetchImpl, timeoutMs }) {
|
||||
try {
|
||||
return await fetchContactProfilesInput({
|
||||
config,
|
||||
token: contactToken,
|
||||
userids,
|
||||
limit,
|
||||
fetchImpl,
|
||||
timeoutMs,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!canFallbackToAgentSecret(error) || contactToken.source === "WECOM_AGENT_SECRET_FALLBACK") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const fallback = await getTokenFromCandidates({
|
||||
config,
|
||||
candidates: [{ source: "WECOM_AGENT_SECRET_FALLBACK", secret: config?.wecomAgentSecret }],
|
||||
timeoutMs,
|
||||
fetchImpl,
|
||||
});
|
||||
if (!fallback.ok) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
return await fetchContactProfilesInput({
|
||||
config,
|
||||
token: fallback,
|
||||
userids,
|
||||
limit,
|
||||
fetchImpl,
|
||||
timeoutMs,
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
if (canFallbackToAgentSecret(fallbackError)) {
|
||||
fallbackError.details = {
|
||||
...(fallbackError.details ?? {}),
|
||||
firstTokenSource: contactToken.source,
|
||||
fallbackTokenSource: fallback.source,
|
||||
};
|
||||
}
|
||||
throw fallbackError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWecomHrProfiles({ config, userids, limit, fetchImpl = fetch }) {
|
||||
const timeoutMs = config.wecomRequestTimeoutMs ?? 10000;
|
||||
const readLimit = normalizeLimit(limit);
|
||||
const contactToken = await getTokenFromCandidates({
|
||||
config,
|
||||
candidates: contactTokenCandidates(config),
|
||||
@@ -517,11 +523,11 @@ async function fetchWecomHrProfiles({ config, userids, limit, fetchImpl = fetch
|
||||
throw new WecomHrMasterSyncError(hrToken.code, hrToken.message, { missing: hrToken.missing });
|
||||
}
|
||||
|
||||
const contactInput = await fetchContactProfilesInputWithFallback({
|
||||
const contactInput = await fetchContactProfilesInput({
|
||||
config,
|
||||
contactToken,
|
||||
token: contactToken,
|
||||
userids,
|
||||
limit,
|
||||
limit: readLimit,
|
||||
fetchImpl,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户