Spaces:
Paused
Paused
File size: 5,437 Bytes
87fc763 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
import type { MSTeamsConversationStore } from "./conversation-store.js";
import type { MSTeamsAdapter } from "./messenger.js";
import type { MSTeamsMonitorLogger } from "./monitor-types.js";
import type { MSTeamsPollStore } from "./polls.js";
import type { MSTeamsTurnContext } from "./sdk-types.js";
import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js";
import { getPendingUpload, removePendingUpload } from "./pending-uploads.js";
export type MSTeamsAccessTokenProvider = {
getAccessToken: (scope: string) => Promise<string>;
};
export type MSTeamsActivityHandler = {
onMessage: (
handler: (context: unknown, next: () => Promise<void>) => Promise<void>,
) => MSTeamsActivityHandler;
onMembersAdded: (
handler: (context: unknown, next: () => Promise<void>) => Promise<void>,
) => MSTeamsActivityHandler;
run?: (context: unknown) => Promise<void>;
};
export type MSTeamsMessageHandlerDeps = {
cfg: OpenClawConfig;
runtime: RuntimeEnv;
appId: string;
adapter: MSTeamsAdapter;
tokenProvider: MSTeamsAccessTokenProvider;
textLimit: number;
mediaMaxBytes: number;
conversationStore: MSTeamsConversationStore;
pollStore: MSTeamsPollStore;
log: MSTeamsMonitorLogger;
};
/**
* Handle fileConsent/invoke activities for large file uploads.
*/
async function handleFileConsentInvoke(
context: MSTeamsTurnContext,
log: MSTeamsMonitorLogger,
): Promise<boolean> {
const activity = context.activity;
if (activity.type !== "invoke" || activity.name !== "fileConsent/invoke") {
return false;
}
const consentResponse = parseFileConsentInvoke(activity);
if (!consentResponse) {
log.debug("invalid file consent invoke", { value: activity.value });
return false;
}
const uploadId =
typeof consentResponse.context?.uploadId === "string"
? consentResponse.context.uploadId
: undefined;
if (consentResponse.action === "accept" && consentResponse.uploadInfo) {
const pendingFile = getPendingUpload(uploadId);
if (pendingFile) {
log.debug("user accepted file consent, uploading", {
uploadId,
filename: pendingFile.filename,
size: pendingFile.buffer.length,
});
try {
// Upload file to the provided URL
await uploadToConsentUrl({
url: consentResponse.uploadInfo.uploadUrl,
buffer: pendingFile.buffer,
contentType: pendingFile.contentType,
});
// Send confirmation card
const fileInfoCard = buildFileInfoCard({
filename: consentResponse.uploadInfo.name,
contentUrl: consentResponse.uploadInfo.contentUrl,
uniqueId: consentResponse.uploadInfo.uniqueId,
fileType: consentResponse.uploadInfo.fileType,
});
await context.sendActivity({
type: "message",
attachments: [fileInfoCard],
});
log.info("file upload complete", {
uploadId,
filename: consentResponse.uploadInfo.name,
uniqueId: consentResponse.uploadInfo.uniqueId,
});
} catch (err) {
log.debug("file upload failed", { uploadId, error: String(err) });
await context.sendActivity(`File upload failed: ${String(err)}`);
} finally {
removePendingUpload(uploadId);
}
} else {
log.debug("pending file not found for consent", { uploadId });
await context.sendActivity(
"The file upload request has expired. Please try sending the file again.",
);
}
} else {
// User declined
log.debug("user declined file consent", { uploadId });
removePendingUpload(uploadId);
}
return true;
}
export function registerMSTeamsHandlers<T extends MSTeamsActivityHandler>(
handler: T,
deps: MSTeamsMessageHandlerDeps,
): T {
const handleTeamsMessage = createMSTeamsMessageHandler(deps);
// Wrap the original run method to intercept invokes
const originalRun = handler.run;
if (originalRun) {
handler.run = async (context: unknown) => {
const ctx = context as MSTeamsTurnContext;
// Handle file consent invokes before passing to normal flow
if (ctx.activity?.type === "invoke" && ctx.activity?.name === "fileConsent/invoke") {
const handled = await handleFileConsentInvoke(ctx, deps.log);
if (handled) {
// Send invoke response for file consent
await ctx.sendActivity({ type: "invokeResponse", value: { status: 200 } });
return;
}
}
return originalRun.call(handler, context);
};
}
handler.onMessage(async (context, next) => {
try {
await handleTeamsMessage(context as MSTeamsTurnContext);
} catch (err) {
deps.runtime.error?.(`msteams handler failed: ${String(err)}`);
}
await next();
});
handler.onMembersAdded(async (context, next) => {
const membersAdded = (context as MSTeamsTurnContext).activity?.membersAdded ?? [];
for (const member of membersAdded) {
if (member.id !== (context as MSTeamsTurnContext).activity?.recipient?.id) {
deps.log.debug("member added", { member: member.id });
// Don't send welcome message - let the user initiate conversation.
}
}
await next();
});
return handler;
}
|