Skip to content

Commit 4fb3939

Browse files
authored
feat(feishu): standardize request UA and register bot as AI agent (#63835)
- Set User-Agent to openclaw-feishu-builtin/{version}/{platform} for all Feishu API requests to comply with OAPI best practices - Switch health-check probe to POST /bot/v1/openclaw_bot/ping to register the app as an AI agent (智能体) on the Feishu platform - Update probe response parsing for new pingBotInfo response shape
1 parent 407da8e commit 4fb3939

4 files changed

Lines changed: 74 additions & 24 deletions

File tree

extensions/feishu/src/client.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
import type { Agent } from "node:https";
2+
import { createRequire } from "node:module";
23
import * as Lark from "@larksuiteoapi/node-sdk";
34
import { resolveAmbientNodeProxyAgent } from "openclaw/plugin-sdk/extension-shared";
45
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
56

7+
const require = createRequire(import.meta.url);
8+
const { version: pluginVersion } = require("../package.json") as { version: string };
9+
10+
export { pluginVersion };
11+
12+
const FEISHU_USER_AGENT = `openclaw-feishu-builtin/${pluginVersion}/${process.platform}`;
13+
export { FEISHU_USER_AGENT };
14+
15+
/** User-Agent header value for all Feishu API requests. */
16+
export function getFeishuUserAgent(): string {
17+
return FEISHU_USER_AGENT;
18+
}
19+
620
type FeishuClientSdk = Pick<
721
typeof Lark,
822
| "AppType"
@@ -26,6 +40,35 @@ const defaultFeishuClientSdk: FeishuClientSdk = {
2640

2741
let feishuClientSdk: FeishuClientSdk = defaultFeishuClientSdk;
2842

43+
// Override the SDK's default User-Agent interceptor.
44+
// The Lark SDK registers an axios request interceptor that sets
45+
// 'oapi-node-sdk/1.0.0'. Axios request interceptors execute in LIFO order
46+
// (last-registered runs first), so simply appending ours doesn't work — the
47+
// SDK's interceptor would run last and overwrite our UA. We must clear
48+
// handlers[] first, then register our own as the sole interceptor.
49+
//
50+
// Risk is low: the SDK only registers one interceptor (UA) at init time, and
51+
// we clear it at module load before any other code can register handlers.
52+
// If a future SDK version adds more interceptors, the upgrade will need
53+
// compatibility verification regardless.
54+
{
55+
const inst = Lark.defaultHttpInstance as {
56+
interceptors?: {
57+
request: { handlers: unknown[]; use: (fn: (req: unknown) => unknown) => void };
58+
};
59+
};
60+
if (inst.interceptors?.request) {
61+
inst.interceptors.request.handlers = [];
62+
inst.interceptors.request.use((req: unknown) => {
63+
const r = req as { headers?: Record<string, string> };
64+
if (r.headers) {
65+
r.headers["User-Agent"] = getFeishuUserAgent();
66+
}
67+
return req;
68+
});
69+
}
70+
}
71+
2972
/** Default HTTP timeout for Feishu API requests (30 seconds). */
3073
export const FEISHU_HTTP_TIMEOUT_MS = 30_000;
3174
export const FEISHU_HTTP_TIMEOUT_MAX_MS = 300_000;
@@ -36,7 +79,7 @@ type FeishuHttpInstanceLike = Pick<
3679
"request" | "get" | "post" | "put" | "patch" | "delete" | "head" | "options"
3780
>;
3881

39-
async function getWsProxyAgent(): Promise<Agent | undefined> {
82+
async function getWsProxyAgent() {
4083
return resolveAmbientNodeProxyAgent<Agent>();
4184
}
4285

@@ -61,8 +104,8 @@ function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
61104

62105
/**
63106
* Create an HTTP instance that delegates to the Lark SDK's default instance
64-
* but injects a default request timeout to prevent indefinite hangs
65-
* (e.g. when the Feishu API is slow, causing per-chat queue deadlocks).
107+
* but injects a default request timeout and User-Agent header to prevent
108+
* indefinite hangs and set a standardized User-Agent per OAPI best practices.
66109
*/
67110
function createTimeoutHttpInstance(defaultTimeoutMs: number): Lark.HttpInstance {
68111
const base: FeishuHttpInstanceLike = feishuClientSdk.defaultHttpInstance;

extensions/feishu/src/probe.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ vi.mock("./client.js", () => ({
1010
const DEFAULT_CREDS = { appId: "cli_123", appSecret: "secret" } as const; // pragma: allowlist secret
1111
const DEFAULT_SUCCESS_RESPONSE = {
1212
code: 0,
13-
bot: { bot_name: "TestBot", open_id: "ou_abc123" },
13+
data: { pingBotInfo: { botName: "TestBot", botID: "ou_abc123" } },
1414
} as const;
1515
const DEFAULT_SUCCESS_RESULT = {
1616
ok: true,
@@ -20,7 +20,7 @@ const DEFAULT_SUCCESS_RESULT = {
2020
} as const;
2121
const BOT1_RESPONSE = {
2222
code: 0,
23-
bot: { bot_name: "Bot1", open_id: "ou_1" },
23+
data: { pingBotInfo: { botName: "Bot1", botID: "ou_1" } },
2424
} as const;
2525

2626
function makeRequestFn(response: Record<string, unknown>) {
@@ -135,8 +135,9 @@ describe("probeFeishu", () => {
135135

136136
expect(requestFn).toHaveBeenCalledWith(
137137
expect.objectContaining({
138-
method: "GET",
139-
url: "/open-apis/bot/v3/info",
138+
method: "POST",
139+
url: "/open-apis/bot/v1/openclaw_bot/ping",
140+
data: { needBotInfo: true },
140141
timeout: FEISHU_PROBE_REQUEST_TIMEOUT_MS,
141142
}),
142143
);
@@ -259,10 +260,10 @@ describe("probeFeishu", () => {
259260
});
260261
});
261262

262-
it("handles response.data.bot fallback path", async () => {
263+
it("handles response with pingBotInfo in data", async () => {
263264
setupClient({
264265
code: 0,
265-
data: { bot: { bot_name: "DataBot", open_id: "ou_data" } },
266+
data: { pingBotInfo: { botName: "DataBot", botID: "ou_data" } },
266267
});
267268

268269
await expectDefaultSuccessResult(DEFAULT_CREDS, {

extensions/feishu/src/probe.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,19 @@ export type ProbeFeishuOptions = {
1818
abortSignal?: AbortSignal;
1919
};
2020

21-
type FeishuBotInfoResponse = {
21+
type FeishuPingResponse = {
2222
code: number;
2323
msg?: string;
24-
bot?: { bot_name?: string; open_id?: string };
25-
data?: { bot?: { bot_name?: string; open_id?: string } };
24+
data?: { pingBotInfo?: { botID?: string; botName?: string } };
2625
};
2726

2827
type FeishuRequestClient = ReturnType<typeof createFeishuClient> & {
2928
request(params: {
30-
method: "GET";
29+
method: "POST";
3130
url: string;
32-
data: Record<string, never>;
31+
data: Record<string, unknown>;
3332
timeout: number;
34-
}): Promise<FeishuBotInfoResponse>;
33+
}): Promise<FeishuPingResponse>;
3534
};
3635

3736
function setCachedProbeResult(
@@ -81,12 +80,14 @@ export async function probeFeishu(
8180

8281
try {
8382
const client = createFeishuClient(creds) as FeishuRequestClient;
84-
// Use bot/v3/info API to get bot information
85-
const responseResult = await raceWithTimeoutAndAbort<FeishuBotInfoResponse>(
83+
// Feishu-provided endpoint for OpenClaw, supported on both Feishu (CN)
84+
// and Lark (international). No OAuth scopes required. Validates
85+
// credentials and registers the app as an AI agent (智能体).
86+
const responseResult = await raceWithTimeoutAndAbort<FeishuPingResponse>(
8687
client.request({
87-
method: "GET",
88-
url: "/open-apis/bot/v3/info",
89-
data: {},
88+
method: "POST",
89+
url: "/open-apis/bot/v1/openclaw_bot/ping",
90+
data: { needBotInfo: true },
9091
timeout: timeoutMs,
9192
}),
9293
{
@@ -135,14 +136,14 @@ export async function probeFeishu(
135136
);
136137
}
137138

138-
const bot = response.bot || response.data?.bot;
139+
const botInfo = response.data?.pingBotInfo;
139140
return setCachedProbeResult(
140141
cacheKey,
141142
{
142143
ok: true,
143144
appId: creds.appId,
144-
botName: bot?.bot_name,
145-
botOpenId: bot?.open_id,
145+
botName: botInfo?.botName,
146+
botOpenId: botInfo?.botID,
146147
},
147148
PROBE_SUCCESS_TTL_MS,
148149
);

extensions/feishu/src/streaming-card.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import type { Client } from "@larksuiteoapi/node-sdk";
66
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
7+
import { getFeishuUserAgent } from "./client.js";
78
import { resolveFeishuCardTemplate, type CardHeaderConfig } from "./send.js";
89
import type { FeishuDomain } from "./types.js";
910

@@ -76,7 +77,7 @@ async function getToken(creds: Credentials): Promise<string> {
7677
url: `${resolveApiBase(creds.domain)}/auth/v3/tenant_access_token/internal`,
7778
init: {
7879
method: "POST",
79-
headers: { "Content-Type": "application/json" },
80+
headers: { "Content-Type": "application/json", "User-Agent": getFeishuUserAgent() },
8081
body: JSON.stringify({ app_id: creds.appId, app_secret: creds.appSecret }),
8182
},
8283
policy: { allowedHostnames: resolveAllowedHostnames(creds.domain) },
@@ -221,6 +222,7 @@ export class FeishuStreamingSession {
221222
headers: {
222223
Authorization: `Bearer ${await getToken(this.creds)}`,
223224
"Content-Type": "application/json",
225+
"User-Agent": getFeishuUserAgent(),
224226
},
225227
body: JSON.stringify({ type: "card_json", data: JSON.stringify(cardJson) }),
226228
},
@@ -305,6 +307,7 @@ export class FeishuStreamingSession {
305307
headers: {
306308
Authorization: `Bearer ${await getToken(this.creds)}`,
307309
"Content-Type": "application/json",
310+
"User-Agent": getFeishuUserAgent(),
308311
},
309312
body: JSON.stringify({
310313
content: text,
@@ -370,6 +373,7 @@ export class FeishuStreamingSession {
370373
headers: {
371374
Authorization: `Bearer ${await getToken(this.creds)}`,
372375
"Content-Type": "application/json",
376+
"User-Agent": getFeishuUserAgent(),
373377
},
374378
body: JSON.stringify({
375379
content: `<font color='grey'>${note}</font>`,
@@ -421,6 +425,7 @@ export class FeishuStreamingSession {
421425
headers: {
422426
Authorization: `Bearer ${await getToken(this.creds)}`,
423427
"Content-Type": "application/json; charset=utf-8",
428+
"User-Agent": getFeishuUserAgent(),
424429
},
425430
body: JSON.stringify({
426431
settings: JSON.stringify({

0 commit comments

Comments
 (0)