@m1heng-clawd/feishu 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -2
- package/index.ts +7 -2
- package/package.json +5 -4
- package/skills/feishu-chat/SKILL.md +139 -0
- package/skills/feishu-urgent/SKILL.md +107 -0
- package/src/bot.ts +569 -59
- package/src/channel.ts +2 -0
- package/src/chat-tools/actions.ts +433 -0
- package/src/chat-tools/common.ts +13 -0
- package/src/chat-tools/index.ts +2 -0
- package/src/chat-tools/register.ts +72 -0
- package/src/chat-tools/schemas.ts +66 -0
- package/src/client.ts +13 -0
- package/src/config-schema.ts +6 -0
- package/src/drive-tools/actions.ts +57 -21
- package/src/drive-tools/schemas.ts +2 -2
- package/src/media-duration.ts +190 -0
- package/src/media.ts +60 -5
- package/src/mention.ts +1 -1
- package/src/monitor.ts +32 -8
- package/src/policy.ts +7 -3
- package/src/reply-dispatcher.ts +97 -13
- package/src/send.ts +360 -42
- package/src/streaming-card.ts +32 -4
- package/src/tools-config.ts +9 -2
- package/src/types.ts +8 -1
- package/src/typing.ts +115 -5
- package/src/urgent-tools/actions.ts +84 -0
- package/src/urgent-tools/index.ts +3 -0
- package/src/urgent-tools/register.ts +64 -0
- package/src/urgent-tools/schemas.ts +25 -0
package/src/tools-config.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { FeishuToolsConfig } from "./types.js";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Default tool configuration.
|
|
5
|
-
* - doc, wiki, drive, scopes, task: enabled by default
|
|
5
|
+
* - doc, wiki, drive, scopes, task, chat: enabled by default
|
|
6
6
|
* - perm: disabled by default (sensitive operation)
|
|
7
7
|
*/
|
|
8
8
|
export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
|
|
@@ -12,11 +12,18 @@ export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
|
|
|
12
12
|
perm: false,
|
|
13
13
|
scopes: true,
|
|
14
14
|
task: true,
|
|
15
|
+
chat: true,
|
|
16
|
+
urgent: true,
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Resolve tools config with defaults.
|
|
21
|
+
* Only includes keys that are present in the input config or defaults.
|
|
19
22
|
*/
|
|
20
23
|
export function resolveToolsConfig(cfg?: FeishuToolsConfig): Required<FeishuToolsConfig> {
|
|
21
|
-
|
|
24
|
+
const res = {} as Required<FeishuToolsConfig>;
|
|
25
|
+
for (const key in DEFAULT_TOOLS_CONFIG) {
|
|
26
|
+
res[key] = cfg?.[key] ?? DEFAULT_TOOLS_CONFIG[key];
|
|
27
|
+
}
|
|
28
|
+
return res;
|
|
22
29
|
}
|
package/src/types.ts
CHANGED
|
@@ -30,13 +30,17 @@ export type FeishuMessageContext = {
|
|
|
30
30
|
senderId: string;
|
|
31
31
|
senderOpenId: string;
|
|
32
32
|
senderName?: string;
|
|
33
|
-
chatType: "p2p" | "group";
|
|
33
|
+
chatType: "p2p" | "group" | "private";
|
|
34
34
|
mentionedBot: boolean;
|
|
35
35
|
hasAnyMention?: boolean;
|
|
36
36
|
rootId?: string;
|
|
37
37
|
parentId?: string;
|
|
38
38
|
content: string;
|
|
39
39
|
contentType: string;
|
|
40
|
+
/** Media placeholder token (e.g. <media:audio>) when inbound message is media. */
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
/** Raw provider payload string (original event.message.content). */
|
|
43
|
+
rawPayload?: string;
|
|
40
44
|
/** Mention forward targets (excluding the bot itself) */
|
|
41
45
|
mentionTargets?: MentionTarget[];
|
|
42
46
|
/** Extracted message body (after removing @ placeholders) */
|
|
@@ -69,6 +73,9 @@ export type FeishuToolsConfig = {
|
|
|
69
73
|
perm?: boolean;
|
|
70
74
|
scopes?: boolean;
|
|
71
75
|
task?: boolean;
|
|
76
|
+
chat?: boolean;
|
|
77
|
+
/** Enable the feishu_urgent tool (buzz/urgent notifications). Enabled by default. */
|
|
78
|
+
urgent?: boolean;
|
|
72
79
|
};
|
|
73
80
|
|
|
74
81
|
export type DynamicAgentCreationConfig = {
|
package/src/typing.ts
CHANGED
|
@@ -7,13 +7,94 @@ import { resolveFeishuAccount } from "./accounts.js";
|
|
|
7
7
|
// Full list: https://github.com/go-lark/lark/blob/main/emoji.go
|
|
8
8
|
const TYPING_EMOJI = "Typing"; // Typing indicator emoji
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Feishu API error codes that indicate the caller should back off.
|
|
12
|
+
* These must propagate to the typing circuit breaker so the keepalive loop
|
|
13
|
+
* can trip and stop retrying.
|
|
14
|
+
*
|
|
15
|
+
* - 99991400: Rate limit (too many requests per second)
|
|
16
|
+
* - 99991403: Monthly API call quota exceeded
|
|
17
|
+
* - 429: Standard HTTP 429 returned as a Feishu SDK error code
|
|
18
|
+
*
|
|
19
|
+
* @see https://open.feishu.cn/document/server-docs/api-call-guide/generic-error-code
|
|
20
|
+
*/
|
|
21
|
+
const FEISHU_BACKOFF_CODES = new Set([99991400, 99991403, 429]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom error class for Feishu backoff conditions detected from non-throwing
|
|
25
|
+
* SDK responses. Carries a numeric `.code` so that `isFeishuBackoffError()`
|
|
26
|
+
* recognises it when the error is caught downstream.
|
|
27
|
+
*/
|
|
28
|
+
export class FeishuBackoffError extends Error {
|
|
29
|
+
code: number;
|
|
30
|
+
constructor(code: number) {
|
|
31
|
+
super(`Feishu API backoff: code ${code}`);
|
|
32
|
+
this.name = "FeishuBackoffError";
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check whether an error represents a rate-limit or quota-exceeded condition
|
|
39
|
+
* from the Feishu API that should stop the typing keepalive loop.
|
|
40
|
+
*
|
|
41
|
+
* Handles two shapes:
|
|
42
|
+
* 1. AxiosError with `response.status` and `response.data.code`
|
|
43
|
+
* 2. Feishu SDK error with a top-level `code` property
|
|
44
|
+
*/
|
|
45
|
+
export function isFeishuBackoffError(err: unknown): boolean {
|
|
46
|
+
if (typeof err !== "object" || err === null) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// AxiosError shape: err.response.status / err.response.data.code
|
|
51
|
+
const response = (err as { response?: { status?: number; data?: { code?: number } } }).response;
|
|
52
|
+
if (response) {
|
|
53
|
+
if (response.status === 429) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (typeof response.data?.code === "number" && FEISHU_BACKOFF_CODES.has(response.data.code)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Feishu SDK error shape: err.code
|
|
62
|
+
const code = (err as { code?: number }).code;
|
|
63
|
+
if (typeof code === "number" && FEISHU_BACKOFF_CODES.has(code)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check whether a Feishu SDK response object contains a backoff error code.
|
|
72
|
+
*
|
|
73
|
+
* The Feishu SDK sometimes returns a normal response (no throw) with an
|
|
74
|
+
* API-level error code in the response body. This must be detected so the
|
|
75
|
+
* circuit breaker can trip.
|
|
76
|
+
*/
|
|
77
|
+
export function getBackoffCodeFromResponse(response: unknown): number | undefined {
|
|
78
|
+
if (typeof response !== "object" || response === null) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const code = (response as { code?: number }).code;
|
|
82
|
+
if (typeof code === "number" && FEISHU_BACKOFF_CODES.has(code)) {
|
|
83
|
+
return code;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
10
88
|
export type TypingIndicatorState = {
|
|
11
89
|
messageId: string;
|
|
12
90
|
reactionId: string | null;
|
|
13
91
|
};
|
|
14
92
|
|
|
15
93
|
/**
|
|
16
|
-
* Add a typing indicator (reaction) to a message
|
|
94
|
+
* Add a typing indicator (reaction) to a message.
|
|
95
|
+
*
|
|
96
|
+
* Rate-limit and quota errors are re-thrown so the circuit breaker in
|
|
97
|
+
* `createTypingCallbacks` can trip and stop the keepalive loop.
|
|
17
98
|
*/
|
|
18
99
|
export async function addTypingIndicator(params: {
|
|
19
100
|
cfg: ClawdbotConfig;
|
|
@@ -36,17 +117,33 @@ export async function addTypingIndicator(params: {
|
|
|
36
117
|
},
|
|
37
118
|
});
|
|
38
119
|
|
|
120
|
+
// Feishu SDK may return a normal response with an API-level error code
|
|
121
|
+
// instead of throwing. Detect backoff codes and throw to trip the breaker.
|
|
122
|
+
const backoffCode = getBackoffCodeFromResponse(response);
|
|
123
|
+
if (backoffCode !== undefined) {
|
|
124
|
+
console.log(
|
|
125
|
+
`[feishu] typing indicator response contains backoff code ${backoffCode}, stopping keepalive`,
|
|
126
|
+
);
|
|
127
|
+
throw new FeishuBackoffError(backoffCode);
|
|
128
|
+
}
|
|
129
|
+
|
|
39
130
|
const reactionId = (response as any)?.data?.reaction_id ?? null;
|
|
40
131
|
return { messageId, reactionId };
|
|
41
132
|
} catch (err) {
|
|
42
|
-
|
|
133
|
+
if (isFeishuBackoffError(err)) {
|
|
134
|
+
console.log(`[feishu] typing indicator hit rate-limit/quota, stopping keepalive`);
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
// Silently fail for other non-critical errors (e.g. message deleted, permission issues)
|
|
43
138
|
console.log(`[feishu] failed to add typing indicator: ${err}`);
|
|
44
139
|
return { messageId, reactionId: null };
|
|
45
140
|
}
|
|
46
141
|
}
|
|
47
142
|
|
|
48
143
|
/**
|
|
49
|
-
* Remove a typing indicator (reaction) from a message
|
|
144
|
+
* Remove a typing indicator (reaction) from a message.
|
|
145
|
+
*
|
|
146
|
+
* Rate-limit and quota errors are re-thrown for the same reason as above.
|
|
50
147
|
*/
|
|
51
148
|
export async function removeTypingIndicator(params: {
|
|
52
149
|
cfg: ClawdbotConfig;
|
|
@@ -62,14 +159,27 @@ export async function removeTypingIndicator(params: {
|
|
|
62
159
|
const client = createFeishuClient(account);
|
|
63
160
|
|
|
64
161
|
try {
|
|
65
|
-
await client.im.messageReaction.delete({
|
|
162
|
+
const result = await client.im.messageReaction.delete({
|
|
66
163
|
path: {
|
|
67
164
|
message_id: state.messageId,
|
|
68
165
|
reaction_id: state.reactionId,
|
|
69
166
|
},
|
|
70
167
|
});
|
|
168
|
+
|
|
169
|
+
// Check for backoff codes in non-throwing SDK responses
|
|
170
|
+
const backoffCode = getBackoffCodeFromResponse(result);
|
|
171
|
+
if (backoffCode !== undefined) {
|
|
172
|
+
console.log(
|
|
173
|
+
`[feishu] typing indicator removal response contains backoff code ${backoffCode}, stopping keepalive`,
|
|
174
|
+
);
|
|
175
|
+
throw new FeishuBackoffError(backoffCode);
|
|
176
|
+
}
|
|
71
177
|
} catch (err) {
|
|
72
|
-
|
|
178
|
+
if (isFeishuBackoffError(err)) {
|
|
179
|
+
console.log(`[feishu] typing indicator removal hit rate-limit/quota, stopping keepalive`);
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
// Silently fail for other non-critical errors
|
|
73
183
|
console.log(`[feishu] failed to remove typing indicator: ${err}`);
|
|
74
184
|
}
|
|
75
185
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import { runFeishuApiCall, type FeishuApiResponse } from "../tools-common/feishu-api.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Urgency type for Feishu urgent messages.
|
|
6
|
+
* - "app": In-app buzz notification (default, no extra cost)
|
|
7
|
+
* - "sms": SMS push to the recipient's phone
|
|
8
|
+
* - "phone": Voice call to the recipient's phone
|
|
9
|
+
*/
|
|
10
|
+
export type FeishuUrgentType = "app" | "sms" | "phone";
|
|
11
|
+
|
|
12
|
+
type UrgentPayload = {
|
|
13
|
+
data: { user_id_list: string[] };
|
|
14
|
+
params: { user_id_type: "open_id" | "user_id" | "union_id" };
|
|
15
|
+
path: { message_id: string };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface UrgentResponse extends FeishuApiResponse {
|
|
19
|
+
data?: { invalid_user_id_list?: string[] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type LarkMessageWithUrgent = Lark.Client["im"]["message"] & {
|
|
23
|
+
urgentApp?: (payload: UrgentPayload) => Promise<UrgentResponse>;
|
|
24
|
+
urgentSms?: (payload: UrgentPayload) => Promise<UrgentResponse>;
|
|
25
|
+
urgentPhone?: (payload: UrgentPayload) => Promise<UrgentResponse>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Send an urgent (buzz) notification for an existing Feishu message.
|
|
30
|
+
*
|
|
31
|
+
* Calls the Feishu "urgent" API which sends a strong push notification
|
|
32
|
+
* to the specified recipients. The message must already be sent.
|
|
33
|
+
*
|
|
34
|
+
* Requires `im:message.urgent` scope (or `im:message.urgent:sms` / `im:message.urgent:phone` variants).
|
|
35
|
+
*
|
|
36
|
+
* Common errors:
|
|
37
|
+
* - Code 230024: Quota exceeded ("Reach the upper limit of urgent message").
|
|
38
|
+
* Check tenant quota in Feishu admin console > Cost Center.
|
|
39
|
+
* - Invalid user IDs cause HTTP 400 with descriptive message (not returned in
|
|
40
|
+
* `invalid_user_id_list` as documented).
|
|
41
|
+
*
|
|
42
|
+
* @see https://open.feishu.cn/document/server-docs/im-v1/message/urgent_app
|
|
43
|
+
* @see https://open.feishu.cn/document/server-docs/im-v1/message/urgent_sms
|
|
44
|
+
* @see https://open.feishu.cn/document/server-docs/im-v1/message/urgent_phone
|
|
45
|
+
*/
|
|
46
|
+
export async function urgentMessageFeishu(params: {
|
|
47
|
+
client: Lark.Client;
|
|
48
|
+
messageId: string;
|
|
49
|
+
userIds: string[];
|
|
50
|
+
urgentType?: FeishuUrgentType;
|
|
51
|
+
}): Promise<{ invalidUserList: string[] }> {
|
|
52
|
+
const { client, messageId, userIds, urgentType = "app" } = params;
|
|
53
|
+
|
|
54
|
+
const larkMessage = client.im.message as LarkMessageWithUrgent;
|
|
55
|
+
|
|
56
|
+
const payload: UrgentPayload = {
|
|
57
|
+
path: { message_id: messageId },
|
|
58
|
+
params: { user_id_type: "open_id" },
|
|
59
|
+
data: { user_id_list: userIds },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const methodMap = {
|
|
63
|
+
app: larkMessage.urgentApp?.bind(larkMessage),
|
|
64
|
+
sms: larkMessage.urgentSms?.bind(larkMessage),
|
|
65
|
+
phone: larkMessage.urgentPhone?.bind(larkMessage),
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
const method = methodMap[urgentType];
|
|
69
|
+
if (typeof method !== "function") {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Feishu urgent: SDK method not available for urgentType="${urgentType}". ` +
|
|
72
|
+
`Check that @larksuiteoapi/node-sdk is up to date.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const response = await runFeishuApiCall<UrgentResponse>(
|
|
77
|
+
`Feishu urgent message (${urgentType})`,
|
|
78
|
+
() => method(payload),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
invalidUserList: response.data?.invalid_user_id_list ?? [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
|
|
3
|
+
import { errorResult, json } from "../tools-common/feishu-api.js";
|
|
4
|
+
import { urgentMessageFeishu } from "./actions.js";
|
|
5
|
+
import { FeishuUrgentSchema, type FeishuUrgentParams } from "./schemas.js";
|
|
6
|
+
|
|
7
|
+
export function registerFeishuUrgentTools(api: OpenClawPluginApi) {
|
|
8
|
+
if (!api.config) {
|
|
9
|
+
api.logger.debug?.("feishu_urgent: No config available, skipping");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
14
|
+
api.logger.debug?.("feishu_urgent: No Feishu accounts configured, skipping");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "urgent")) {
|
|
19
|
+
api.logger.debug?.("feishu_urgent: urgent tool disabled in config");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
api.registerTool(
|
|
24
|
+
{
|
|
25
|
+
name: "feishu_urgent",
|
|
26
|
+
label: "Feishu Urgent",
|
|
27
|
+
description:
|
|
28
|
+
"Send an urgent (buzz) notification for an existing Feishu message. " +
|
|
29
|
+
"Supported urgent_type values: app (in-app buzz, default), sms (SMS push), phone (voice call). " +
|
|
30
|
+
"Requires the message_id of an already-sent message and the open_id list of recipients to buzz. " +
|
|
31
|
+
"Use this to escalate important messages that require immediate attention.",
|
|
32
|
+
parameters: FeishuUrgentSchema,
|
|
33
|
+
async execute(_toolCallId, params) {
|
|
34
|
+
const p = params as FeishuUrgentParams;
|
|
35
|
+
try {
|
|
36
|
+
return await withFeishuToolClient({
|
|
37
|
+
api,
|
|
38
|
+
toolName: "feishu_urgent",
|
|
39
|
+
requiredTool: "urgent",
|
|
40
|
+
run: async ({ client }) => {
|
|
41
|
+
const result = await urgentMessageFeishu({
|
|
42
|
+
client,
|
|
43
|
+
messageId: p.message_id,
|
|
44
|
+
userIds: p.user_ids,
|
|
45
|
+
urgentType: p.urgent_type ?? "app",
|
|
46
|
+
});
|
|
47
|
+
return json({
|
|
48
|
+
ok: true,
|
|
49
|
+
message_id: p.message_id,
|
|
50
|
+
urgent_type: p.urgent_type ?? "app",
|
|
51
|
+
invalid_user_list: result.invalidUserList,
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return errorResult(err);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{ name: "feishu_urgent" },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
api.logger.debug?.("feishu_urgent: Registered feishu_urgent tool");
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
const URGENT_TYPE_VALUES = ["app", "sms", "phone"] as const;
|
|
4
|
+
|
|
5
|
+
export const FeishuUrgentSchema = Type.Object({
|
|
6
|
+
message_id: Type.String({
|
|
7
|
+
description: "Message ID to send urgent notification for (e.g. om_xxx). The message must already be sent.",
|
|
8
|
+
}),
|
|
9
|
+
user_ids: Type.Array(Type.String(), {
|
|
10
|
+
description:
|
|
11
|
+
"List of open_id values to buzz. Recipients must be members of the chat where the message was sent.",
|
|
12
|
+
minItems: 1,
|
|
13
|
+
}),
|
|
14
|
+
urgent_type: Type.Optional(
|
|
15
|
+
Type.Unsafe<(typeof URGENT_TYPE_VALUES)[number]>({
|
|
16
|
+
type: "string",
|
|
17
|
+
enum: [...URGENT_TYPE_VALUES],
|
|
18
|
+
description:
|
|
19
|
+
"Urgency delivery method: app (in-app buzz, default), sms (SMS push), phone (voice call). Note: sms and phone may incur cost on the tenant.",
|
|
20
|
+
default: "app",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type FeishuUrgentParams = Static<typeof FeishuUrgentSchema>;
|