| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869 |
- /** Server only. An aibot sender is a directory UserID only after an explicit mapping. */
- export function createWecomDirectory({ config, hash, canonical, fetchImpl = fetch, now = () => Date.now() }) {
- const fail = (code) => { throw Object.assign(new Error(code), { code }); };
- const text = (v) => typeof v === 'string' && v.trim() && v.length <= 200 ? v.trim() : null;
- const list = (v) => Array.isArray(v) ? v : [];
- let token = null, tokenUntil = 0;
- const requests = new Map();
- async function json(url) {
- let response;
- try { response = await fetchImpl(url, { redirect: 'error', signal: AbortSignal.timeout(10000) }); }
- catch { fail('WECOM_DIRECTORY_UNAVAILABLE'); }
- if (!response.ok) fail('WECOM_DIRECTORY_UNAVAILABLE');
- let body;
- try { body = await response.json(); } catch { fail('WECOM_DIRECTORY_INVALID_RESPONSE'); }
- if (body?.errcode !== 0) fail(body?.errcode === 60011 || body?.errcode === 48002 ? 'WECOM_DIRECTORY_PERMISSION_REQUIRED' : 'WECOM_DIRECTORY_API_ERROR');
- return body;
- }
- async function accessToken() {
- if (token && tokenUntil > now()) return token;
- const body = await json('https://qyapi.weixin.qq.com/cgi-bin/gettoken?' + new URLSearchParams({ corpid: config.corpId, corpsecret: config.secret }));
- if (typeof body.access_token !== 'string' || !body.access_token || body.access_token.length > 8192 || !Number.isFinite(body.expires_in) || body.expires_in < 60) fail('WECOM_DIRECTORY_INVALID_RESPONSE');
- token = body.access_token; tokenUntil = now() + (body.expires_in - 30) * 1000;
- return token;
- }
- const accountKey = (account) => {
- if (account?.platform !== 'wecom' || account.identifierType !== 'wecom_aibot_sender' || !text(account.scopeId) || !text(account.accountId)) return null;
- return hash(canonical(['wecom_aibot_sender', account.scopeId, account.accountId]));
- };
- function mapping(account) {
- if (!accountKey(account)) fail('WECOM_SENDER_IDENTITY_REQUIRED');
- if (config?.enabled !== true || !text(config.corpId) || !text(config.secret)) fail('WECOM_DIRECTORY_NOT_CONFIGURED');
- const bot = config.bots?.[account.scopeId];
- if (!bot || bot.mappingVerified !== true) fail('WECOM_SENDER_MAPPING_REQUIRED');
- // Never guess an ID type from its prefix or try a bot-scoped identifier against the directory.
- const userId = bot.senderIdType === 'directory_userid' ? account.accountId : bot.senderIdType === 'mapped' ? bot.userIds?.[account.accountId] : null;
- if (!text(userId)) fail('WECOM_SENDER_MAPPING_REQUIRED');
- return userId;
- }
- async function resolve(account) {
- const userId = mapping(account), cacheKey = accountKey(account);
- if (requests.has(cacheKey)) return requests.get(cacheKey);
- const pending = (async () => {
- const access = await accessToken();
- const user = await json('https://qyapi.weixin.qq.com/cgi-bin/user/get?' + new URLSearchParams({ access_token: access, userid: userId }));
- if (user.userid !== userId || !text(user.name) || user.status !== 1) fail('WECOM_DIRECTORY_MEMBER_UNAVAILABLE');
- let departmentNames = [];
- if (list(user.department).length) {
- try {
- const departments = await json('https://qyapi.weixin.qq.com/cgi-bin/department/list?' + new URLSearchParams({ access_token: access }));
- const byId = new Map(list(departments.department).map((d) => [d.id, text(d.name)]));
- departmentNames = list(user.department).map((id) => byId.get(id)).filter(Boolean);
- } catch { /* Optional department access must not hide a verified member name. */ }
- }
- return {
- version: 'ksf.wecom-employee.v1', key: 'employee:' + hash(canonical([config.corpId, userId])),
- name: text(user.name), displayName: text(user.alias) || text(user.name),
- departmentNames: [...new Set(departmentNames)], position: text(user.position),
- organizationName: text(config.organizationName), source: 'wecom_directory', nameVerified: true,
- verifiedAt: new Date(now()).toISOString(),
- };
- })();
- requests.set(cacheKey, pending);
- try { return await pending; } catch (e) { requests.delete(cacheKey); throw e; }
- }
- const expectedEmployeeKey = (account) => {
- try { return 'employee:' + hash(canonical([config.corpId, mapping(account)])); } catch { return null; }
- };
- return { accountKey, resolve, expectedEmployeeKey };
- }
|