@xwang152/claw-lark 0.1.23 → 0.1.25

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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Group-level Policy and Access Control
3
+ *
4
+ * Handles per-group configuration for Lark/Feishu integration.
5
+ * Supports group-specific tool policies, mention requirements, and access control.
6
+ */
7
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
8
+ import type { LarkGroupConfig } from "./types.js";
9
+ export type LarkAllowlistMatch = {
10
+ allowed: boolean;
11
+ matchKey?: string;
12
+ matchSource?: "wildcard" | "id" | "name";
13
+ };
14
+ /**
15
+ * Resolve allowlist match for a sender.
16
+ *
17
+ * Checks if a sender is allowed based on allowlist configuration.
18
+ * Supports wildcards, IDs, and names.
19
+ */
20
+ export declare function resolveLarkAllowlistMatch(params: {
21
+ allowFrom: Array<string | number>;
22
+ senderId: string;
23
+ senderName?: string | null;
24
+ }): LarkAllowlistMatch;
25
+ /**
26
+ * Resolve group configuration for a specific group.
27
+ *
28
+ * Looks up group-specific settings from the configuration.
29
+ * Supports case-insensitive group ID matching.
30
+ */
31
+ export declare function resolveLarkGroupConfig(params: {
32
+ cfg?: OpenClawConfig;
33
+ groupId?: string | null;
34
+ accountId?: string;
35
+ }): LarkGroupConfig | undefined;
36
+ /**
37
+ * Resolve group tool policy for agent tool access control.
38
+ *
39
+ * Returns the tools configuration for a specific group.
40
+ */
41
+ export declare function resolveLarkGroupToolPolicy(params: {
42
+ cfg?: OpenClawConfig;
43
+ groupId?: string | null;
44
+ accountId?: string;
45
+ }): {
46
+ allow?: string[];
47
+ deny?: string[];
48
+ } | undefined;
49
+ /**
50
+ * Check if a group is allowed based on policy.
51
+ *
52
+ * @param groupPolicy - "open" | "allowlist" | "disabled"
53
+ * @param allowFrom - Allowlist configuration
54
+ * @param senderId - Sender's ID
55
+ * @param senderName - Sender's display name (optional)
56
+ */
57
+ export declare function isLarkGroupAllowed(params: {
58
+ groupPolicy: "open" | "allowlist" | "disabled";
59
+ allowFrom: Array<string | number>;
60
+ senderId: string;
61
+ senderName?: string | null;
62
+ }): boolean;
63
+ /**
64
+ * Resolve mention requirement for a group.
65
+ *
66
+ * Determines if @mention is required in a group based on
67
+ * group configuration or global default.
68
+ */
69
+ export declare function resolveLarkGroupRequireMention(params: {
70
+ cfg?: OpenClawConfig;
71
+ groupId?: string | null;
72
+ accountId?: string;
73
+ groupConfig?: LarkGroupConfig;
74
+ }): boolean;
75
+ /**
76
+ * Resolve reply-to mode for a group.
77
+ *
78
+ * Determines how replies should be threaded in a group chat.
79
+ */
80
+ export declare function resolveLarkReplyToMode(params: {
81
+ cfg?: OpenClawConfig;
82
+ groupId?: string | null;
83
+ accountId?: string;
84
+ }): "first" | "last" | "off";
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Group-level Policy and Access Control
3
+ *
4
+ * Handles per-group configuration for Lark/Feishu integration.
5
+ * Supports group-specific tool policies, mention requirements, and access control.
6
+ */
7
+ /**
8
+ * Extract Lark channel config from OpenClaw config
9
+ */
10
+ function getChannelConfig(cfg) {
11
+ return cfg.channels?.lark ?? {};
12
+ }
13
+ /**
14
+ * Resolve allowlist match for a sender.
15
+ *
16
+ * Checks if a sender is allowed based on allowlist configuration.
17
+ * Supports wildcards, IDs, and names.
18
+ */
19
+ export function resolveLarkAllowlistMatch(params) {
20
+ const allowFrom = params.allowFrom
21
+ .map((entry) => String(entry).trim().toLowerCase())
22
+ .filter(Boolean);
23
+ if (allowFrom.length === 0)
24
+ return { allowed: false };
25
+ if (allowFrom.includes("*")) {
26
+ return { allowed: true, matchKey: "*", matchSource: "wildcard" };
27
+ }
28
+ const senderId = params.senderId.toLowerCase();
29
+ if (allowFrom.includes(senderId)) {
30
+ return { allowed: true, matchKey: senderId, matchSource: "id" };
31
+ }
32
+ const senderName = params.senderName?.toLowerCase();
33
+ if (senderName && allowFrom.includes(senderName)) {
34
+ return { allowed: true, matchKey: senderName, matchSource: "name" };
35
+ }
36
+ return { allowed: false };
37
+ }
38
+ /**
39
+ * Resolve group configuration for a specific group.
40
+ *
41
+ * Looks up group-specific settings from the configuration.
42
+ * Supports case-insensitive group ID matching.
43
+ */
44
+ export function resolveLarkGroupConfig(params) {
45
+ const channelConfig = getChannelConfig(params.cfg ?? {});
46
+ const groupId = params.groupId?.trim();
47
+ const accountId = params.accountId ?? "default";
48
+ if (!groupId)
49
+ return undefined;
50
+ // First check account-level groups configuration
51
+ const accountConfig = channelConfig.accounts?.[accountId];
52
+ if (accountConfig?.groups) {
53
+ const direct = accountConfig.groups[groupId];
54
+ if (direct)
55
+ return direct;
56
+ // Try case-insensitive match
57
+ const lowered = groupId.toLowerCase();
58
+ const matchKey = Object.keys(accountConfig.groups).find((key) => key.toLowerCase() === lowered);
59
+ if (matchKey) {
60
+ return accountConfig.groups[matchKey];
61
+ }
62
+ }
63
+ // Then check channel-level groups configuration
64
+ if (channelConfig.groups) {
65
+ const direct = channelConfig.groups[groupId];
66
+ if (direct)
67
+ return direct;
68
+ // Try case-insensitive match
69
+ const lowered = groupId.toLowerCase();
70
+ const matchKey = Object.keys(channelConfig.groups).find((key) => key.toLowerCase() === lowered);
71
+ if (matchKey) {
72
+ return channelConfig.groups[matchKey];
73
+ }
74
+ }
75
+ return undefined;
76
+ }
77
+ /**
78
+ * Resolve group tool policy for agent tool access control.
79
+ *
80
+ * Returns the tools configuration for a specific group.
81
+ */
82
+ export function resolveLarkGroupToolPolicy(params) {
83
+ const groupConfig = resolveLarkGroupConfig(params);
84
+ return groupConfig?.tools;
85
+ }
86
+ /**
87
+ * Check if a group is allowed based on policy.
88
+ *
89
+ * @param groupPolicy - "open" | "allowlist" | "disabled"
90
+ * @param allowFrom - Allowlist configuration
91
+ * @param senderId - Sender's ID
92
+ * @param senderName - Sender's display name (optional)
93
+ */
94
+ export function isLarkGroupAllowed(params) {
95
+ const { groupPolicy, allowFrom, senderId, senderName } = params;
96
+ if (groupPolicy === "disabled")
97
+ return false;
98
+ if (groupPolicy === "open")
99
+ return true;
100
+ return resolveLarkAllowlistMatch({
101
+ allowFrom,
102
+ senderId,
103
+ senderName,
104
+ }).allowed;
105
+ }
106
+ /**
107
+ * Resolve mention requirement for a group.
108
+ *
109
+ * Determines if @mention is required in a group based on
110
+ * group configuration or global default.
111
+ */
112
+ export function resolveLarkGroupRequireMention(params) {
113
+ // If explicit group config is provided, use it
114
+ if (params.groupConfig) {
115
+ return params.groupConfig.requireMention ?? true;
116
+ }
117
+ // Otherwise, resolve from config
118
+ const groupConfig = resolveLarkGroupConfig(params);
119
+ if (groupConfig) {
120
+ return groupConfig.requireMention ?? true;
121
+ }
122
+ // Fall back to channel/account default
123
+ const channelConfig = getChannelConfig(params.cfg ?? {});
124
+ const accountId = params.accountId ?? "default";
125
+ const accountConfig = channelConfig.accounts?.[accountId];
126
+ return accountConfig?.requireMention ?? true;
127
+ }
128
+ /**
129
+ * Resolve reply-to mode for a group.
130
+ *
131
+ * Determines how replies should be threaded in a group chat.
132
+ */
133
+ export function resolveLarkReplyToMode(params) {
134
+ const groupConfig = resolveLarkGroupConfig(params);
135
+ return groupConfig?.replyToMode ?? "first";
136
+ }
137
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Sender Name Resolution
3
+ *
4
+ * Resolves sender display names from open_id using Lark contact API.
5
+ * Caches results for 10 minutes to avoid excessive API calls.
6
+ *
7
+ * This is useful in group chats where the agent needs to distinguish
8
+ * who is speaking (by name rather than just open_id).
9
+ */
10
+ import type { ResolvedLarkAccount } from "./types.js";
11
+ /**
12
+ * Resolve sender display name from open_id.
13
+ *
14
+ * Uses the contact.user.get API to fetch the user's display name.
15
+ * Results are cached for 10 minutes to reduce API calls.
16
+ *
17
+ * @param params - Account and sender open_id
18
+ * @returns Display name or undefined if not found
19
+ */
20
+ export declare function resolveLarkSenderName(params: {
21
+ account: ResolvedLarkAccount;
22
+ senderOpenId: string;
23
+ log?: (...args: any[]) => void;
24
+ }): Promise<string | undefined>;
25
+ /**
26
+ * Clear the sender name cache.
27
+ *
28
+ * This can be useful for testing or when you need fresh data.
29
+ */
30
+ export declare function clearSenderNameCache(): void;
31
+ /**
32
+ * Get cache statistics.
33
+ *
34
+ * Useful for monitoring and debugging.
35
+ */
36
+ export declare function getSenderNameCacheStats(): {
37
+ size: number;
38
+ entries: Array<{
39
+ openId: string;
40
+ name: string;
41
+ expireAt: number;
42
+ }>;
43
+ };
44
+ /**
45
+ * Prefetch sender names for a list of open_ids.
46
+ *
47
+ * This can be useful for warming up the cache before processing
48
+ * a batch of messages.
49
+ */
50
+ export declare function prefetchLarkSenderNames(params: {
51
+ account: ResolvedLarkAccount;
52
+ senderOpenIds: string[];
53
+ log?: (...args: any[]) => void;
54
+ }): Promise<void>;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Sender Name Resolution
3
+ *
4
+ * Resolves sender display names from open_id using Lark contact API.
5
+ * Caches results for 10 minutes to avoid excessive API calls.
6
+ *
7
+ * This is useful in group chats where the agent needs to distinguish
8
+ * who is speaking (by name rather than just open_id).
9
+ */
10
+ import { createLarkClient } from "./client.js";
11
+ // Cache display names by open_id to avoid an API call on every message
12
+ const SENDER_NAME_TTL_MS = 10 * 60 * 1000; // 10 minutes
13
+ const senderNameCache = new Map();
14
+ /**
15
+ * Resolve sender display name from open_id.
16
+ *
17
+ * Uses the contact.user.get API to fetch the user's display name.
18
+ * Results are cached for 10 minutes to reduce API calls.
19
+ *
20
+ * @param params - Account and sender open_id
21
+ * @returns Display name or undefined if not found
22
+ */
23
+ export async function resolveLarkSenderName(params) {
24
+ const { account, senderOpenId, log = console.log } = params;
25
+ if (!senderOpenId)
26
+ return undefined;
27
+ // Check cache first
28
+ const cached = senderNameCache.get(senderOpenId);
29
+ const now = Date.now();
30
+ if (cached && cached.expireAt > now) {
31
+ return cached.name;
32
+ }
33
+ try {
34
+ const client = createLarkClient(account);
35
+ // contact/v3/user/:user_id?user_id_type=open_id
36
+ const res = await client.contact.user.get({
37
+ path: { user_id: senderOpenId },
38
+ params: { user_id_type: "open_id" },
39
+ });
40
+ // Try multiple name fields in order of preference
41
+ const name = res?.data?.user?.name ||
42
+ res?.data?.user?.display_name ||
43
+ res?.data?.user?.nickname ||
44
+ res?.data?.user?.en_name;
45
+ if (name && typeof name === "string") {
46
+ // Cache the result
47
+ senderNameCache.set(senderOpenId, { name, expireAt: now + SENDER_NAME_TTL_MS });
48
+ return name;
49
+ }
50
+ return undefined;
51
+ }
52
+ catch (err) {
53
+ // Best-effort. Don't fail message handling if name lookup fails.
54
+ log(`[lark] Failed to resolve sender name for ${senderOpenId}: ${String(err)}`);
55
+ return undefined;
56
+ }
57
+ }
58
+ /**
59
+ * Clear the sender name cache.
60
+ *
61
+ * This can be useful for testing or when you need fresh data.
62
+ */
63
+ export function clearSenderNameCache() {
64
+ senderNameCache.clear();
65
+ }
66
+ /**
67
+ * Get cache statistics.
68
+ *
69
+ * Useful for monitoring and debugging.
70
+ */
71
+ export function getSenderNameCacheStats() {
72
+ const now = Date.now();
73
+ const entries = Array.from(senderNameCache.entries())
74
+ .filter(([, value]) => value.expireAt > now)
75
+ .map(([openId, value]) => ({
76
+ openId,
77
+ name: value.name,
78
+ expireAt: value.expireAt,
79
+ }));
80
+ return {
81
+ size: entries.length,
82
+ entries,
83
+ };
84
+ }
85
+ /**
86
+ * Prefetch sender names for a list of open_ids.
87
+ *
88
+ * This can be useful for warming up the cache before processing
89
+ * a batch of messages.
90
+ */
91
+ export async function prefetchLarkSenderNames(params) {
92
+ const { account, senderOpenIds, log = console.log } = params;
93
+ // Only fetch names that aren't already cached
94
+ const now = Date.now();
95
+ const uncachedIds = senderOpenIds.filter((id) => {
96
+ const cached = senderNameCache.get(id);
97
+ return !cached || cached.expireAt <= now;
98
+ });
99
+ // Process in parallel with a limit to avoid overwhelming the API
100
+ const batchSize = 10;
101
+ for (let i = 0; i < uncachedIds.length; i += batchSize) {
102
+ const batch = uncachedIds.slice(i, i + batchSize);
103
+ await Promise.all(batch.map((id) => resolveLarkSenderName({ account, senderOpenId: id, log }).catch(() => {
104
+ // Ignore errors - best effort prefetch
105
+ })));
106
+ }
107
+ }
108
+ //# sourceMappingURL=sender-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sender-name.js","sourceRoot":"","sources":["../../src/sender-name.ts"],"names":[],"mappings":"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"}
@@ -30,6 +30,7 @@ export interface LarkAccountConfig {
30
30
  mediaMaxMb?: number;
31
31
  renderMode?: RenderMode;
32
32
  historyLimit?: number;
33
+ groups?: Record<string, LarkGroupConfig>;
33
34
  }
