fix: resolve Feishu sender names via basic profile API

This commit is contained in:
2026-07-08 23:59:17 +08:00
parent 79505e6d44
commit 491fd13ca8
2 changed files with 58 additions and 11 deletions
+29 -10
View File
@@ -96,11 +96,18 @@ export interface CardActionEvent {
readonly token?: string;
}
type ContactUserResponse = {
interface ContactBasicUser {
readonly name?: string | undefined;
readonly i18n_name?: {
readonly zh_cn?: string | undefined;
readonly en_us?: string | undefined;
readonly ja_jp?: string | undefined;
} | undefined;
}
type ContactUserBasicBatchResponse = {
readonly data?: {
readonly user?: {
readonly name?: string | undefined;
} | undefined;
readonly users?: readonly ContactBasicUser[] | undefined;
} | undefined;
} | null;
@@ -393,13 +400,12 @@ export async function resolveSenderName(
const fetchName = async (id: string): Promise<string | null> => {
try {
const res = await rt.client.request({
method: "GET",
url: `/open-apis/contact/v3/users/${encodeURIComponent(id)}`,
const res = await rt.client.contact.v3.user.basicBatch({
params: { user_id_type: "open_id" },
}) as ContactUserResponse;
const name = res?.data?.user?.name;
return typeof name === "string" && name !== "" ? name : null;
data: { user_ids: [id] },
}) as ContactUserBasicBatchResponse;
const user = res?.data?.users?.[0];
return user === undefined ? null : contactBasicUserName(user);
} catch (e) {
rt.logger.warn({ openId: id, err: e instanceof Error ? e.message : String(e) }, "resolveSenderName failed");
return null;
@@ -412,6 +418,19 @@ export async function resolveSenderName(
return fetchName(openId);
}
function contactBasicUserName(user: ContactBasicUser): string | null {
const candidates = [
user.name,
user.i18n_name?.zh_cn,
user.i18n_name?.en_us,
user.i18n_name?.ja_jp,
];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim() !== "") return candidate.trim();
}
return null;
}
// --- File helpers ---
/** Download a file/image from a Feishu message to a local path. */
+29 -1
View File
@@ -2,7 +2,8 @@ import { describe, expect, it, vi } from "vitest";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { sendFile, sendLongText, sendTextMessage, type FeishuRuntime } from "../../src/feishu/client.js";
import { resolveSenderName, sendFile, sendLongText, sendTextMessage, type FeishuRuntime } from "../../src/feishu/client.js";
import { SenderNameCache } from "../../src/feishu/senderCache.js";
describe("Feishu client helpers", () => {
it("accepts top-level file_key from the Lark SDK file upload response", async () => {
@@ -47,14 +48,41 @@ describe("Feishu client helpers", () => {
});
expect(sentTexts.map((text) => text.length)).toEqual([8000, 8000, 5]);
});
it("resolves sender names through the basic contact API and caches hits", async () => {
const contactBasicBatch = vi.fn(async () => ({ data: { users: [{ name: "Jiarong" }] } }));
const request = vi.fn();
const rt = mockRuntime({ fileCreate: vi.fn(), messageCreate: vi.fn(), contactBasicBatch, request });
const cache = new SenderNameCache();
await expect(resolveSenderName(rt, "ou_user_1", cache)).resolves.toBe("Jiarong");
await expect(resolveSenderName(rt, "ou_user_1", cache)).resolves.toBe("Jiarong");
expect(contactBasicBatch).toHaveBeenCalledTimes(1);
expect(contactBasicBatch).toHaveBeenCalledWith({
params: { user_id_type: "open_id" },
data: { user_ids: ["ou_user_1"] },
});
expect(request).not.toHaveBeenCalled();
});
});
function mockRuntime(options: {
readonly fileCreate: (payload: unknown) => Promise<unknown>;
readonly messageCreate: (payload: unknown) => Promise<unknown>;
readonly contactBasicBatch?: (payload: unknown) => Promise<unknown>;
readonly request?: (payload: unknown) => Promise<unknown>;
}): FeishuRuntime {
return {
client: {
request: options.request,
contact: {
v3: {
user: {
basicBatch: options.contactBasicBatch,
},
},
},
im: {
v1: {
file: { create: options.fileCreate },