feat: 企微通讯录选人替换手填 userid(项目负责人/总群成员/群主合作方)

- wecom-client 新增 listInternalContacts:department/list + 逐部门 user/simplelist(cursor 翻页、去重、部门名映射)
- 新 action wecom_list_internal_contacts(仅返回应用可见范围内成员)
- admin 新增 WecomUserPickerModal:单选点行填入/多选勾选合并,姓名+userid 搜索,5 分钟共享缓存
- 三处接线:项目负责人(单选)、项目总群成员(多选合并去重)、新建群主合作方(单选)
- 拉取失败(60020 等)提示手填兜底,手填路径不变
This commit is contained in:
ABAPPLO
2026-09-03 13:52:10 +08:00
parent bd80b2d7d8
commit 5430624a43
5 changed files with 559 additions and 13 deletions

View File

@@ -10,6 +10,7 @@ import {
getAppChat,
listCustomerGroupChats,
listExternalContacts,
listInternalContacts,
resolveWecomConfig,
sendAppChatMessage,
sendWecomAppMessage,
@@ -57,6 +58,56 @@ function fakeWecom(options = {}) {
invaliduser: invalid.join("|"),
});
}
if (url.pathname.endsWith("/cgi-bin/department/list")) {
if (options.departmentErrcode) {
return Response.json({
errcode: options.departmentErrcode,
errmsg: "not allow to access from your ip",
});
}
return Response.json({
errcode: 0,
errmsg: "ok",
department: options.departments ?? [
{ id: 1, name: "总办" },
{ id: 2, name: "运营部" },
],
});
}
if (url.pathname.endsWith("/cgi-bin/user/simplelist")) {
const departmentId = url.searchParams.get("department_id") ?? "";
const cursor = url.searchParams.get("cursor") ?? "";
const table = options.departmentUsers ?? {
"1": [
{ userid: "zhangsan", name: "张三", department: [1, 2] },
{ userid: "lisi", name: "李四", department: [1] },
],
"2": [
{ userid: "zhangsan", name: "张三", department: [1, 2] },
{ userid: "wangwu", name: "王五", department: [2] },
],
};
if (options.paginateSimplelist && departmentId === "1") {
if (!cursor) {
return Response.json({
errcode: 0,
errmsg: "ok",
userlist: table[departmentId] ?? [],
next_cursor: "cursor-2",
});
}
return Response.json({
errcode: 0,
errmsg: "ok",
userlist: [{ userid: "pagetwo", name: "第二页", department: [1] }],
});
}
return Response.json({
errcode: 0,
errmsg: "ok",
userlist: table[departmentId] ?? [],
});
}
return new Response("not found", { status: 404 });
};
return { calls, fetchImpl };
@@ -947,3 +998,61 @@ test("sendAppChatMessage throws WecomClientError when provider errors", async ()
},
);
});
test("listInternalContacts merges departments, dedupes and maps dept names", async () => {
const { calls, fetchImpl } = fakeWecom();
const contacts = await listInternalContacts(
resolveWecomConfig(baseBindings),
fetchImpl,
);
assert.deepEqual(
contacts.map((contact) => `${contact.name}:${contact.userid}`),
["李四:lisi", "王五:wangwu", "张三:zhangsan"],
"deduped and sorted by name (zh)",
);
const zhangsan = contacts.find((contact) => contact.userid === "zhangsan");
assert.deepEqual(zhangsan.departmentNames.sort(), ["总办", "运营部"]);
const simplelistCalls = calls.filter((call) =>
call.url.pathname.endsWith("/cgi-bin/user/simplelist"),
);
assert.deepEqual(
simplelistCalls
.map((call) => call.url.searchParams.get("department_id"))
.sort(),
["1", "2"],
"one simplelist call per department",
);
assert.ok(
simplelistCalls.every((call) => call.url.searchParams.get("fetch_child") === "0"),
);
});
test("listInternalContacts follows simplelist cursor pages", async () => {
const { fetchImpl } = fakeWecom({ paginateSimplelist: true });
const contacts = await listInternalContacts(
resolveWecomConfig(baseBindings),
fetchImpl,
);
assert.ok(contacts.some((contact) => contact.userid === "pagetwo"));
});
test("listInternalContacts throws WecomClientError on department errcode", async () => {
const { fetchImpl } = fakeWecom({ departmentErrcode: 60020 });
await assert.rejects(
() => listInternalContacts(resolveWecomConfig(baseBindings), fetchImpl),
(error) =>
error instanceof WecomClientError && /部门列表/.test(error.message),
);
});
test("listInternalContacts rejects without app credentials", async () => {
const { fetchImpl } = fakeWecom();
await assert.rejects(
() =>
listInternalContacts(
resolveWecomConfig({ ...baseBindings, WECOM_SECRET: "" }),
fetchImpl,
),
/未配置/,
);
});