mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
fix(feishu): preserve api error diagnostics
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Feishu: preserve Feishu/Lark HTTP error bodies for message sends, media sends, and chat member lookups, so HTTP 400 failures include vendor code, message, log id, and troubleshooter details. Fixes #73860. Thanks @desksk.
|
||||
- Telegram: inherit the process DNS result order for Bot API transport and downgrade recovered sticky IPv4 fallback promotions to debug logs, while keeping pinned-IP escalation warnings visible. Fixes #75904. Thanks @highfly-hi and @neeravmakwana.
|
||||
- Web search/MiniMax: allow `MINIMAX_OAUTH_TOKEN` to satisfy MiniMax Search credentials, so OAuth-authorized MiniMax Token Plan setups do not need a separate web-search key. Fixes #65768. Thanks @kikibrian and @zhouhe-xydt.
|
||||
- Providers/MiniMax: derive Coding Plan usage polling from the configured MiniMax base URL, so global setups no longer query the CN usage host. Fixes #65054. Thanks @sixone74 and @Yanhu007.
|
||||
|
||||
@@ -142,4 +142,55 @@ describe("registerFeishuChatTools", () => {
|
||||
);
|
||||
expect(registerTool).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves Feishu diagnostics from rejected member lookups", async () => {
|
||||
const registerTool = vi.fn();
|
||||
registerFeishuChatTools(
|
||||
createChatToolApi({
|
||||
config: {
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
appId: "app_id",
|
||||
appSecret: "app_secret", // pragma: allowlist secret
|
||||
tools: { chat: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
registerTool,
|
||||
}),
|
||||
);
|
||||
|
||||
const tool = registerTool.mock.calls[0]?.[0];
|
||||
contactUserGetMock.mockRejectedValueOnce(
|
||||
Object.assign(new Error("Request failed with status code 400"), {
|
||||
response: {
|
||||
status: 400,
|
||||
data: {
|
||||
code: 99992360,
|
||||
msg: "The request you send is not a valid {user_id} or not exists",
|
||||
error: {
|
||||
log_id: "20260429124800CHAT",
|
||||
troubleshooter: "https://open.feishu.cn/search?log_id=20260429124800CHAT",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await tool.execute("tc_4", {
|
||||
action: "member_info",
|
||||
member_id: "ou_1",
|
||||
});
|
||||
|
||||
expect(result.details.error).toContain('"http_status":400');
|
||||
expect(result.details.error).toContain('"feishu_code":99992360');
|
||||
expect(result.details.error).toContain(
|
||||
'"feishu_msg":"The request you send is not a valid {user_id} or not exists"',
|
||||
);
|
||||
expect(result.details.error).toContain('"feishu_log_id":"20260429124800CHAT"');
|
||||
expect(result.details.error).toContain(
|
||||
'"feishu_troubleshooter":"https://open.feishu.cn/search?log_id=20260429124800CHAT"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import type { OpenClawPluginApi } from "../runtime-api.js";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { formatFeishuApiError } from "./comment-shared.js";
|
||||
import { resolveToolsConfig } from "./tools-config.js";
|
||||
|
||||
function json(data: unknown) {
|
||||
@@ -179,7 +179,7 @@ export function registerFeishuChatTools(api: OpenClawPluginApi) {
|
||||
return json({ error: `Unknown action: ${String(p.action)}` });
|
||||
}
|
||||
} catch (err) {
|
||||
return json({ error: formatErrorMessage(err) });
|
||||
return json({ error: formatFeishuApiError(err, { includeNestedErrorLogId: true }) });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ export function formatFeishuApiError(
|
||||
(options.includeNestedErrorLogId
|
||||
? readString(isRecord(responseData?.error) ? responseData.error.log_id : undefined)
|
||||
: undefined);
|
||||
const nestedError = isRecord(responseData?.error) ? responseData.error : undefined;
|
||||
|
||||
return JSON.stringify({
|
||||
message:
|
||||
@@ -58,9 +59,49 @@ export function formatFeishuApiError(
|
||||
typeof responseData?.code === "number" ? responseData.code : readString(responseData?.code),
|
||||
feishu_msg: readString(responseData?.msg),
|
||||
feishu_log_id: feishuLogId,
|
||||
feishu_troubleshooter:
|
||||
readString(responseData?.troubleshooter) || readString(nestedError?.troubleshooter),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatFeishuApiFailure(
|
||||
error: unknown,
|
||||
errorPrefix: string,
|
||||
options: {
|
||||
includeConfigParams?: boolean;
|
||||
includeNestedErrorLogId?: boolean;
|
||||
} = {},
|
||||
): string {
|
||||
const details = formatFeishuApiError(error, options);
|
||||
return `${errorPrefix}: ${details || "unknown error"}`;
|
||||
}
|
||||
|
||||
export function createFeishuApiError(
|
||||
error: unknown,
|
||||
errorPrefix: string,
|
||||
options: {
|
||||
includeConfigParams?: boolean;
|
||||
includeNestedErrorLogId?: boolean;
|
||||
} = {},
|
||||
): Error {
|
||||
return new Error(formatFeishuApiFailure(error, errorPrefix, options), { cause: error });
|
||||
}
|
||||
|
||||
export async function requestFeishuApi<T>(
|
||||
request: () => Promise<T>,
|
||||
errorPrefix: string,
|
||||
options: {
|
||||
includeConfigParams?: boolean;
|
||||
includeNestedErrorLogId?: boolean;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await request();
|
||||
} catch (error) {
|
||||
throw createFeishuApiError(error, errorPrefix, options);
|
||||
}
|
||||
}
|
||||
|
||||
type ParsedCommentDocumentRef = {
|
||||
fileType?: CommentFileType;
|
||||
fileToken?: string;
|
||||
|
||||
@@ -384,6 +384,34 @@ describe("sendMediaFeishu msg_type routing", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves Feishu diagnostics when media sends reject before response checks", async () => {
|
||||
messageCreateMock.mockRejectedValueOnce(
|
||||
Object.assign(new Error("Request failed with status code 400"), {
|
||||
response: {
|
||||
status: 400,
|
||||
data: {
|
||||
code: 9499,
|
||||
msg: "Bad Request",
|
||||
error: {
|
||||
log_id: "20260429124731MEDIA",
|
||||
troubleshooter: "https://open.feishu.cn/search?log_id=20260429124731MEDIA",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const send = sendMediaFeishu({
|
||||
cfg: emptyConfig,
|
||||
to: "user:ou_target",
|
||||
mediaBuffer: Buffer.from("image"),
|
||||
fileName: "photo.png",
|
||||
});
|
||||
|
||||
await expect(send).rejects.toThrow(/Feishu image send failed: .*"feishu_code":9499/);
|
||||
await expect(send).rejects.toThrow(/"feishu_log_id":"20260429124731MEDIA"/);
|
||||
});
|
||||
|
||||
it("uses msg_type=media when replying with mp4", async () => {
|
||||
await sendMediaFeishu({
|
||||
cfg: emptyConfig,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtim
|
||||
import type { ClawdbotConfig } from "../runtime-api.js";
|
||||
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { requestFeishuApi } from "./comment-shared.js";
|
||||
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
|
||||
@@ -418,12 +419,17 @@ export async function uploadImageFeishu(params: {
|
||||
// See: https://github.com/larksuite/node-sdk/issues/121
|
||||
const imageData = typeof image === "string" ? fs.createReadStream(image) : image;
|
||||
|
||||
const response = await client.im.image.create({
|
||||
data: {
|
||||
image_type: imageType,
|
||||
image: imageData,
|
||||
},
|
||||
});
|
||||
const response = await requestFeishuApi(
|
||||
() =>
|
||||
client.im.image.create({
|
||||
data: {
|
||||
image_type: imageType,
|
||||
image: imageData,
|
||||
},
|
||||
}),
|
||||
"Feishu image upload failed",
|
||||
{ includeNestedErrorLogId: true },
|
||||
);
|
||||
|
||||
return {
|
||||
imageKey: extractFeishuUploadKey(response, {
|
||||
@@ -469,14 +475,19 @@ export async function uploadFileFeishu(params: {
|
||||
|
||||
const safeFileName = sanitizeFileNameForUpload(fileName);
|
||||
|
||||
const response = await client.im.file.create({
|
||||
data: {
|
||||
file_type: fileType,
|
||||
file_name: safeFileName,
|
||||
file: fileData,
|
||||
...(duration !== undefined && { duration }),
|
||||
},
|
||||
});
|
||||
const response = await requestFeishuApi(
|
||||
() =>
|
||||
client.im.file.create({
|
||||
data: {
|
||||
file_type: fileType,
|
||||
file_name: safeFileName,
|
||||
file: fileData,
|
||||
...(duration !== undefined && { duration }),
|
||||
},
|
||||
}),
|
||||
"Feishu file upload failed",
|
||||
{ includeNestedErrorLogId: true },
|
||||
);
|
||||
|
||||
return {
|
||||
fileKey: extractFeishuUploadKey(response, {
|
||||
@@ -506,26 +517,36 @@ export async function sendImageFeishu(params: {
|
||||
const content = JSON.stringify({ image_key: imageKey });
|
||||
|
||||
if (replyToMessageId) {
|
||||
const response = await client.im.message.reply({
|
||||
path: { message_id: replyToMessageId },
|
||||
data: {
|
||||
content,
|
||||
msg_type: "image",
|
||||
...(replyInThread ? { reply_in_thread: true } : {}),
|
||||
},
|
||||
});
|
||||
const response = await requestFeishuApi(
|
||||
() =>
|
||||
client.im.message.reply({
|
||||
path: { message_id: replyToMessageId },
|
||||
data: {
|
||||
content,
|
||||
msg_type: "image",
|
||||
...(replyInThread ? { reply_in_thread: true } : {}),
|
||||
},
|
||||
}),
|
||||
"Feishu image reply failed",
|
||||
{ includeNestedErrorLogId: true },
|
||||
);
|
||||
assertFeishuMessageApiSuccess(response, "Feishu image reply failed");
|
||||
return toFeishuSendResult(response, receiveId);
|
||||
}
|
||||
|
||||
const response = await client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
content,
|
||||
msg_type: "image",
|
||||
},
|
||||
});
|
||||
const response = await requestFeishuApi(
|
||||
() =>
|
||||
client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
content,
|
||||
msg_type: "image",
|
||||
},
|
||||
}),
|
||||
"Feishu image send failed",
|
||||
{ includeNestedErrorLogId: true },
|
||||
);
|
||||
assertFeishuMessageApiSuccess(response, "Feishu image send failed");
|
||||
return toFeishuSendResult(response, receiveId);
|
||||
}
|
||||
@@ -553,26 +574,36 @@ export async function sendFileFeishu(params: {
|
||||
const content = JSON.stringify({ file_key: fileKey });
|
||||
|
||||
if (replyToMessageId) {
|
||||
const response = await client.im.message.reply({
|
||||
path: { message_id: replyToMessageId },
|
||||
data: {
|
||||
content,
|
||||
msg_type: msgType,
|
||||
...(replyInThread ? { reply_in_thread: true } : {}),
|
||||
},
|
||||
});
|
||||
const response = await requestFeishuApi(
|
||||
() =>
|
||||
client.im.message.reply({
|
||||
path: { message_id: replyToMessageId },
|
||||
data: {
|
||||
content,
|
||||
msg_type: msgType,
|
||||
...(replyInThread ? { reply_in_thread: true } : {}),
|
||||
},
|
||||
}),
|
||||
"Feishu file reply failed",
|
||||
{ includeNestedErrorLogId: true },
|
||||
);
|
||||
assertFeishuMessageApiSuccess(response, "Feishu file reply failed");
|
||||
return toFeishuSendResult(response, receiveId);
|
||||
}
|
||||
|
||||
const response = await client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
content,
|
||||
msg_type: msgType,
|
||||
},
|
||||
});
|
||||
const response = await requestFeishuApi(
|
||||
() =>
|
||||
client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
content,
|
||||
msg_type: msgType,
|
||||
},
|
||||
}),
|
||||
"Feishu file send failed",
|
||||
{ includeNestedErrorLogId: true },
|
||||
);
|
||||
assertFeishuMessageApiSuccess(response, "Feishu file send failed");
|
||||
return toFeishuSendResult(response, receiveId);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,34 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves Feishu diagnostics when direct sends reject before response checks", async () => {
|
||||
const apiError = Object.assign(new Error("Request failed with status code 400"), {
|
||||
response: {
|
||||
status: 400,
|
||||
data: {
|
||||
code: 9499,
|
||||
msg: "Bad Request",
|
||||
error: {
|
||||
log_id: "202604291247104BEF4C42D2420A9AD569",
|
||||
troubleshooter:
|
||||
"https://open.feishu.cn/search?log_id=202604291247104BEF4C42D2420A9AD569",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
createMock.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
sendMessageFeishu({
|
||||
cfg: {} as never,
|
||||
to: "user:ou_target",
|
||||
text: "hello",
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/Feishu send failed: .*"http_status":400.*"feishu_code":9499.*"feishu_msg":"Bad Request".*"feishu_log_id":"202604291247104BEF4C42D2420A9AD569".*"feishu_troubleshooter":"https:\/\/open\.feishu\.cn\/search\?log_id=202604291247104BEF4C42D2420A9AD569"/,
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to create for withdrawn post replies", async () => {
|
||||
replyMock.mockResolvedValue({
|
||||
code: 230011,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import type { ClawdbotConfig } from "../runtime-api.js";
|
||||
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { createFeishuApiError, requestFeishuApi } from "./comment-shared.js";
|
||||
import type { MentionTarget } from "./mention-target.types.js";
|
||||
import { buildMentionedCardContent, buildMentionedMessage } from "./mention.js";
|
||||
import { parsePostContent } from "./post.js";
|
||||
@@ -117,14 +118,19 @@ async function sendFallbackDirect(
|
||||
},
|
||||
errorPrefix: string,
|
||||
): Promise<FeishuSendResult> {
|
||||
const response = await client.im.message.create({
|
||||
params: { receive_id_type: params.receiveIdType },
|
||||
data: {
|
||||
receive_id: params.receiveId,
|
||||
content: params.content,
|
||||
msg_type: params.msgType,
|
||||
},
|
||||
});
|
||||
const response = await requestFeishuApi(
|
||||
() =>
|
||||
client.im.message.create({
|
||||
params: { receive_id_type: params.receiveIdType },
|
||||
data: {
|
||||
receive_id: params.receiveId,
|
||||
content: params.content,
|
||||
msg_type: params.msgType,
|
||||
},
|
||||
}),
|
||||
errorPrefix,
|
||||
{ includeNestedErrorLogId: true },
|
||||
);
|
||||
assertFeishuMessageApiSuccess(response, errorPrefix);
|
||||
return toFeishuSendResult(response, params.receiveId);
|
||||
}
|
||||
@@ -168,7 +174,7 @@ async function sendReplyOrFallbackDirect(
|
||||
});
|
||||
} catch (err) {
|
||||
if (!isWithdrawnReplyError(err)) {
|
||||
throw err;
|
||||
throw createFeishuApiError(err, params.replyErrorPrefix, { includeNestedErrorLogId: true });
|
||||
}
|
||||
if (threadReplyFallbackError) {
|
||||
throw threadReplyFallbackError;
|
||||
|
||||
Reference in New Issue
Block a user