@openclaw-plugins/feishu-plus 0.1.7-fork.1
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/LICENSE +21 -0
- package/README.md +560 -0
- package/index.ts +74 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +65 -0
- package/skills/feishu-doc/SKILL.md +99 -0
- package/skills/feishu-doc/references/block-types.md +102 -0
- package/skills/feishu-drive/SKILL.md +96 -0
- package/skills/feishu-perm/SKILL.md +90 -0
- package/skills/feishu-wiki/SKILL.md +96 -0
- package/src/accounts.ts +140 -0
- package/src/bitable.ts +441 -0
- package/src/bot.ts +919 -0
- package/src/channel.ts +335 -0
- package/src/client.ts +114 -0
- package/src/config-schema.ts +199 -0
- package/src/directory.ts +165 -0
- package/src/doc-schema.ts +47 -0
- package/src/docx.ts +525 -0
- package/src/drive-schema.ts +46 -0
- package/src/drive.ts +207 -0
- package/src/dynamic-agent.ts +131 -0
- package/src/media.ts +523 -0
- package/src/mention.ts +121 -0
- package/src/monitor.ts +190 -0
- package/src/onboarding.ts +358 -0
- package/src/outbound.ts +40 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +166 -0
- package/src/policy.ts +92 -0
- package/src/probe.ts +115 -0
- package/src/reactions.ts +160 -0
- package/src/reply-dispatcher.ts +225 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +492 -0
- package/src/stream.ts +160 -0
- package/src/targets.ts +58 -0
- package/src/tools-config.ts +21 -0
- package/src/types.ts +77 -0
- package/src/typing.ts +75 -0
- package/src/wiki-schema.ts +55 -0
- package/src/wiki.ts +224 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createReplyPrefixContext,
|
|
3
|
+
createTypingCallbacks,
|
|
4
|
+
logTypingFailure,
|
|
5
|
+
type ClawdbotConfig,
|
|
6
|
+
type RuntimeEnv,
|
|
7
|
+
type ReplyPayload,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
9
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
10
|
+
import { sendMessageFeishu, sendMarkdownCardFeishu } from "./send.js";
|
|
11
|
+
import type { FeishuConfig } from "./types.js";
|
|
12
|
+
import type { MentionTarget } from "./mention.js";
|
|
13
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
14
|
+
import {
|
|
15
|
+
addTypingIndicator,
|
|
16
|
+
removeTypingIndicator,
|
|
17
|
+
type TypingIndicatorState,
|
|
18
|
+
} from "./typing.js";
|
|
19
|
+
import { createFeishuStreamingHandler, FeishuStream } from "./stream.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect if text contains markdown elements that benefit from card rendering.
|
|
23
|
+
* Used by auto render mode.
|
|
24
|
+
*/
|
|
25
|
+
function shouldUseCard(text: string): boolean {
|
|
26
|
+
// Code blocks (fenced)
|
|
27
|
+
if (/```[\s\S]*?```/.test(text)) return true;
|
|
28
|
+
// Tables (at least header + separator row with |)
|
|
29
|
+
if (/\|.+\|[\r\n]+\|[-:| ]+\|/.test(text)) return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type CreateFeishuReplyDispatcherParams = {
|
|
34
|
+
cfg: ClawdbotConfig;
|
|
35
|
+
agentId: string;
|
|
36
|
+
runtime: RuntimeEnv;
|
|
37
|
+
chatId: string;
|
|
38
|
+
replyToMessageId?: string;
|
|
39
|
+
/** Mention targets, will be auto-included in replies */
|
|
40
|
+
mentionTargets?: MentionTarget[];
|
|
41
|
+
/** Account ID for multi-account support */
|
|
42
|
+
accountId?: string;
|
|
43
|
+
/** Enable streaming mode for card-based real-time updates */
|
|
44
|
+
enableStreaming?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) {
|
|
48
|
+
const core = getFeishuRuntime();
|
|
49
|
+
const { cfg, agentId, chatId, replyToMessageId, mentionTargets, accountId, enableStreaming } = params;
|
|
50
|
+
|
|
51
|
+
// Resolve account for config access
|
|
52
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
53
|
+
|
|
54
|
+
const prefixContext = createReplyPrefixContext({
|
|
55
|
+
cfg,
|
|
56
|
+
agentId,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Feishu doesn't have a native typing indicator API.
|
|
60
|
+
// We use message reactions as a typing indicator substitute.
|
|
61
|
+
let typingState: TypingIndicatorState | null = null;
|
|
62
|
+
|
|
63
|
+
// Streaming state
|
|
64
|
+
let streamInstance: FeishuStream | null = null;
|
|
65
|
+
let streamingMessageId: string | null = null;
|
|
66
|
+
|
|
67
|
+
const typingCallbacks = createTypingCallbacks({
|
|
68
|
+
start: async () => {
|
|
69
|
+
// Skip typing indicator when streaming is enabled
|
|
70
|
+
if (enableStreaming) {
|
|
71
|
+
params.runtime.log?.(`feishu[${account.accountId}]: streaming enabled, skipping typing indicator`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!replyToMessageId) return;
|
|
75
|
+
typingState = await addTypingIndicator({ cfg, messageId: replyToMessageId, accountId });
|
|
76
|
+
params.runtime.log?.(`feishu[${account.accountId}]: added typing indicator reaction`);
|
|
77
|
+
},
|
|
78
|
+
stop: async () => {
|
|
79
|
+
if (!typingState) return;
|
|
80
|
+
await removeTypingIndicator({ cfg, state: typingState, accountId });
|
|
81
|
+
typingState = null;
|
|
82
|
+
params.runtime.log?.(`feishu[${account.accountId}]: removed typing indicator reaction`);
|
|
83
|
+
},
|
|
84
|
+
onStartError: (err) => {
|
|
85
|
+
logTypingFailure({
|
|
86
|
+
log: (message) => params.runtime.log?.(message),
|
|
87
|
+
channel: "feishu",
|
|
88
|
+
action: "start",
|
|
89
|
+
error: err,
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
onStopError: (err) => {
|
|
93
|
+
logTypingFailure({
|
|
94
|
+
log: (message) => params.runtime.log?.(message),
|
|
95
|
+
channel: "feishu",
|
|
96
|
+
action: "stop",
|
|
97
|
+
error: err,
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const textChunkLimit = core.channel.text.resolveTextChunkLimit({
|
|
103
|
+
cfg,
|
|
104
|
+
channel: "feishu",
|
|
105
|
+
defaultLimit: 4000,
|
|
106
|
+
});
|
|
107
|
+
const chunkMode = core.channel.text.resolveChunkMode(cfg, "feishu");
|
|
108
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
109
|
+
cfg,
|
|
110
|
+
channel: "feishu",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
114
|
+
core.channel.reply.createReplyDispatcherWithTyping({
|
|
115
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
116
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
117
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
118
|
+
onReplyStart: typingCallbacks.onReplyStart,
|
|
119
|
+
deliver: async (payload: ReplyPayload) => {
|
|
120
|
+
params.runtime.log?.(`feishu[${account.accountId}] deliver called: text=${payload.text?.slice(0, 100)}`);
|
|
121
|
+
const text = payload.text ?? "";
|
|
122
|
+
if (!text.trim()) {
|
|
123
|
+
params.runtime.log?.(`feishu[${account.accountId}] deliver: empty text, skipping`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check render mode: auto (default), raw, or card
|
|
128
|
+
const feishuCfg = account.config;
|
|
129
|
+
const renderMode = feishuCfg?.renderMode ?? "auto";
|
|
130
|
+
|
|
131
|
+
// Determine if we should use card for this message
|
|
132
|
+
const useCard =
|
|
133
|
+
renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
|
|
134
|
+
|
|
135
|
+
// Only include @mentions in the first chunk (avoid duplicate @s)
|
|
136
|
+
let isFirstChunk = true;
|
|
137
|
+
|
|
138
|
+
if (useCard) {
|
|
139
|
+
// Card mode: send as interactive card with markdown rendering
|
|
140
|
+
const chunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode);
|
|
141
|
+
params.runtime.log?.(`feishu[${account.accountId}] deliver: sending ${chunks.length} card chunks to ${chatId}`);
|
|
142
|
+
for (const chunk of chunks) {
|
|
143
|
+
await sendMarkdownCardFeishu({
|
|
144
|
+
cfg,
|
|
145
|
+
to: chatId,
|
|
146
|
+
text: chunk,
|
|
147
|
+
replyToMessageId,
|
|
148
|
+
mentions: isFirstChunk ? mentionTargets : undefined,
|
|
149
|
+
accountId,
|
|
150
|
+
});
|
|
151
|
+
isFirstChunk = false;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
// Raw mode: send as plain text with table conversion
|
|
155
|
+
const converted = core.channel.text.convertMarkdownTables(text, tableMode);
|
|
156
|
+
const chunks = core.channel.text.chunkTextWithMode(converted, textChunkLimit, chunkMode);
|
|
157
|
+
params.runtime.log?.(`feishu[${account.accountId}] deliver: sending ${chunks.length} text chunks to ${chatId}`);
|
|
158
|
+
for (const chunk of chunks) {
|
|
159
|
+
await sendMessageFeishu({
|
|
160
|
+
cfg,
|
|
161
|
+
to: chatId,
|
|
162
|
+
text: chunk,
|
|
163
|
+
replyToMessageId,
|
|
164
|
+
mentions: isFirstChunk ? mentionTargets : undefined,
|
|
165
|
+
accountId,
|
|
166
|
+
});
|
|
167
|
+
isFirstChunk = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
onError: (err, info) => {
|
|
172
|
+
params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(err)}`);
|
|
173
|
+
typingCallbacks.onIdle?.();
|
|
174
|
+
},
|
|
175
|
+
onIdle: typingCallbacks.onIdle,
|
|
176
|
+
// Enable partial reply callback for streaming
|
|
177
|
+
onPartialReply: enableStreaming
|
|
178
|
+
? async (text: string, isFinal: boolean) => {
|
|
179
|
+
// Initialize stream on first partial reply
|
|
180
|
+
if (!streamInstance) {
|
|
181
|
+
// Send initial card message
|
|
182
|
+
const result = await sendMarkdownCardFeishu({
|
|
183
|
+
cfg,
|
|
184
|
+
to: chatId,
|
|
185
|
+
text: text,
|
|
186
|
+
replyToMessageId,
|
|
187
|
+
mentions: mentionTargets,
|
|
188
|
+
accountId,
|
|
189
|
+
});
|
|
190
|
+
streamingMessageId = result.messageId;
|
|
191
|
+
|
|
192
|
+
// Create stream handler for subsequent updates
|
|
193
|
+
if (streamingMessageId) {
|
|
194
|
+
const handler = createFeishuStreamingHandler({
|
|
195
|
+
cfg,
|
|
196
|
+
messageId: streamingMessageId,
|
|
197
|
+
accountId,
|
|
198
|
+
});
|
|
199
|
+
streamInstance = handler.stream;
|
|
200
|
+
}
|
|
201
|
+
} else if (streamInstance) {
|
|
202
|
+
// Update existing stream
|
|
203
|
+
if (isFinal) {
|
|
204
|
+
await streamInstance.finalize(text);
|
|
205
|
+
streamInstance.destroy();
|
|
206
|
+
streamInstance = null;
|
|
207
|
+
} else {
|
|
208
|
+
await streamInstance.update(text);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
: undefined,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
dispatcher,
|
|
217
|
+
replyOptions: {
|
|
218
|
+
...replyOptions,
|
|
219
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
220
|
+
},
|
|
221
|
+
markDispatchIdle,
|
|
222
|
+
// Expose stream instance for external control
|
|
223
|
+
streamInstance: () => streamInstance,
|
|
224
|
+
};
|
|
225
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setFeishuRuntime(next: PluginRuntime) {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getFeishuRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error("Feishu runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|