34
35
  /**
35
36
  * Fully resolved account with all defaults applied.
@@ -61,6 +62,20 @@ export interface ResolvedLarkAccount {
61
62
  export interface LarkChannelConfig {
62
63
  enabled?: boolean;
63
64
  accounts?: Record<string, LarkAccountConfig>;
65
+ groups?: Record<string, LarkGroupConfig>;
66
+ }
67
+ /**
68
+ * Group-level configuration for specific Lark groups
69
+ */
70
+ export interface LarkGroupConfig {
71
+ enabled?: boolean;
72
+ requireMention?: boolean;
73
+ replyToMode?: "first" | "last" | "off";
74
+ tools?: {
75
+ allow?: string[];
76
+ deny?: string[];
77
+ };
78
+ allowFrom?: string[];
64
79
  }
65
80
  /**
66
81
  * Lark message event payload from im.message.receive_v1
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Typing Indicators for Lark/Feishu
3
+ *
4
+ * Since Lark/Feishu doesn't have a native typing indicator API,
5
+ * we use emoji reactions as a substitute.
6
+ *
7
+ * When the bot starts processing a message, it adds a "Typing" reaction.
8
+ * When the reply is sent, it removes the reaction.
9
+ *
10
+ * Reference: https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
11
+ */
12
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
13
+ export type TypingIndicatorState = {
14
+ messageId: string;
15
+ reactionId: string | null;
16
+ };
17
+ /**
18
+ * Add a typing indicator (reaction) to a message.
19
+ *
20
+ * This adds a visual indicator that the bot is "typing" or processing the message.
21
+ * Uses the "Typing" emoji reaction if available, falls back gracefully if not.
22
+ *
23
+ * @param params - Configuration and message ID
24
+ * @returns State object containing message ID and reaction ID (for later removal)
25
+ */
26
+ export declare function addTypingIndicator(params: {
27
+ cfg: OpenClawConfig;
28
+ messageId: string;
29
+ accountId?: string;
30
+ }): Promise<TypingIndicatorState>;
31
+ /**
32
+ * Remove a typing indicator (reaction) from a message.
33
+ *
34
+ * This should be called after the reply is sent to clean up the typing indicator.
35
+ *
36
+ * @param params - Configuration and typing indicator state
37
+ */
38
+ export declare function removeTypingIndicator(params: {
39
+ cfg: OpenClawConfig;
40
+ state: TypingIndicatorState;
41
+ accountId?: string;
42
+ }): Promise<void>;
43
+ /**
44
+ * Create a typing indicator controller for managing the lifecycle.
45
+ *
46
+ * This is useful for ensuring the typing indicator is removed even if errors occur.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const typing = createTypingController({ cfg, messageId });
51
+ * await typing.start();
52
+ * try {
53
+ * // ... process message and send reply ...
54
+ * } finally {
55
+ * await typing.stop();
56
+ * }
57
+ * ```
58
+ */
59
+ export declare function createTypingController(params: {
60
+ cfg: OpenClawConfig;
61
+ messageId: string;
62
+ accountId?: string;
63
+ }): {
64
+ /**
65
+ * Start the typing indicator
66
+ */
67
+ start(): Promise<void>;
68
+ /**
69
+ * Stop the typing indicator
70
+ */
71
+ stop(): Promise<void>;
72
+ /**
73
+ * Get current state
74
+ */
75
+ getState(): TypingIndicatorState | null;
76
+ };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Typing Indicators for Lark/Feishu
3
+ *
4
+ * Since Lark/Feishu doesn't have a native typing indicator API,
5
+ * we use emoji reactions as a substitute.
6
+ *
7
+ * When the bot starts processing a message, it adds a "Typing" reaction.
8
+ * When the reply is sent, it removes the reaction.
9
+ *
10
+ * Reference: https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
11
+ */
12
+ import { createLarkClient } from "./client.js";
13
+ // Lark emoji type for typing indicator
14
+ // Note: "Typing" may not be available in all Lark instances
15
+ // Fallback options: "THUMBSUP", "OK", "SMILE"
16
+ const TYPING_EMOJI = "Typing";
17
+ /**
18
+ * Extract Lark channel config from OpenClaw config
19
+ */
20
+ function getChannelConfig(cfg) {
21
+ return cfg.channels?.lark ?? {};
22
+ }
23
+ /**
24
+ * Add a typing indicator (reaction) to a message.
25
+ *
26
+ * This adds a visual indicator that the bot is "typing" or processing the message.
27
+ * Uses the "Typing" emoji reaction if available, falls back gracefully if not.
28
+ *
29
+ * @param params - Configuration and message ID
30
+ * @returns State object containing message ID and reaction ID (for later removal)
31
+ */
32
+ export async function addTypingIndicator(params) {
33
+ const { cfg, messageId, accountId = "default" } = params;
34
+ const channelConfig = getChannelConfig(cfg);
35
+ const accountConfig = channelConfig.accounts?.[accountId];
36
+ if (!accountConfig?.appId || !accountConfig?.appSecret) {
37
+ return { messageId, reactionId: null };
38
+ }
39
+ const client = createLarkClient({
40
+ accountId,
41
+ enabled: true,
42
+ configured: true,
43
+ appId: accountConfig.appId,
44
+ appSecret: accountConfig.appSecret,
45
+ domain: accountConfig.domain ?? "feishu",
46
+ connectionMode: "websocket",
47
+ webhookPort: 3000,
48
+ dmPolicy: "open",
49
+ dmAllowlist: [],
50
+ groupPolicy: "open",
51
+ groupAllowlist: [],
52
+ requireMention: true,
53
+ mediaMaxMb: 30,
54
+ renderMode: "auto",
55
+ historyLimit: 10,
56
+ });
57
+ try {
58
+ const response = await client.im.messageReaction.create({
59
+ path: { message_id: messageId },
60
+ data: {
61
+ reaction_type: { emoji_type: TYPING_EMOJI },
62
+ },
63
+ });
64
+ const reactionId = response?.data?.reaction_id ?? null;
65
+ return { messageId, reactionId };
66
+ }
67
+ catch (err) {
68
+ // Silently fail - typing indicator is not critical
69
+ console.log(`[lark] Failed to add typing indicator: ${err}`);
70
+ return { messageId, reactionId: null };
71
+ }
72
+ }
73
+ /**
74
+ * Remove a typing indicator (reaction) from a message.
75
+ *
76
+ * This should be called after the reply is sent to clean up the typing indicator.
77
+ *
78
+ * @param params - Configuration and typing indicator state
79
+ */
80
+ export async function removeTypingIndicator(params) {
81
+ const { cfg, state, accountId = "default" } = params;
82
+ if (!state.reactionId)
83
+ return;
84
+ const channelConfig = getChannelConfig(cfg);
85
+ const accountConfig = channelConfig.accounts?.[accountId];
86
+ if (!accountConfig?.appId || !accountConfig?.appSecret)
87
+ return;
88
+ const client = createLarkClient({
89
+ accountId,
90
+ enabled: true,
91
+ configured: true,
92
+ appId: accountConfig.appId,
93
+ appSecret: accountConfig.appSecret,
94
+ domain: accountConfig.domain ?? "feishu",
95
+ connectionMode: "websocket",
96
+ webhookPort: 3000,
97
+ dmPolicy: "open",
98
+ dmAllowlist: [],
99
+ groupPolicy: "open",
100
+ groupAllowlist: [],
101
+ requireMention: true,
102
+ mediaMaxMb: 30,
103
+ renderMode: "auto",
104
+ historyLimit: 10,
105
+ });
106
+ try {
107
+ await client.im.messageReaction.delete({
108
+ path: {
109
+ message_id: state.messageId,
110
+ reaction_id: state.reactionId,
111
+ },
112
+ });
113
+ }
114
+ catch (err) {
115
+ // Silently fail - cleanup is not critical
116
+ console.log(`[lark] Failed to remove typing indicator: ${err}`);
117
+ }
118
+ }
119
+ /**
120
+ * Create a typing indicator controller for managing the lifecycle.
121
+ *
122
+ * This is useful for ensuring the typing indicator is removed even if errors occur.
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * const typing = createTypingController({ cfg, messageId });
127
+ * await typing.start();
128
+ * try {
129
+ * // ... process message and send reply ...
130
+ * } finally {
131
+ * await typing.stop();
132
+ * }
133
+ * ```
134
+ */
135
+ export function createTypingController(params) {
136
+ let state = null;
137
+ return {
138
+ /**
139
+ * Start the typing indicator
140
+ */
141
+ async start() {
142
+ state = await addTypingIndicator(params);
143
+ },
144
+ /**
145
+ * Stop the typing indicator
146
+ */
147
+ async stop() {
148
+ if (state) {
149
+ await removeTypingIndicator({
150
+ cfg: params.cfg,
151
+ state,
152
+ accountId: params.accountId,
153
+ });
154
+ state = null;
155
+ }
156
+ },
157
+ /**
158
+ * Get current state
159
+ */
160
+ getState() {
161
+ return state;
162
+ },
163
+ };
164
+ }
165
+ //# sourceMappingURL=typing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typing.js","sourceRoot":"","sources":["../../src/typing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xwang152/claw-lark",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "description": "Lark/Feishu channel plugin for OpenClaw with WebSocket and Webhook support",
6
6
  "license": "MIT",