mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
feat: feishu comment event (#58497)
Merged via squash.
Prepared head SHA: a9dfeb0d62
Co-authored-by: wittam-01 <271711640+wittam-01@users.noreply.github.com>
Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com>
Reviewed-by: @odysseus0
This commit is contained in:
247
extensions/feishu/src/comment-handler.ts
Normal file
247
extensions/feishu/src/comment-handler.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
createChannelPairingController,
|
||||
type ClawdbotConfig,
|
||||
type RuntimeEnv,
|
||||
} from "../runtime-api.js";
|
||||
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { createFeishuCommentReplyDispatcher } from "./comment-dispatcher.js";
|
||||
import { buildFeishuCommentTarget } from "./comment-target.js";
|
||||
import { replyComment } from "./drive.js";
|
||||
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
||||
import {
|
||||
resolveDriveCommentEventTurn,
|
||||
type FeishuDriveCommentNoticeEvent,
|
||||
} from "./monitor.comment.js";
|
||||
import { resolveFeishuAllowlistMatch } from "./policy.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import type { DynamicAgentCreationConfig } from "./types.js";
|
||||
|
||||
type HandleFeishuCommentEventParams = {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId: string;
|
||||
runtime?: RuntimeEnv;
|
||||
event: FeishuDriveCommentNoticeEvent;
|
||||
botOpenId?: string;
|
||||
};
|
||||
|
||||
function buildCommentSessionKey(params: {
|
||||
core: ReturnType<typeof getFeishuRuntime>;
|
||||
route: ResolvedAgentRoute;
|
||||
commentTarget: string;
|
||||
}): string {
|
||||
return params.core.channel.routing.buildAgentSessionKey({
|
||||
agentId: params.route.agentId,
|
||||
channel: "feishu",
|
||||
accountId: params.route.accountId,
|
||||
peer: {
|
||||
kind: "direct",
|
||||
id: params.commentTarget,
|
||||
},
|
||||
dmScope: "per-account-channel-peer",
|
||||
});
|
||||
}
|
||||
|
||||
function parseTimestampMs(value: string | undefined): number {
|
||||
const parsed = value ? Number.parseInt(value, 10) : Number.NaN;
|
||||
return Number.isFinite(parsed) ? parsed : Date.now();
|
||||
}
|
||||
|
||||
export async function handleFeishuCommentEvent(
|
||||
params: HandleFeishuCommentEventParams,
|
||||
): Promise<void> {
|
||||
const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const feishuCfg = account.config;
|
||||
const core = getFeishuRuntime();
|
||||
const log = params.runtime?.log ?? console.log;
|
||||
const error = params.runtime?.error ?? console.error;
|
||||
const runtime = (params.runtime ?? { log, error }) as RuntimeEnv;
|
||||
|
||||
const turn = await resolveDriveCommentEventTurn({
|
||||
cfg: params.cfg,
|
||||
accountId: account.accountId,
|
||||
event: params.event,
|
||||
botOpenId: params.botOpenId,
|
||||
logger: log,
|
||||
});
|
||||
if (!turn) {
|
||||
log(
|
||||
`feishu[${account.accountId}]: drive comment notice skipped ` +
|
||||
`event=${params.event.event_id ?? "unknown"} comment=${params.event.comment_id ?? "unknown"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const commentTarget = buildFeishuCommentTarget({
|
||||
fileType: turn.fileType,
|
||||
fileToken: turn.fileToken,
|
||||
commentId: turn.commentId,
|
||||
});
|
||||
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
||||
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
||||
const pairing = createChannelPairingController({
|
||||
core,
|
||||
channel: "feishu",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const storeAllowFrom =
|
||||
dmPolicy !== "allowlist" && dmPolicy !== "open"
|
||||
? await pairing.readAllowFromStore().catch(() => [])
|
||||
: [];
|
||||
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
||||
const senderAllowed = resolveFeishuAllowlistMatch({
|
||||
allowFrom: effectiveDmAllowFrom,
|
||||
senderId: turn.senderId,
|
||||
senderIds: [turn.senderUserId],
|
||||
}).allowed;
|
||||
if (dmPolicy !== "open" && !senderAllowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const client = createFeishuClient(account);
|
||||
await pairing.issueChallenge({
|
||||
senderId: turn.senderId,
|
||||
senderIdLine: `Your Feishu user id: ${turn.senderId}`,
|
||||
meta: { name: turn.senderId },
|
||||
onCreated: ({ code }) => {
|
||||
log(
|
||||
`feishu[${account.accountId}]: comment pairing request sender=${turn.senderId} code=${code}`,
|
||||
);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await replyComment(client, {
|
||||
file_token: turn.fileToken,
|
||||
file_type: turn.fileType,
|
||||
comment_id: turn.commentId,
|
||||
content: text,
|
||||
});
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
log(
|
||||
`feishu[${account.accountId}]: comment pairing reply failed for ${turn.senderId}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
log(
|
||||
`feishu[${account.accountId}]: blocked unauthorized comment sender ${turn.senderId} ` +
|
||||
`(dmPolicy=${dmPolicy}, comment=${turn.commentId})`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let effectiveCfg = params.cfg;
|
||||
let route = core.channel.routing.resolveAgentRoute({
|
||||
cfg: params.cfg,
|
||||
channel: "feishu",
|
||||
accountId: account.accountId,
|
||||
peer: {
|
||||
kind: "direct",
|
||||
id: turn.senderId,
|
||||
},
|
||||
});
|
||||
if (route.matchedBy === "default") {
|
||||
const dynamicCfg = feishuCfg?.dynamicAgentCreation as DynamicAgentCreationConfig | undefined;
|
||||
if (dynamicCfg?.enabled) {
|
||||
const dynamicResult = await maybeCreateDynamicAgent({
|
||||
cfg: params.cfg,
|
||||
runtime: core,
|
||||
senderOpenId: turn.senderId,
|
||||
dynamicCfg,
|
||||
log: (message) => log(message),
|
||||
});
|
||||
if (dynamicResult.created) {
|
||||
effectiveCfg = dynamicResult.updatedCfg;
|
||||
route = core.channel.routing.resolveAgentRoute({
|
||||
cfg: dynamicResult.updatedCfg,
|
||||
channel: "feishu",
|
||||
accountId: account.accountId,
|
||||
peer: {
|
||||
kind: "direct",
|
||||
id: turn.senderId,
|
||||
},
|
||||
});
|
||||
log(
|
||||
`feishu[${account.accountId}]: dynamic agent created for comment flow, route=${route.sessionKey}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const commentSessionKey = buildCommentSessionKey({
|
||||
core,
|
||||
route,
|
||||
commentTarget,
|
||||
});
|
||||
const bodyForAgent = `[message_id: ${turn.messageId}]\n${turn.prompt}`;
|
||||
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
||||
Body: bodyForAgent,
|
||||
BodyForAgent: bodyForAgent,
|
||||
RawBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
|
||||
CommandBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
|
||||
From: `feishu:${turn.senderId}`,
|
||||
To: commentTarget,
|
||||
SessionKey: commentSessionKey,
|
||||
AccountId: route.accountId,
|
||||
ChatType: "direct",
|
||||
ConversationLabel: turn.documentTitle
|
||||
? `Feishu comment · ${turn.documentTitle}`
|
||||
: "Feishu comment",
|
||||
SenderName: turn.senderId,
|
||||
SenderId: turn.senderId,
|
||||
Provider: "feishu",
|
||||
Surface: "feishu-comment",
|
||||
MessageSid: turn.messageId,
|
||||
Timestamp: parseTimestampMs(turn.timestamp),
|
||||
WasMentioned: turn.isMentioned,
|
||||
CommandAuthorized: false,
|
||||
OriginatingChannel: "feishu",
|
||||
OriginatingTo: commentTarget,
|
||||
});
|
||||
|
||||
const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
await core.channel.session.recordInboundSession({
|
||||
storePath,
|
||||
sessionKey: commentSessionKey,
|
||||
ctx: ctxPayload,
|
||||
onRecordError: (err) => {
|
||||
error(
|
||||
`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const { dispatcher, replyOptions, markDispatchIdle } = createFeishuCommentReplyDispatcher({
|
||||
cfg: effectiveCfg,
|
||||
agentId: route.agentId,
|
||||
runtime,
|
||||
accountId: account.accountId,
|
||||
fileToken: turn.fileToken,
|
||||
fileType: turn.fileType,
|
||||
commentId: turn.commentId,
|
||||
});
|
||||
|
||||
log(
|
||||
`feishu[${account.accountId}]: dispatching drive comment to agent ` +
|
||||
`(session=${commentSessionKey} comment=${turn.commentId} type=${turn.noticeType})`,
|
||||
);
|
||||
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => {
|
||||
markDispatchIdle();
|
||||
},
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg: effectiveCfg,
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
}),
|
||||
});
|
||||
log(
|
||||
`feishu[${account.accountId}]: drive comment dispatch complete ` +
|
||||
`(queuedFinal=${queuedFinal}, replies=${counts.final}, session=${commentSessionKey})`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user