mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(extensions): reuse shared helper primitives
This commit is contained in:
@@ -4,7 +4,8 @@ import type {
|
||||
OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/msteams";
|
||||
import {
|
||||
buildBaseChannelStatusSummary,
|
||||
buildProbeChannelStatusSummary,
|
||||
buildRuntimeAccountStatusSnapshot,
|
||||
buildChannelConfigSchema,
|
||||
createDefaultChannelRuntimeState,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
@@ -250,11 +251,43 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
name: undefined as string | undefined,
|
||||
note: undefined as string | undefined,
|
||||
}));
|
||||
type ResolveTargetResultEntry = (typeof results)[number];
|
||||
type PendingTargetEntry = { input: string; query: string; index: number };
|
||||
|
||||
const stripPrefix = (value: string) => normalizeMSTeamsUserInput(value);
|
||||
const markPendingLookupFailed = (pending: PendingTargetEntry[]) => {
|
||||
pending.forEach(({ index }) => {
|
||||
const entry = results[index];
|
||||
if (entry) {
|
||||
entry.note = "lookup failed";
|
||||
}
|
||||
});
|
||||
};
|
||||
const resolvePending = async <T>(
|
||||
pending: PendingTargetEntry[],
|
||||
resolveEntries: (entries: string[]) => Promise<T[]>,
|
||||
applyResolvedEntry: (target: ResolveTargetResultEntry, entry: T) => void,
|
||||
) => {
|
||||
if (pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resolved = await resolveEntries(pending.map((entry) => entry.query));
|
||||
resolved.forEach((entry, idx) => {
|
||||
const target = results[pending[idx]?.index ?? -1];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
applyResolvedEntry(target, entry);
|
||||
});
|
||||
} catch (err) {
|
||||
runtime.error?.(`msteams resolve failed: ${String(err)}`);
|
||||
markPendingLookupFailed(pending);
|
||||
}
|
||||
};
|
||||
|
||||
if (kind === "user") {
|
||||
const pending: Array<{ input: string; query: string; index: number }> = [];
|
||||
const pending: PendingTargetEntry[] = [];
|
||||
results.forEach((entry, index) => {
|
||||
const trimmed = entry.input.trim();
|
||||
if (!trimmed) {
|
||||
@@ -270,37 +303,21 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
pending.push({ input: entry.input, query: cleaned, index });
|
||||
});
|
||||
|
||||
if (pending.length > 0) {
|
||||
try {
|
||||
const resolved = await resolveMSTeamsUserAllowlist({
|
||||
cfg,
|
||||
entries: pending.map((entry) => entry.query),
|
||||
});
|
||||
resolved.forEach((entry, idx) => {
|
||||
const target = results[pending[idx]?.index ?? -1];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
target.resolved = entry.resolved;
|
||||
target.id = entry.id;
|
||||
target.name = entry.name;
|
||||
target.note = entry.note;
|
||||
});
|
||||
} catch (err) {
|
||||
runtime.error?.(`msteams resolve failed: ${String(err)}`);
|
||||
pending.forEach(({ index }) => {
|
||||
const entry = results[index];
|
||||
if (entry) {
|
||||
entry.note = "lookup failed";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
await resolvePending(
|
||||
pending,
|
||||
(entries) => resolveMSTeamsUserAllowlist({ cfg, entries }),
|
||||
(target, entry) => {
|
||||
target.resolved = entry.resolved;
|
||||
target.id = entry.id;
|
||||
target.name = entry.name;
|
||||
target.note = entry.note;
|
||||
},
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
const pending: Array<{ input: string; query: string; index: number }> = [];
|
||||
const pending: PendingTargetEntry[] = [];
|
||||
results.forEach((entry, index) => {
|
||||
const trimmed = entry.input.trim();
|
||||
if (!trimmed) {
|
||||
@@ -323,48 +340,32 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
pending.push({ input: entry.input, query, index });
|
||||
});
|
||||
|
||||
if (pending.length > 0) {
|
||||
try {
|
||||
const resolved = await resolveMSTeamsChannelAllowlist({
|
||||
cfg,
|
||||
entries: pending.map((entry) => entry.query),
|
||||
});
|
||||
resolved.forEach((entry, idx) => {
|
||||
const target = results[pending[idx]?.index ?? -1];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (!entry.resolved || !entry.teamId) {
|
||||
target.resolved = false;
|
||||
target.note = entry.note;
|
||||
return;
|
||||
}
|
||||
target.resolved = true;
|
||||
if (entry.channelId) {
|
||||
target.id = `${entry.teamId}/${entry.channelId}`;
|
||||
target.name =
|
||||
entry.channelName && entry.teamName
|
||||
? `${entry.teamName}/${entry.channelName}`
|
||||
: (entry.channelName ?? entry.teamName);
|
||||
} else {
|
||||
target.id = entry.teamId;
|
||||
target.name = entry.teamName;
|
||||
target.note = "team id";
|
||||
}
|
||||
if (entry.note) {
|
||||
target.note = entry.note;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
runtime.error?.(`msteams resolve failed: ${String(err)}`);
|
||||
pending.forEach(({ index }) => {
|
||||
const entry = results[index];
|
||||
if (entry) {
|
||||
entry.note = "lookup failed";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
await resolvePending(
|
||||
pending,
|
||||
(entries) => resolveMSTeamsChannelAllowlist({ cfg, entries }),
|
||||
(target, entry) => {
|
||||
if (!entry.resolved || !entry.teamId) {
|
||||
target.resolved = false;
|
||||
target.note = entry.note;
|
||||
return;
|
||||
}
|
||||
target.resolved = true;
|
||||
if (entry.channelId) {
|
||||
target.id = `${entry.teamId}/${entry.channelId}`;
|
||||
target.name =
|
||||
entry.channelName && entry.teamName
|
||||
? `${entry.teamName}/${entry.channelName}`
|
||||
: (entry.channelName ?? entry.teamName);
|
||||
} else {
|
||||
target.id = entry.teamId;
|
||||
target.name = entry.teamName;
|
||||
target.note = "team id";
|
||||
}
|
||||
if (entry.note) {
|
||||
target.note = entry.note;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return results;
|
||||
},
|
||||
@@ -429,23 +430,17 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
outbound: msteamsOutbound,
|
||||
status: {
|
||||
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
|
||||
buildChannelSummary: ({ snapshot }) => ({
|
||||
...buildBaseChannelStatusSummary(snapshot),
|
||||
port: snapshot.port ?? null,
|
||||
probe: snapshot.probe,
|
||||
lastProbeAt: snapshot.lastProbeAt ?? null,
|
||||
}),
|
||||
buildChannelSummary: ({ snapshot }) =>
|
||||
buildProbeChannelStatusSummary(snapshot, {
|
||||
port: snapshot.port ?? null,
|
||||
}),
|
||||
probeAccount: async ({ cfg }) => await probeMSTeams(cfg.channels?.msteams),
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
||||
accountId: account.accountId,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
running: runtime?.running ?? false,
|
||||
lastStartAt: runtime?.lastStartAt ?? null,
|
||||
lastStopAt: runtime?.lastStopAt ?? null,
|
||||
lastError: runtime?.lastError ?? null,
|
||||
...buildRuntimeAccountStatusSnapshot({ runtime, probe }),
|
||||
port: runtime?.port ?? null,
|
||||
probe,
|
||||
}),
|
||||
},
|
||||
gateway: {
|
||||
|
||||
@@ -72,6 +72,17 @@ const createRecordedSendActivity = (
|
||||
};
|
||||
};
|
||||
|
||||
const REVOCATION_ERROR = "Cannot perform 'set' on a proxy that has been revoked";
|
||||
|
||||
const createFallbackAdapter = (proactiveSent: string[]): MSTeamsAdapter => ({
|
||||
continueConversation: async (_appId, _reference, logic) => {
|
||||
await logic({
|
||||
sendActivity: createRecordedSendActivity(proactiveSent),
|
||||
});
|
||||
},
|
||||
process: async () => {},
|
||||
});
|
||||
|
||||
describe("msteams messenger", () => {
|
||||
beforeEach(() => {
|
||||
setMSTeamsRuntime(runtimeStub);
|
||||
@@ -297,18 +308,11 @@ describe("msteams messenger", () => {
|
||||
|
||||
const ctx = {
|
||||
sendActivity: async () => {
|
||||
throw new TypeError("Cannot perform 'set' on a proxy that has been revoked");
|
||||
throw new TypeError(REVOCATION_ERROR);
|
||||
},
|
||||
};
|
||||
|
||||
const adapter: MSTeamsAdapter = {
|
||||
continueConversation: async (_appId, _reference, logic) => {
|
||||
await logic({
|
||||
sendActivity: createRecordedSendActivity(proactiveSent),
|
||||
});
|
||||
},
|
||||
process: async () => {},
|
||||
};
|
||||
const adapter = createFallbackAdapter(proactiveSent);
|
||||
|
||||
const ids = await sendMSTeamsMessages({
|
||||
replyStyle: "thread",
|
||||
@@ -338,18 +342,11 @@ describe("msteams messenger", () => {
|
||||
threadSent.push(content);
|
||||
return { id: `id:${content}` };
|
||||
}
|
||||
throw new TypeError("Cannot perform 'set' on a proxy that has been revoked");
|
||||
throw new TypeError(REVOCATION_ERROR);
|
||||
},
|
||||
};
|
||||
|
||||
const adapter: MSTeamsAdapter = {
|
||||
continueConversation: async (_appId, _reference, logic) => {
|
||||
await logic({
|
||||
sendActivity: createRecordedSendActivity(proactiveSent),
|
||||
});
|
||||
},
|
||||
process: async () => {},
|
||||
};
|
||||
const adapter = createFallbackAdapter(proactiveSent);
|
||||
|
||||
const ids = await sendMSTeamsMessages({
|
||||
replyStyle: "thread",
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntriesIfEnabled,
|
||||
dispatchReplyFromConfigWithSettledDispatcher,
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
createScopedPairingAccess,
|
||||
logInboundDrop,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
isDangerousNameMatchingEnabled,
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveMentionGating,
|
||||
resolveInboundSessionEnvelopeContext,
|
||||
formatAllowlistMatchMeta,
|
||||
resolveEffectiveAllowFromLists,
|
||||
resolveDmGroupAccessWithLists,
|
||||
@@ -451,12 +453,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
|
||||
const mediaPayload = buildMSTeamsMediaPayload(mediaList);
|
||||
const envelopeFrom = isDirectMessage ? senderName : conversationType;
|
||||
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
||||
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
||||
cfg,
|
||||
agentId: route.agentId,
|
||||
});
|
||||
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
||||
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
||||
storePath,
|
||||
sessionKey: route.sessionKey,
|
||||
});
|
||||
const body = core.channel.reply.formatAgentEnvelope({
|
||||
@@ -559,18 +558,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
|
||||
log.info("dispatching to agent", { sessionKey: route.sessionKey });
|
||||
try {
|
||||
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
|
||||
const { queuedFinal, counts } = await dispatchReplyFromConfigWithSettledDispatcher({
|
||||
cfg,
|
||||
ctxPayload,
|
||||
dispatcher,
|
||||
onSettled: () => {
|
||||
markDispatchIdle();
|
||||
},
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
}),
|
||||
replyOptions,
|
||||
});
|
||||
|
||||
log.info("dispatch complete", { queuedFinal, counts });
|
||||
|
||||
@@ -157,24 +157,13 @@ export async function sendMessageMSTeams(
|
||||
|
||||
log.debug?.("sending file consent card", { uploadId, fileName, size: media.buffer.length });
|
||||
|
||||
const baseRef = buildConversationReference(ref);
|
||||
const proactiveRef = { ...baseRef, activityId: undefined };
|
||||
|
||||
let messageId = "unknown";
|
||||
try {
|
||||
await adapter.continueConversation(appId, proactiveRef, async (turnCtx) => {
|
||||
const response = await turnCtx.sendActivity(activity);
|
||||
messageId = extractMessageId(response) ?? "unknown";
|
||||
});
|
||||
} catch (err) {
|
||||
const classification = classifyMSTeamsSendError(err);
|
||||
const hint = formatMSTeamsSendErrorHint(classification);
|
||||
const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
|
||||
throw new Error(
|
||||
`msteams consent card send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
const messageId = await sendProactiveActivity({
|
||||
adapter,
|
||||
appId,
|
||||
ref,
|
||||
activity,
|
||||
errorPrefix: "msteams consent card send",
|
||||
});
|
||||
|
||||
log.info("sent file consent card", { conversationId, messageId, uploadId });
|
||||
|
||||
@@ -245,14 +234,11 @@ export async function sendMessageMSTeams(
|
||||
text: messageText || undefined,
|
||||
attachments: [fileCardAttachment],
|
||||
};
|
||||
|
||||
const baseRef = buildConversationReference(ref);
|
||||
const proactiveRef = { ...baseRef, activityId: undefined };
|
||||
|
||||
let messageId = "unknown";
|
||||
await adapter.continueConversation(appId, proactiveRef, async (turnCtx) => {
|
||||
const response = await turnCtx.sendActivity(activity);
|
||||
messageId = extractMessageId(response) ?? "unknown";
|
||||
const messageId = await sendProactiveActivityRaw({
|
||||
adapter,
|
||||
appId,
|
||||
ref,
|
||||
activity,
|
||||
});
|
||||
|
||||
log.info("sent native file card", {
|
||||
@@ -288,14 +274,11 @@ export async function sendMessageMSTeams(
|
||||
type: "message",
|
||||
text: messageText ? `${messageText}\n\n${fileLink}` : fileLink,
|
||||
};
|
||||
|
||||
const baseRef = buildConversationReference(ref);
|
||||
const proactiveRef = { ...baseRef, activityId: undefined };
|
||||
|
||||
let messageId = "unknown";
|
||||
await adapter.continueConversation(appId, proactiveRef, async (turnCtx) => {
|
||||
const response = await turnCtx.sendActivity(activity);
|
||||
messageId = extractMessageId(response) ?? "unknown";
|
||||
const messageId = await sendProactiveActivityRaw({
|
||||
adapter,
|
||||
appId,
|
||||
ref,
|
||||
activity,
|
||||
});
|
||||
|
||||
log.info("sent message with OneDrive file link", {
|
||||
@@ -382,13 +365,14 @@ type ProactiveActivityParams = {
|
||||
errorPrefix: string;
|
||||
};
|
||||
|
||||
async function sendProactiveActivity({
|
||||
type ProactiveActivityRawParams = Omit<ProactiveActivityParams, "errorPrefix">;
|
||||
|
||||
async function sendProactiveActivityRaw({
|
||||
adapter,
|
||||
appId,
|
||||
ref,
|
||||
activity,
|
||||
errorPrefix,
|
||||
}: ProactiveActivityParams): Promise<string> {
|
||||
}: ProactiveActivityRawParams): Promise<string> {
|
||||
const baseRef = buildConversationReference(ref);
|
||||
const proactiveRef = {
|
||||
...baseRef,
|
||||
@@ -396,12 +380,27 @@ async function sendProactiveActivity({
|
||||
};
|
||||
|
||||
let messageId = "unknown";
|
||||
await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
|
||||
const response = await ctx.sendActivity(activity);
|
||||
messageId = extractMessageId(response) ?? "unknown";
|
||||
});
|
||||
return messageId;
|
||||
}
|
||||
|
||||
async function sendProactiveActivity({
|
||||
adapter,
|
||||
appId,
|
||||
ref,
|
||||
activity,
|
||||
errorPrefix,
|
||||
}: ProactiveActivityParams): Promise<string> {
|
||||
try {
|
||||
await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
|
||||
const response = await ctx.sendActivity(activity);
|
||||
messageId = extractMessageId(response) ?? "unknown";
|
||||
return await sendProactiveActivityRaw({
|
||||
adapter,
|
||||
appId,
|
||||
ref,
|
||||
activity,
|
||||
});
|
||||
return messageId;
|
||||
} catch (err) {
|
||||
const classification = classifyMSTeamsSendError(err);
|
||||
const hint = formatMSTeamsSendErrorHint(classification);
|
||||
|
||||
Reference in New Issue
Block a user