mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
204 lines
6.7 KiB
TypeScript
204 lines
6.7 KiB
TypeScript
// Qa Lab plugin module implements bus queries behavior.
|
|
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { parseQaTarget } from "./qa-bus-protocol.js";
|
|
import type {
|
|
QaBusAttachment,
|
|
QaBusConversation,
|
|
QaBusEvent,
|
|
QaBusMessage,
|
|
QaBusPollInput,
|
|
QaBusPollResult,
|
|
QaBusReadMessageInput,
|
|
QaBusSearchMessagesInput,
|
|
QaBusSnapshotConversation,
|
|
QaBusStateSnapshot,
|
|
QaBusThread,
|
|
QaBusToolCall,
|
|
} from "./runtime-api.js";
|
|
|
|
export const DEFAULT_ACCOUNT_ID = "default";
|
|
|
|
export function normalizeAccountId(raw?: string): string {
|
|
const trimmed = raw?.trim();
|
|
return trimmed || DEFAULT_ACCOUNT_ID;
|
|
}
|
|
|
|
export function normalizeConversationFromTarget(target: string): {
|
|
conversation: QaBusConversation;
|
|
threadId?: string;
|
|
} {
|
|
const parsed = parseQaTarget(target);
|
|
return {
|
|
conversation: { id: parsed.conversationId, kind: parsed.chatType },
|
|
...(parsed.threadId !== undefined ? { threadId: parsed.threadId } : {}),
|
|
};
|
|
}
|
|
|
|
export function cloneMessage(message: QaBusMessage): QaBusMessage {
|
|
return {
|
|
...message,
|
|
conversation: { ...message.conversation },
|
|
attachments: (message.attachments ?? []).map((attachment) => cloneAttachment(attachment)),
|
|
...(message.nativeCommand ? { nativeCommand: { ...message.nativeCommand } } : {}),
|
|
toolCalls: message.toolCalls?.map((toolCall) => cloneToolCall(toolCall)),
|
|
reactions: message.reactions.map((reaction) => ({ ...reaction })),
|
|
};
|
|
}
|
|
|
|
function cloneAttachment(attachment: QaBusAttachment): QaBusAttachment {
|
|
return { ...attachment };
|
|
}
|
|
|
|
function cloneToolCall(toolCall: QaBusToolCall): QaBusToolCall {
|
|
return {
|
|
name: toolCall.name,
|
|
...(toolCall.arguments ? { arguments: structuredClone(toolCall.arguments) } : {}),
|
|
};
|
|
}
|
|
|
|
export function cloneEvent(event: QaBusEvent): QaBusEvent {
|
|
switch (event.kind) {
|
|
case "inbound-message":
|
|
case "outbound-message":
|
|
case "message-edited":
|
|
case "message-deleted":
|
|
case "reaction-added":
|
|
return { ...event, message: cloneMessage(event.message) };
|
|
case "thread-created":
|
|
return { ...event, thread: { ...event.thread } };
|
|
}
|
|
throw new Error("Unsupported QA bus event kind");
|
|
}
|
|
|
|
export function buildQaBusSnapshot(params: {
|
|
cursor: number;
|
|
conversations: Map<string, QaBusSnapshotConversation>;
|
|
threads: Map<string, QaBusThread>;
|
|
messages: Map<string, QaBusMessage>;
|
|
events: QaBusEvent[];
|
|
}): QaBusStateSnapshot {
|
|
return {
|
|
cursor: params.cursor,
|
|
conversations: Array.from(params.conversations.values()).map((conversation) =>
|
|
Object.assign({}, conversation),
|
|
),
|
|
threads: Array.from(params.threads.values()).map((thread) => Object.assign({}, thread)),
|
|
messages: Array.from(params.messages.values()).map((message) => cloneMessage(message)),
|
|
events: params.events.map((event) => cloneEvent(event)),
|
|
};
|
|
}
|
|
|
|
export function requireQaBusMessageForAccount(params: {
|
|
messages: Map<string, QaBusMessage>;
|
|
input: Pick<QaBusReadMessageInput, "accountId" | "messageId">;
|
|
}): QaBusMessage {
|
|
const accountId = normalizeAccountId(params.input.accountId);
|
|
let match: QaBusMessage | undefined;
|
|
for (const message of params.messages.values()) {
|
|
if (message.id !== params.input.messageId || message.accountId !== accountId) {
|
|
continue;
|
|
}
|
|
// Reads have no conversation selector, so never guess which chat to mutate.
|
|
if (match) {
|
|
throw new Error(
|
|
`qa-bus message id is ambiguous for selected account: ${params.input.messageId}`,
|
|
);
|
|
}
|
|
match = message;
|
|
}
|
|
if (!match) {
|
|
throw new Error(`qa-bus message not found: ${params.input.messageId}`);
|
|
}
|
|
return match;
|
|
}
|
|
|
|
export function readQaBusMessage(params: {
|
|
messages: Map<string, QaBusMessage>;
|
|
input: QaBusReadMessageInput;
|
|
}) {
|
|
const message = requireQaBusMessageForAccount(params);
|
|
return cloneMessage(message);
|
|
}
|
|
|
|
export function searchQaBusMessages(params: {
|
|
messages: Map<string, QaBusMessage>;
|
|
input: QaBusSearchMessagesInput;
|
|
}) {
|
|
const accountId = normalizeAccountId(params.input.accountId);
|
|
const limit = Math.max(1, Math.min(params.input.limit ?? 20, 100));
|
|
const query = normalizeOptionalLowercaseString(params.input.query);
|
|
return Array.from(params.messages.values())
|
|
.filter((message) => message.accountId === accountId && !message.deleted)
|
|
.filter((message) =>
|
|
params.input.conversationId !== undefined
|
|
? message.conversation.id === params.input.conversationId
|
|
: true,
|
|
)
|
|
.filter((message) =>
|
|
params.input.conversationKind
|
|
? message.conversation.kind === params.input.conversationKind
|
|
: true,
|
|
)
|
|
.filter((message) =>
|
|
params.input.threadId !== undefined
|
|
? (message.threadId ?? null) === params.input.threadId
|
|
: true,
|
|
)
|
|
.filter((message) => {
|
|
if (!query) {
|
|
return true;
|
|
}
|
|
const attachmentHaystack = message.attachments ?? [];
|
|
const searchableAttachmentText = attachmentHaystack
|
|
.flatMap((attachment) => [
|
|
attachment.fileName,
|
|
attachment.altText,
|
|
attachment.transcript,
|
|
attachment.mimeType,
|
|
])
|
|
.filter((value): value is string => Boolean(value))
|
|
.join(" ")
|
|
.toLowerCase();
|
|
const messageText = normalizeOptionalLowercaseString(message.text) ?? "";
|
|
const searchableToolText = (message.toolCalls ?? [])
|
|
.map((toolCall) => toolCall.name)
|
|
.join(" ")
|
|
.toLowerCase();
|
|
return `${messageText} ${searchableAttachmentText} ${searchableToolText}`.includes(query);
|
|
})
|
|
.slice(-limit)
|
|
.map((message) => cloneMessage(message));
|
|
}
|
|
|
|
export function resolveQaBusPollStartCursor(params: {
|
|
currentCursor: number;
|
|
requestedCursor?: number;
|
|
}): number {
|
|
const requestedCursor = params.requestedCursor ?? 0;
|
|
return params.currentCursor < requestedCursor ? 0 : requestedCursor;
|
|
}
|
|
|
|
export function pollQaBusEvents(params: {
|
|
events: QaBusEvent[];
|
|
cursor: number;
|
|
input?: QaBusPollInput;
|
|
}): QaBusPollResult {
|
|
const accountId = normalizeAccountId(params.input?.accountId);
|
|
const effectiveStartCursor = resolveQaBusPollStartCursor({
|
|
currentCursor: params.cursor,
|
|
requestedCursor: params.input?.cursor,
|
|
});
|
|
const limit = Math.max(1, Math.min(params.input?.limit ?? 100, 500));
|
|
const matchingEvents = params.events.filter(
|
|
(event) => event.accountId === accountId && event.cursor > effectiveStartCursor,
|
|
);
|
|
const page = matchingEvents.slice(0, limit);
|
|
// A limited page may advance only through events it returns. Jumping to the
|
|
// bus-wide cursor would make every remaining account event unreachable.
|
|
const nextCursor = matchingEvents.length > page.length ? page.at(-1)?.cursor : params.cursor;
|
|
return {
|
|
cursor: nextCursor ?? params.cursor,
|
|
events: page.map((event) => cloneEvent(event)),
|
|
};
|
|
}
|