@gakr-gakr/feishu 0.1.0
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/api.ts +32 -0
- package/autobot.plugin.json +180 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +82 -0
- package/package.json +62 -0
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.ts +13 -0
- package/skills/feishu-doc/SKILL.md +211 -0
- package/skills/feishu-doc/references/block-types.md +103 -0
- package/skills/feishu-drive/SKILL.md +97 -0
- package/skills/feishu-perm/SKILL.md +119 -0
- package/skills/feishu-wiki/SKILL.md +113 -0
- package/src/accounts.ts +333 -0
- package/src/agent-config.ts +21 -0
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.ts +104 -0
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.ts +762 -0
- package/src/bot-content.ts +485 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.ts +1703 -0
- package/src/card-action.ts +447 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +54 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.ts +1423 -0
- package/src/chat-schema.ts +25 -0
- package/src/chat.ts +188 -0
- package/src/client-timeout.ts +42 -0
- package/src/client.ts +262 -0
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.ts +303 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.ts +406 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.ts +335 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +141 -0
- package/src/dedupe-key.ts +72 -0
- package/src/directory.static.ts +61 -0
- package/src/directory.ts +124 -0
- package/src/doc-schema.ts +182 -0
- package/src/docx-batch-insert.ts +223 -0
- package/src/docx-color-text.ts +154 -0
- package/src/docx-table-ops.ts +316 -0
- package/src/docx-types.ts +38 -0
- package/src/docx.ts +1596 -0
- package/src/drive-schema.ts +92 -0
- package/src/drive.ts +829 -0
- package/src/dynamic-agent.ts +143 -0
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +19 -0
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.ts +1105 -0
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +114 -0
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +10 -0
- package/src/monitor.account.ts +492 -0
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +421 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.message-handler.ts +350 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.startup.ts +74 -0
- package/src/monitor.state.ts +170 -0
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +46 -0
- package/src/monitor.transport.ts +451 -0
- package/src/monitor.ts +100 -0
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.ts +785 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +170 -0
- package/src/pins.ts +108 -0
- package/src/policy.ts +321 -0
- package/src/post.ts +275 -0
- package/src/probe.ts +166 -0
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +123 -0
- package/src/reasoning-preview.ts +28 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.ts +748 -0
- package/src/runtime.ts +9 -0
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -0
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +80 -0
- package/src/send-target.ts +35 -0
- package/src/send.ts +861 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.ts +86 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/setup-surface.ts +618 -0
- package/src/streaming-card.ts +571 -0
- package/src/subagent-hooks.ts +413 -0
- package/src/targets.ts +97 -0
- package/src/thread-bindings.ts +331 -0
- package/src/tool-account.ts +93 -0
- package/src/tool-factory-test-harness.ts +79 -0
- package/src/tool-result.ts +16 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +106 -0
- package/src/typing.ts +214 -0
- package/src/wiki-schema.ts +69 -0
- package/src/wiki.ts +270 -0
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { resolveChannelConfigWrites } from "autobot/plugin-sdk/channel-config-writes";
|
|
2
|
+
import type { ResolvedAgentRoute } from "autobot/plugin-sdk/routing";
|
|
3
|
+
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
4
|
+
import { createFeishuClient } from "./client.js";
|
|
5
|
+
import { createFeishuCommentReplyDispatcher } from "./comment-dispatcher.js";
|
|
6
|
+
import {
|
|
7
|
+
createChannelPairingController,
|
|
8
|
+
type ClawdbotConfig,
|
|
9
|
+
type RuntimeEnv,
|
|
10
|
+
} from "./comment-handler-runtime-api.js";
|
|
11
|
+
import { buildFeishuCommentTarget } from "./comment-target.js";
|
|
12
|
+
import { deliverCommentThreadText } from "./drive.js";
|
|
13
|
+
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
|
14
|
+
import {
|
|
15
|
+
resolveDriveCommentEventTurn,
|
|
16
|
+
type FeishuDriveCommentNoticeEvent,
|
|
17
|
+
} from "./monitor.comment.js";
|
|
18
|
+
import { resolveFeishuDmIngressAccess } from "./policy.js";
|
|
19
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
20
|
+
import type { DynamicAgentCreationConfig } from "./types.js";
|
|
21
|
+
|
|
22
|
+
type HandleFeishuCommentEventParams = {
|
|
23
|
+
cfg: ClawdbotConfig;
|
|
24
|
+
accountId: string;
|
|
25
|
+
runtime?: RuntimeEnv;
|
|
26
|
+
event: FeishuDriveCommentNoticeEvent;
|
|
27
|
+
botOpenId?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function buildCommentSessionKey(params: {
|
|
31
|
+
core: ReturnType<typeof getFeishuRuntime>;
|
|
32
|
+
route: ResolvedAgentRoute;
|
|
33
|
+
fileType: string;
|
|
34
|
+
fileToken: string;
|
|
35
|
+
}): string {
|
|
36
|
+
return params.core.channel.routing.buildAgentSessionKey({
|
|
37
|
+
agentId: params.route.agentId,
|
|
38
|
+
channel: "feishu",
|
|
39
|
+
accountId: params.route.accountId,
|
|
40
|
+
peer: {
|
|
41
|
+
kind: "direct",
|
|
42
|
+
id: `comment-doc:${params.fileType}:${params.fileToken}`,
|
|
43
|
+
},
|
|
44
|
+
dmScope: "per-account-channel-peer",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseTimestampMs(value: string | undefined): number {
|
|
49
|
+
const parsed = value ? Number.parseInt(value, 10) : Number.NaN;
|
|
50
|
+
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function handleFeishuCommentEvent(
|
|
54
|
+
params: HandleFeishuCommentEventParams,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
57
|
+
const feishuCfg = account.config;
|
|
58
|
+
const core = getFeishuRuntime();
|
|
59
|
+
const log = params.runtime?.log ?? console.log;
|
|
60
|
+
const error = params.runtime?.error ?? console.error;
|
|
61
|
+
const runtime = (params.runtime ?? { log, error }) as RuntimeEnv;
|
|
62
|
+
|
|
63
|
+
const turn = await resolveDriveCommentEventTurn({
|
|
64
|
+
cfg: params.cfg,
|
|
65
|
+
accountId: account.accountId,
|
|
66
|
+
event: params.event,
|
|
67
|
+
botOpenId: params.botOpenId,
|
|
68
|
+
logger: log,
|
|
69
|
+
});
|
|
70
|
+
if (!turn) {
|
|
71
|
+
log(
|
|
72
|
+
`feishu[${account.accountId}]: drive comment notice skipped ` +
|
|
73
|
+
`event=${params.event.event_id ?? "unknown"} comment=${params.event.comment_id ?? "unknown"}`,
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const commentTarget = buildFeishuCommentTarget({
|
|
79
|
+
fileType: turn.fileType,
|
|
80
|
+
fileToken: turn.fileToken,
|
|
81
|
+
commentId: turn.commentId,
|
|
82
|
+
});
|
|
83
|
+
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
|
84
|
+
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
|
85
|
+
const pairing = createChannelPairingController({
|
|
86
|
+
core,
|
|
87
|
+
channel: "feishu",
|
|
88
|
+
accountId: account.accountId,
|
|
89
|
+
});
|
|
90
|
+
const dmIngress = await resolveFeishuDmIngressAccess({
|
|
91
|
+
cfg: params.cfg,
|
|
92
|
+
accountId: account.accountId,
|
|
93
|
+
dmPolicy,
|
|
94
|
+
allowFrom: configAllowFrom,
|
|
95
|
+
readAllowFromStore: pairing.readAllowFromStore,
|
|
96
|
+
senderOpenId: turn.senderId,
|
|
97
|
+
senderUserId: turn.senderUserId,
|
|
98
|
+
conversationId: turn.senderId,
|
|
99
|
+
mayPair: true,
|
|
100
|
+
});
|
|
101
|
+
if (dmIngress.ingress.admission !== "dispatch") {
|
|
102
|
+
if (dmIngress.ingress.admission === "pairing-required") {
|
|
103
|
+
const client = createFeishuClient(account);
|
|
104
|
+
await pairing.issueChallenge({
|
|
105
|
+
senderId: turn.senderId,
|
|
106
|
+
senderIdLine: `Your Feishu user id: ${turn.senderId}`,
|
|
107
|
+
meta: { name: turn.senderId },
|
|
108
|
+
onCreated: ({ code }) => {
|
|
109
|
+
log(
|
|
110
|
+
`feishu[${account.accountId}]: comment pairing request sender=${turn.senderId} code=${code}`,
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
sendPairingReply: async (text) => {
|
|
114
|
+
await deliverCommentThreadText(client, {
|
|
115
|
+
file_token: turn.fileToken,
|
|
116
|
+
file_type: turn.fileType,
|
|
117
|
+
comment_id: turn.commentId,
|
|
118
|
+
content: text,
|
|
119
|
+
is_whole_comment: turn.isWholeComment,
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
onReplyError: (err) => {
|
|
123
|
+
log(
|
|
124
|
+
`feishu[${account.accountId}]: comment pairing reply failed for ${turn.senderId}: ${String(err)}`,
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
log(
|
|
130
|
+
`feishu[${account.accountId}]: blocked unauthorized comment sender ${turn.senderId} ` +
|
|
131
|
+
`(dmPolicy=${dmPolicy}, comment=${turn.commentId})`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let effectiveCfg = params.cfg;
|
|
138
|
+
let route = core.channel.routing.resolveAgentRoute({
|
|
139
|
+
cfg: params.cfg,
|
|
140
|
+
channel: "feishu",
|
|
141
|
+
accountId: account.accountId,
|
|
142
|
+
peer: {
|
|
143
|
+
kind: "direct",
|
|
144
|
+
id: turn.senderId,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
if (route.matchedBy === "default") {
|
|
148
|
+
const dynamicCfg = feishuCfg?.dynamicAgentCreation as DynamicAgentCreationConfig | undefined;
|
|
149
|
+
if (dynamicCfg?.enabled) {
|
|
150
|
+
const dynamicResult = await maybeCreateDynamicAgent({
|
|
151
|
+
cfg: params.cfg,
|
|
152
|
+
runtime: core,
|
|
153
|
+
senderOpenId: turn.senderId,
|
|
154
|
+
dynamicCfg,
|
|
155
|
+
configWritesAllowed: resolveChannelConfigWrites({
|
|
156
|
+
cfg: params.cfg,
|
|
157
|
+
channelId: "feishu",
|
|
158
|
+
accountId: account.accountId,
|
|
159
|
+
}),
|
|
160
|
+
log: (message) => log(message),
|
|
161
|
+
});
|
|
162
|
+
if (dynamicResult.created) {
|
|
163
|
+
effectiveCfg = dynamicResult.updatedCfg;
|
|
164
|
+
route = core.channel.routing.resolveAgentRoute({
|
|
165
|
+
cfg: dynamicResult.updatedCfg,
|
|
166
|
+
channel: "feishu",
|
|
167
|
+
accountId: account.accountId,
|
|
168
|
+
peer: {
|
|
169
|
+
kind: "direct",
|
|
170
|
+
id: turn.senderId,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
log(
|
|
174
|
+
`feishu[${account.accountId}]: dynamic agent created for comment flow, route=${route.sessionKey}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const commentSessionKey = buildCommentSessionKey({
|
|
181
|
+
core,
|
|
182
|
+
route,
|
|
183
|
+
fileType: turn.fileType,
|
|
184
|
+
fileToken: turn.fileToken,
|
|
185
|
+
});
|
|
186
|
+
const bodyForAgent = `[message_id: ${turn.messageId}]\n${turn.prompt}`;
|
|
187
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
188
|
+
Body: bodyForAgent,
|
|
189
|
+
BodyForAgent: bodyForAgent,
|
|
190
|
+
RawBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
|
|
191
|
+
CommandBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
|
|
192
|
+
From: `feishu:${turn.senderId}`,
|
|
193
|
+
To: commentTarget,
|
|
194
|
+
SessionKey: commentSessionKey,
|
|
195
|
+
AccountId: route.accountId,
|
|
196
|
+
ChatType: "direct",
|
|
197
|
+
ConversationLabel: turn.documentTitle
|
|
198
|
+
? `Feishu comment · ${turn.documentTitle}`
|
|
199
|
+
: "Feishu comment",
|
|
200
|
+
SenderName: turn.senderId,
|
|
201
|
+
SenderId: turn.senderId,
|
|
202
|
+
Provider: "feishu",
|
|
203
|
+
Surface: "feishu-comment",
|
|
204
|
+
MessageSid: turn.messageId,
|
|
205
|
+
// For Feishu comment turns, MessageThreadId carries the inbound reply_id so
|
|
206
|
+
// comment-aware tools can clean typing reaction before sending visible output.
|
|
207
|
+
MessageThreadId: turn.replyId,
|
|
208
|
+
Timestamp: parseTimestampMs(turn.timestamp),
|
|
209
|
+
WasMentioned: turn.isMentioned,
|
|
210
|
+
CommandAuthorized: false,
|
|
211
|
+
OriginatingChannel: "feishu",
|
|
212
|
+
OriginatingTo: commentTarget,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, {
|
|
216
|
+
agentId: route.agentId,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const { dispatcher, replyOptions, markDispatchIdle, markRunComplete, cleanupTypingReaction } =
|
|
220
|
+
createFeishuCommentReplyDispatcher({
|
|
221
|
+
cfg: effectiveCfg,
|
|
222
|
+
agentId: route.agentId,
|
|
223
|
+
runtime,
|
|
224
|
+
accountId: account.accountId,
|
|
225
|
+
fileToken: turn.fileToken,
|
|
226
|
+
fileType: turn.fileType,
|
|
227
|
+
commentId: turn.commentId,
|
|
228
|
+
replyId: turn.replyId,
|
|
229
|
+
isWholeComment: turn.isWholeComment,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
let dispatchSettledBeforeStart = false;
|
|
233
|
+
try {
|
|
234
|
+
log(
|
|
235
|
+
`feishu[${account.accountId}]: dispatching drive comment to agent ` +
|
|
236
|
+
`(session=${commentSessionKey} comment=${turn.commentId} type=${turn.noticeType})`,
|
|
237
|
+
);
|
|
238
|
+
const turnResult = await core.channel.turn.run({
|
|
239
|
+
channel: "feishu",
|
|
240
|
+
accountId: route.accountId,
|
|
241
|
+
raw: turn,
|
|
242
|
+
adapter: {
|
|
243
|
+
ingest: () => ({
|
|
244
|
+
id: turn.messageId,
|
|
245
|
+
timestamp: parseTimestampMs(turn.timestamp),
|
|
246
|
+
rawText: ctxPayload.RawBody ?? "",
|
|
247
|
+
textForAgent: ctxPayload.BodyForAgent,
|
|
248
|
+
textForCommands: ctxPayload.CommandBody,
|
|
249
|
+
raw: turn,
|
|
250
|
+
}),
|
|
251
|
+
resolveTurn: () => ({
|
|
252
|
+
channel: "feishu",
|
|
253
|
+
accountId: route.accountId,
|
|
254
|
+
routeSessionKey: commentSessionKey,
|
|
255
|
+
storePath,
|
|
256
|
+
ctxPayload,
|
|
257
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
258
|
+
record: {
|
|
259
|
+
onRecordError: (err) => {
|
|
260
|
+
error(
|
|
261
|
+
`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`,
|
|
262
|
+
);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
onPreDispatchFailure: async () => {
|
|
266
|
+
dispatchSettledBeforeStart = true;
|
|
267
|
+
await core.channel.reply.settleReplyDispatcher({
|
|
268
|
+
dispatcher,
|
|
269
|
+
onSettled: () => {
|
|
270
|
+
markRunComplete();
|
|
271
|
+
markDispatchIdle();
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
runDispatch: () =>
|
|
276
|
+
core.channel.reply.withReplyDispatcher({
|
|
277
|
+
dispatcher,
|
|
278
|
+
run: () =>
|
|
279
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
280
|
+
ctx: ctxPayload,
|
|
281
|
+
cfg: effectiveCfg,
|
|
282
|
+
dispatcher,
|
|
283
|
+
replyOptions,
|
|
284
|
+
}),
|
|
285
|
+
}),
|
|
286
|
+
}),
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : undefined;
|
|
290
|
+
const queuedFinal = dispatchResult?.queuedFinal ?? false;
|
|
291
|
+
const counts = dispatchResult?.counts ?? { tool: 0, block: 0, final: 0 };
|
|
292
|
+
log(
|
|
293
|
+
`feishu[${account.accountId}]: drive comment dispatch complete ` +
|
|
294
|
+
`(queuedFinal=${queuedFinal}, replies=${counts.final}, session=${commentSessionKey})`,
|
|
295
|
+
);
|
|
296
|
+
} finally {
|
|
297
|
+
if (!dispatchSettledBeforeStart) {
|
|
298
|
+
markRunComplete();
|
|
299
|
+
markDispatchIdle();
|
|
300
|
+
}
|
|
301
|
+
void cleanupTypingReaction();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
|
2
|
+
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
3
|
+
import { createFeishuClient } from "./client.js";
|
|
4
|
+
import { encodeQuery, formatFeishuApiError } from "./comment-shared.js";
|
|
5
|
+
import { parseFeishuCommentTarget, type CommentFileType } from "./comment-target.js";
|
|
6
|
+
|
|
7
|
+
const COMMENT_TYPING_REACTION_TYPE = "Typing";
|
|
8
|
+
const COMMENT_REACTION_TIMEOUT_MS = 30_000;
|
|
9
|
+
const commentTypingReactionState = new Map<
|
|
10
|
+
string,
|
|
11
|
+
{
|
|
12
|
+
active: boolean;
|
|
13
|
+
cleaned: boolean;
|
|
14
|
+
cleanupPromise?: Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
>();
|
|
17
|
+
|
|
18
|
+
type FeishuCommentReactionClient = ReturnType<typeof createFeishuClient> & {
|
|
19
|
+
request(params: {
|
|
20
|
+
method: "POST";
|
|
21
|
+
url: string;
|
|
22
|
+
data: unknown;
|
|
23
|
+
timeout: number;
|
|
24
|
+
}): Promise<unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function buildCommentTypingReactionKey(params: {
|
|
28
|
+
fileToken: string;
|
|
29
|
+
fileType: CommentFileType;
|
|
30
|
+
replyId: string;
|
|
31
|
+
}): string {
|
|
32
|
+
return `${params.fileType}:${params.fileToken}:${params.replyId}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureCommentTypingReactionState(key: string) {
|
|
36
|
+
const existing = commentTypingReactionState.get(key);
|
|
37
|
+
if (existing) {
|
|
38
|
+
return existing;
|
|
39
|
+
}
|
|
40
|
+
const created = {
|
|
41
|
+
active: false,
|
|
42
|
+
cleaned: false,
|
|
43
|
+
cleanupPromise: undefined,
|
|
44
|
+
};
|
|
45
|
+
commentTypingReactionState.set(key, created);
|
|
46
|
+
return created;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function requestCommentTypingReactionWithClient(params: {
|
|
50
|
+
client: FeishuCommentReactionClient;
|
|
51
|
+
fileToken: string;
|
|
52
|
+
fileType: CommentFileType;
|
|
53
|
+
replyId: string;
|
|
54
|
+
action: "add" | "delete";
|
|
55
|
+
runtime?: RuntimeEnv;
|
|
56
|
+
logPrefix?: string;
|
|
57
|
+
}): Promise<boolean> {
|
|
58
|
+
try {
|
|
59
|
+
const response = (await params.client.request({
|
|
60
|
+
method: "POST",
|
|
61
|
+
url:
|
|
62
|
+
`/open-apis/drive/v2/files/${encodeURIComponent(params.fileToken)}/comments/reaction` +
|
|
63
|
+
encodeQuery({
|
|
64
|
+
file_type: params.fileType,
|
|
65
|
+
}),
|
|
66
|
+
data: {
|
|
67
|
+
action: params.action,
|
|
68
|
+
reply_id: params.replyId,
|
|
69
|
+
reaction_type: COMMENT_TYPING_REACTION_TYPE,
|
|
70
|
+
},
|
|
71
|
+
timeout: COMMENT_REACTION_TIMEOUT_MS,
|
|
72
|
+
})) as {
|
|
73
|
+
code?: number;
|
|
74
|
+
msg?: string;
|
|
75
|
+
log_id?: string;
|
|
76
|
+
error?: { log_id?: string };
|
|
77
|
+
};
|
|
78
|
+
if (response.code === 0) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
params.runtime?.log?.(
|
|
82
|
+
`${params.logPrefix ?? "[feishu]"}: comment typing reaction ${params.action} failed ` +
|
|
83
|
+
`reply=${params.replyId} file=${params.fileType}:${params.fileToken} ` +
|
|
84
|
+
`code=${response.code ?? "unknown"} msg=${response.msg ?? "unknown"} ` +
|
|
85
|
+
`log_id=${response.log_id ?? response.error?.log_id ?? "unknown"}`,
|
|
86
|
+
);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
params.runtime?.log?.(
|
|
89
|
+
`${params.logPrefix ?? "[feishu]"}: comment typing reaction ${params.action} threw ` +
|
|
90
|
+
`reply=${params.replyId} file=${params.fileType}:${params.fileToken} ` +
|
|
91
|
+
`error=${formatCommentReactionFailure(error)}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatCommentReactionFailure(error: unknown): string {
|
|
98
|
+
return formatFeishuApiError(error, { includeNestedErrorLogId: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function requestCommentTypingReaction(params: {
|
|
102
|
+
cfg: ClawdbotConfig;
|
|
103
|
+
fileToken: string;
|
|
104
|
+
fileType: CommentFileType;
|
|
105
|
+
replyId: string;
|
|
106
|
+
action: "add" | "delete";
|
|
107
|
+
accountId?: string;
|
|
108
|
+
runtime?: RuntimeEnv;
|
|
109
|
+
}): Promise<boolean> {
|
|
110
|
+
const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
111
|
+
if (!account.configured || !(account.config.typingIndicator ?? true)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const client = createFeishuClient(account) as FeishuCommentReactionClient;
|
|
115
|
+
return requestCommentTypingReactionWithClient({
|
|
116
|
+
client,
|
|
117
|
+
fileToken: params.fileToken,
|
|
118
|
+
fileType: params.fileType,
|
|
119
|
+
replyId: params.replyId,
|
|
120
|
+
action: params.action,
|
|
121
|
+
runtime: params.runtime,
|
|
122
|
+
logPrefix: `feishu[${account.accountId}]`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function cleanupCommentTypingReactionByKey(params: {
|
|
127
|
+
key: string;
|
|
128
|
+
performDelete: () => Promise<boolean>;
|
|
129
|
+
}): Promise<boolean> {
|
|
130
|
+
const state = ensureCommentTypingReactionState(params.key);
|
|
131
|
+
if (state.cleaned) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (state.cleanupPromise) {
|
|
135
|
+
return await state.cleanupPromise;
|
|
136
|
+
}
|
|
137
|
+
const cleanupPromise = (async (): Promise<boolean> => {
|
|
138
|
+
if (!state.active) {
|
|
139
|
+
state.cleaned = true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const deleted = await params.performDelete();
|
|
143
|
+
if (deleted) {
|
|
144
|
+
state.cleaned = true;
|
|
145
|
+
state.active = false;
|
|
146
|
+
}
|
|
147
|
+
return deleted;
|
|
148
|
+
})();
|
|
149
|
+
state.cleanupPromise = cleanupPromise;
|
|
150
|
+
try {
|
|
151
|
+
return await cleanupPromise;
|
|
152
|
+
} finally {
|
|
153
|
+
state.cleanupPromise = undefined;
|
|
154
|
+
if (state.cleaned) {
|
|
155
|
+
state.active = false;
|
|
156
|
+
commentTypingReactionState.delete(params.key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function cleanupAmbientCommentTypingReaction(params: {
|
|
162
|
+
client: FeishuCommentReactionClient;
|
|
163
|
+
deliveryContext?: {
|
|
164
|
+
channel?: string;
|
|
165
|
+
to?: string;
|
|
166
|
+
threadId?: string | number;
|
|
167
|
+
};
|
|
168
|
+
runtime?: RuntimeEnv;
|
|
169
|
+
}): Promise<boolean> {
|
|
170
|
+
const deliveryContext = params.deliveryContext;
|
|
171
|
+
if (
|
|
172
|
+
deliveryContext?.channel &&
|
|
173
|
+
deliveryContext.channel !== "feishu" &&
|
|
174
|
+
deliveryContext.channel !== "feishu-comment"
|
|
175
|
+
) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const target = parseFeishuCommentTarget(deliveryContext?.to);
|
|
179
|
+
const replyId =
|
|
180
|
+
typeof deliveryContext?.threadId === "string" || typeof deliveryContext?.threadId === "number"
|
|
181
|
+
? String(deliveryContext.threadId).trim()
|
|
182
|
+
: "";
|
|
183
|
+
if (!target || !replyId) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const key = buildCommentTypingReactionKey({
|
|
187
|
+
fileToken: target.fileToken,
|
|
188
|
+
fileType: target.fileType,
|
|
189
|
+
replyId,
|
|
190
|
+
});
|
|
191
|
+
return cleanupCommentTypingReactionByKey({
|
|
192
|
+
key,
|
|
193
|
+
performDelete: () =>
|
|
194
|
+
requestCommentTypingReactionWithClient({
|
|
195
|
+
client: params.client,
|
|
196
|
+
fileToken: target.fileToken,
|
|
197
|
+
fileType: target.fileType,
|
|
198
|
+
replyId,
|
|
199
|
+
action: "delete",
|
|
200
|
+
runtime: params.runtime,
|
|
201
|
+
logPrefix: "[feishu]",
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createCommentTypingReactionLifecycle(params: {
|
|
207
|
+
cfg: ClawdbotConfig;
|
|
208
|
+
fileToken: string;
|
|
209
|
+
fileType: CommentFileType;
|
|
210
|
+
replyId?: string;
|
|
211
|
+
accountId?: string;
|
|
212
|
+
runtime?: RuntimeEnv;
|
|
213
|
+
}) {
|
|
214
|
+
const key = params.replyId?.trim()
|
|
215
|
+
? buildCommentTypingReactionKey({
|
|
216
|
+
fileToken: params.fileToken,
|
|
217
|
+
fileType: params.fileType,
|
|
218
|
+
replyId: params.replyId.trim(),
|
|
219
|
+
})
|
|
220
|
+
: undefined;
|
|
221
|
+
const state = key ? ensureCommentTypingReactionState(key) : undefined;
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
start: async (): Promise<void> => {
|
|
225
|
+
const replyId = params.replyId?.trim();
|
|
226
|
+
if (!state || state.cleaned || state.active || !replyId) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
state.active = await requestCommentTypingReaction({
|
|
230
|
+
cfg: params.cfg,
|
|
231
|
+
fileToken: params.fileToken,
|
|
232
|
+
fileType: params.fileType,
|
|
233
|
+
replyId,
|
|
234
|
+
action: "add",
|
|
235
|
+
accountId: params.accountId,
|
|
236
|
+
runtime: params.runtime,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
cleanup: async (): Promise<void> => {
|
|
240
|
+
const replyId = params.replyId?.trim();
|
|
241
|
+
if (!key || !replyId) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
await cleanupCommentTypingReactionByKey({
|
|
245
|
+
key,
|
|
246
|
+
performDelete: () =>
|
|
247
|
+
requestCommentTypingReaction({
|
|
248
|
+
cfg: params.cfg,
|
|
249
|
+
fileToken: params.fileToken,
|
|
250
|
+
fileType: params.fileType,
|
|
251
|
+
replyId,
|
|
252
|
+
action: "delete",
|
|
253
|
+
accountId: params.accountId,
|
|
254
|
+
runtime: params.runtime,
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|