forked from EduCraft/curriculum-project-hub
fix: resolve Feishu sender names via basic profile API
This commit is contained in:
+29
-10
@@ -96,11 +96,18 @@ export interface CardActionEvent {
|
|||||||
readonly token?: string;
|
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 data?: {
|
||||||
readonly user?: {
|
readonly users?: readonly ContactBasicUser[] | undefined;
|
||||||
readonly name?: string | undefined;
|
|
||||||
} | undefined;
|
|
||||||
} | undefined;
|
} | undefined;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
@@ -393,13 +400,12 @@ export async function resolveSenderName(
|
|||||||
|
|
||||||
const fetchName = async (id: string): Promise<string | null> => {
|
const fetchName = async (id: string): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
const res = await rt.client.request({
|
const res = await rt.client.contact.v3.user.basicBatch({
|
||||||
method: "GET",
|
|
||||||
url: `/open-apis/contact/v3/users/${encodeURIComponent(id)}`,
|
|
||||||
params: { user_id_type: "open_id" },
|
params: { user_id_type: "open_id" },
|
||||||
}) as ContactUserResponse;
|
data: { user_ids: [id] },
|
||||||
const name = res?.data?.user?.name;
|
}) as ContactUserBasicBatchResponse;
|
||||||
return typeof name === "string" && name !== "" ? name : null;
|
const user = res?.data?.users?.[0];
|
||||||
|
return user === undefined ? null : contactBasicUserName(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rt.logger.warn({ openId: id, err: e instanceof Error ? e.message : String(e) }, "resolveSenderName failed");
|
rt.logger.warn({ openId: id, err: e instanceof Error ? e.message : String(e) }, "resolveSenderName failed");
|
||||||
return null;
|
return null;
|
||||||
@@ -412,6 +418,19 @@ export async function resolveSenderName(
|
|||||||
return fetchName(openId);
|
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 ---
|
// --- File helpers ---
|
||||||
|
|
||||||
/** Download a file/image from a Feishu message to a local path. */
|
/** Download a file/image from a Feishu message to a local path. */
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
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", () => {
|
describe("Feishu client helpers", () => {
|
||||||
it("accepts top-level file_key from the Lark SDK file upload response", async () => {
|
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]);
|
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: {
|
function mockRuntime(options: {
|
||||||
readonly fileCreate: (payload: unknown) => Promise<unknown>;
|
readonly fileCreate: (payload: unknown) => Promise<unknown>;
|
||||||
readonly messageCreate: (payload: unknown) => Promise<unknown>;
|
readonly messageCreate: (payload: unknown) => Promise<unknown>;
|
||||||
|
readonly contactBasicBatch?: (payload: unknown) => Promise<unknown>;
|
||||||
|
readonly request?: (payload: unknown) => Promise<unknown>;
|
||||||
}): FeishuRuntime {
|
}): FeishuRuntime {
|
||||||
return {
|
return {
|
||||||
client: {
|
client: {
|
||||||
|
request: options.request,
|
||||||
|
contact: {
|
||||||
|
v3: {
|
||||||
|
user: {
|
||||||
|
basicBatch: options.contactBasicBatch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
im: {
|
im: {
|
||||||
v1: {
|
v1: {
|
||||||
file: { create: options.fileCreate },
|
file: { create: options.fileCreate },
|
||||||
|
|||||||
Reference in New Issue
Block a user