Skip to content

Commit 27882dc

Browse files
BigUncleTakhoffman
andauthored
feat(feishu): add quota optimization flags (#10513) thanks @BigUncle
Verified: - pnpm build - pnpm check - pnpm vitest run --config vitest.extensions.config.ts extensions/feishu/src/config-schema.test.ts extensions/feishu/src/reply-dispatcher.test.ts extensions/feishu/src/bot.test.ts Co-authored-by: BigUncle <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent e0b1b48 commit 27882dc

File tree

8 files changed

+142
-16
lines changed

8 files changed

+142
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
3434
- Feishu/Group sender allowlist fallback: add global `channels.feishu.groupSenderAllowFrom` sender authorization for group chats, with per-group `groups.<id>.allowFrom` precedence and regression coverage for allow/block/precedence behavior. (#29174) Thanks @1MoreBuild.
3535
- Feishu/Docx append/write ordering: insert converted Docx blocks sequentially (single-block creates) so Feishu append/write preserves markdown block order instead of returning shuffled sections in asynchronous batch inserts. (#26172, #26022) Thanks @echoVic.
3636
- Feishu/Inbound media regression coverage: add explicit tests for message resource type mapping (`image` stays `image`, non-image maps to `file`) to prevent reintroducing unsupported Feishu `type=audio` fetches. (#16311, #8746) Thanks @Yaxuan42.
37+
- Feishu/API quota controls: add `typingIndicator` and `resolveSenderNames` config flags (top-level and per-account) so operators can disable typing reactions and sender-name lookup requests while keeping default behavior unchanged. (#10513) Thanks @BigUncle.
3738
- Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3.
3839
- Security/Compaction audit: remove the post-compaction audit injection message. (#28507) Thanks @fuller-stack-dev and @vincentkoc.
3940
- Telegram/Reply media context: include replied media files in inbound context when replying to media, defer reply-media downloads to debounce flush, gate reply-media fetch behind DM authorization, and preserve replied media when non-vision sticker fallback runs (including cached-sticker paths). (#28488) Thanks @obviyus.

docs/channels/feishu.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,34 @@ If your tenant is on Lark (international), set the domain to `lark` (or a full d
224224
}
225225
```
226226

227+
### Quota optimization flags
228+
229+
You can reduce Feishu API usage with two optional flags:
230+
231+
- `typingIndicator` (default `true`): when `false`, skip typing reaction calls.
232+
- `resolveSenderNames` (default `true`): when `false`, skip sender profile lookup calls.
233+
234+
Set them at top level or per account:
235+
236+
```json5
237+
{
238+
channels: {
239+
feishu: {
240+
typingIndicator: false,
241+
resolveSenderNames: false,
242+
accounts: {
243+
main: {
244+
appId: "cli_xxx",
245+
appSecret: "xxx",
246+
typingIndicator: true,
247+
resolveSenderNames: false,
248+
},
249+
},
250+
},
251+
},
252+
}
253+
```
254+
227255
---
228256

229257
## Step 3: Start + test

extensions/feishu/src/bot.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,37 @@ describe("handleFeishuMessage command authorization", () => {
256256
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
257257
});
258258

259+
it("skips sender-name lookup when resolveSenderNames is false", async () => {
260+
const cfg: ClawdbotConfig = {
261+
channels: {
262+
feishu: {
263+
dmPolicy: "open",
264+
allowFrom: ["*"],
265+
resolveSenderNames: false,
266+
},
267+
},
268+
} as ClawdbotConfig;
269+
270+
const event: FeishuMessageEvent = {
271+
sender: {
272+
sender_id: {
273+
open_id: "ou-attacker",
274+
},
275+
},
276+
message: {
277+
message_id: "msg-skip-sender-lookup",
278+
chat_id: "oc-dm",
279+
chat_type: "p2p",
280+
message_type: "text",
281+
content: JSON.stringify({ text: "hello" }),
282+
},
283+
};
284+
285+
await dispatchMessage({ cfg, event });
286+
287+
expect(mockCreateFeishuClient).not.toHaveBeenCalled();
288+
});
289+
259290
it("creates pairing request and drops unauthorized DMs in pairing mode", async () => {
260291
mockShouldComputeCommandAuthorized.mockReturnValue(false);
261292
mockReadAllowFromStore.mockResolvedValue([]);

extensions/feishu/src/bot.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -771,23 +771,26 @@ export async function handleFeishuMessage(params: {
771771
}
772772

773773
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
774-
const senderResult = await resolveFeishuSenderName({
775-
account,
776-
senderId: ctx.senderOpenId,
777-
log,
778-
});
779-
if (senderResult.name) ctx = { ...ctx, senderName: senderResult.name };
780-
781-
// Track permission error to inform agent later (with cooldown to avoid repetition)
774+
// Optimization: skip if disabled to save API quota (Feishu free tier limit).
782775
let permissionErrorForAgent: PermissionError | undefined;
783-
if (senderResult.permissionError) {
784-
const appKey = account.appId ?? "default";
785-
const now = Date.now();
786-
const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
787-
788-
if (now - lastNotified > PERMISSION_ERROR_COOLDOWN_MS) {
789-
permissionErrorNotifiedAt.set(appKey, now);
790-
permissionErrorForAgent = senderResult.permissionError;
776+
if (feishuCfg?.resolveSenderNames ?? true) {
777+
const senderResult = await resolveFeishuSenderName({
778+
account,
779+
senderId: ctx.senderOpenId,
780+
log,
781+
});
782+
if (senderResult.name) ctx = { ...ctx, senderName: senderResult.name };
783+
784+
// Track permission error to inform agent later (with cooldown to avoid repetition)
785+
if (senderResult.permissionError) {
786+
const appKey = account.appId ?? "default";
787+
const now = Date.now();
788+
const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
789+
790+
if (now - lastNotified > PERMISSION_ERROR_COOLDOWN_MS) {
791+
permissionErrorNotifiedAt.set(appKey, now);
792+
permissionErrorForAgent = senderResult.permissionError;
793+
}
791794
}
792795
}
793796

extensions/feishu/src/config-schema.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,24 @@ describe("FeishuConfigSchema replyInThread", () => {
117117
expect(result.accounts?.main?.replyInThread).toBe("enabled");
118118
});
119119
});
120+
121+
describe("FeishuConfigSchema optimization flags", () => {
122+
it("defaults top-level typingIndicator and resolveSenderNames to true", () => {
123+
const result = FeishuConfigSchema.parse({});
124+
expect(result.typingIndicator).toBe(true);
125+
expect(result.resolveSenderNames).toBe(true);
126+
});
127+
128+
it("accepts account-level optimization flags", () => {
129+
const result = FeishuConfigSchema.parse({
130+
accounts: {
131+
main: {
132+
typingIndicator: false,
133+
resolveSenderNames: false,
134+
},
135+
},
136+
});
137+
expect(result.accounts?.main?.typingIndicator).toBe(false);
138+
expect(result.accounts?.main?.resolveSenderNames).toBe(false);
139+
});
140+
});

extensions/feishu/src/config-schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ const FeishuSharedConfigShape = {
162162
tools: FeishuToolsConfigSchema,
163163
replyInThread: ReplyInThreadSchema,
164164
reactionNotifications: ReactionNotificationModeSchema,
165+
typingIndicator: z.boolean().optional(),
166+
resolveSenderNames: z.boolean().optional(),
165167
};
166168

167169
/**
@@ -205,6 +207,9 @@ export const FeishuConfigSchema = z
205207
topicSessionMode: TopicSessionModeSchema,
206208
// Dynamic agent creation for DM users
207209
dynamicAgentCreation: DynamicAgentCreationSchema,
210+
// Optimization flags
211+
typingIndicator: z.boolean().optional().default(true),
212+
resolveSenderNames: z.boolean().optional().default(true),
208213
// Multi-account configuration
209214
accounts: z.record(z.string(), FeishuAccountConfigSchema.optional()).optional(),
210215
})

extensions/feishu/src/reply-dispatcher.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const sendMediaFeishuMock = vi.hoisted(() => vi.fn());
88
const createFeishuClientMock = vi.hoisted(() => vi.fn());
99
const resolveReceiveIdTypeMock = vi.hoisted(() => vi.fn());
1010
const createReplyDispatcherWithTypingMock = vi.hoisted(() => vi.fn());
11+
const addTypingIndicatorMock = vi.hoisted(() => vi.fn(async () => ({ messageId: "om_msg" })));
12+
const removeTypingIndicatorMock = vi.hoisted(() => vi.fn(async () => {}));
1113
const streamingInstances = vi.hoisted(() => [] as any[]);
1214

1315
vi.mock("./accounts.js", () => ({ resolveFeishuAccount: resolveFeishuAccountMock }));
@@ -19,6 +21,10 @@ vi.mock("./send.js", () => ({
1921
vi.mock("./media.js", () => ({ sendMediaFeishu: sendMediaFeishuMock }));
2022
vi.mock("./client.js", () => ({ createFeishuClient: createFeishuClientMock }));
2123
vi.mock("./targets.js", () => ({ resolveReceiveIdType: resolveReceiveIdTypeMock }));
24+
vi.mock("./typing.js", () => ({
25+
addTypingIndicator: addTypingIndicatorMock,
26+
removeTypingIndicator: removeTypingIndicatorMock,
27+
}));
2228
vi.mock("./streaming-card.js", () => ({
2329
FeishuStreamingSession: class {
2430
active = false;
@@ -83,6 +89,33 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
8389
});
8490
});
8591

92+
it("skips typing indicator when account typingIndicator is disabled", async () => {
93+
resolveFeishuAccountMock.mockReturnValue({
94+
accountId: "main",
95+
appId: "app_id",
96+
appSecret: "app_secret",
97+
domain: "feishu",
98+
config: {
99+
renderMode: "auto",
100+
streaming: true,
101+
typingIndicator: false,
102+
},
103+
});
104+
105+
createFeishuReplyDispatcher({
106+
cfg: {} as never,
107+
agentId: "agent",
108+
runtime: {} as never,
109+
chatId: "oc_chat",
110+
replyToMessageId: "om_parent",
111+
});
112+
113+
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
114+
await options.onReplyStart?.();
115+
116+
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
117+
});
118+
86119
it("keeps auto mode plain text on non-streaming send path", async () => {
87120
createFeishuReplyDispatcher({
88121
cfg: {} as never,

extensions/feishu/src/reply-dispatcher.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
5656
let typingState: TypingIndicatorState | null = null;
5757
const typingCallbacks = createTypingCallbacks({
5858
start: async () => {
59+
// Check if typing indicator is enabled (default: true)
60+
if (!(account.config.typingIndicator ?? true)) {
61+
return;
62+
}
5963
if (!replyToMessageId) {
6064
return;
6165
}

0 commit comments

Comments
 (0)