mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-09 08:11:09 +00:00
Merged via squash.
Prepared head SHA: b062b18b03
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
194 lines
5.3 KiB
TypeScript
194 lines
5.3 KiB
TypeScript
export type ParsedAgentSessionKey = {
|
|
agentId: string;
|
|
rest: string;
|
|
};
|
|
|
|
export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown";
|
|
export type ParsedThreadSessionSuffix = {
|
|
baseSessionKey: string | undefined;
|
|
threadId: string | undefined;
|
|
};
|
|
|
|
export type RawSessionConversationRef = {
|
|
channel: string;
|
|
kind: "group" | "channel";
|
|
rawId: string;
|
|
prefix: string;
|
|
};
|
|
|
|
/**
|
|
* Parse agent-scoped session keys in a canonical, case-insensitive way.
|
|
* Returned values are normalized to lowercase for stable comparisons/routing.
|
|
*/
|
|
export function parseAgentSessionKey(
|
|
sessionKey: string | undefined | null,
|
|
): ParsedAgentSessionKey | null {
|
|
const raw = (sessionKey ?? "").trim().toLowerCase();
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
const parts = raw.split(":").filter(Boolean);
|
|
if (parts.length < 3) {
|
|
return null;
|
|
}
|
|
if (parts[0] !== "agent") {
|
|
return null;
|
|
}
|
|
const agentId = parts[1]?.trim();
|
|
const rest = parts.slice(2).join(":");
|
|
if (!agentId || !rest) {
|
|
return null;
|
|
}
|
|
return { agentId, rest };
|
|
}
|
|
|
|
/**
|
|
* Best-effort chat-type extraction from session keys across canonical and legacy formats.
|
|
*/
|
|
export function deriveSessionChatType(sessionKey: string | undefined | null): SessionKeyChatType {
|
|
const raw = (sessionKey ?? "").trim().toLowerCase();
|
|
if (!raw) {
|
|
return "unknown";
|
|
}
|
|
const scoped = parseAgentSessionKey(raw)?.rest ?? raw;
|
|
const tokens = new Set(scoped.split(":").filter(Boolean));
|
|
if (tokens.has("group")) {
|
|
return "group";
|
|
}
|
|
if (tokens.has("channel")) {
|
|
return "channel";
|
|
}
|
|
if (tokens.has("direct") || tokens.has("dm")) {
|
|
return "direct";
|
|
}
|
|
// Legacy Discord keys can be shaped like:
|
|
// discord:<accountId>:guild-<guildId>:channel-<channelId>
|
|
if (/^discord:(?:[^:]+:)?guild-[^:]+:channel-[^:]+$/.test(scoped)) {
|
|
return "channel";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
export function isCronRunSessionKey(sessionKey: string | undefined | null): boolean {
|
|
const parsed = parseAgentSessionKey(sessionKey);
|
|
if (!parsed) {
|
|
return false;
|
|
}
|
|
return /^cron:[^:]+:run:[^:]+$/.test(parsed.rest);
|
|
}
|
|
|
|
export function isCronSessionKey(sessionKey: string | undefined | null): boolean {
|
|
const parsed = parseAgentSessionKey(sessionKey);
|
|
if (!parsed) {
|
|
return false;
|
|
}
|
|
return parsed.rest.toLowerCase().startsWith("cron:");
|
|
}
|
|
|
|
export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean {
|
|
const raw = (sessionKey ?? "").trim();
|
|
if (!raw) {
|
|
return false;
|
|
}
|
|
if (raw.toLowerCase().startsWith("subagent:")) {
|
|
return true;
|
|
}
|
|
const parsed = parseAgentSessionKey(raw);
|
|
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("subagent:"));
|
|
}
|
|
|
|
export function getSubagentDepth(sessionKey: string | undefined | null): number {
|
|
const raw = (sessionKey ?? "").trim().toLowerCase();
|
|
if (!raw) {
|
|
return 0;
|
|
}
|
|
return raw.split(":subagent:").length - 1;
|
|
}
|
|
|
|
export function isAcpSessionKey(sessionKey: string | undefined | null): boolean {
|
|
const raw = (sessionKey ?? "").trim();
|
|
if (!raw) {
|
|
return false;
|
|
}
|
|
const normalized = raw.toLowerCase();
|
|
if (normalized.startsWith("acp:")) {
|
|
return true;
|
|
}
|
|
const parsed = parseAgentSessionKey(raw);
|
|
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("acp:"));
|
|
}
|
|
|
|
function normalizeSessionConversationChannel(value: string | undefined | null): string | undefined {
|
|
const trimmed = (value ?? "").trim().toLowerCase();
|
|
return trimmed || undefined;
|
|
}
|
|
|
|
export function parseThreadSessionSuffix(
|
|
sessionKey: string | undefined | null,
|
|
): ParsedThreadSessionSuffix {
|
|
const raw = (sessionKey ?? "").trim();
|
|
if (!raw) {
|
|
return { baseSessionKey: undefined, threadId: undefined };
|
|
}
|
|
|
|
const lowerRaw = raw.toLowerCase();
|
|
const threadMarker = ":thread:";
|
|
const threadIndex = lowerRaw.lastIndexOf(threadMarker);
|
|
const markerIndex = threadIndex;
|
|
const marker = threadMarker;
|
|
|
|
const baseSessionKey = markerIndex === -1 ? raw : raw.slice(0, markerIndex);
|
|
const threadIdRaw = markerIndex === -1 ? undefined : raw.slice(markerIndex + marker.length);
|
|
const threadId = threadIdRaw?.trim() || undefined;
|
|
|
|
return { baseSessionKey, threadId };
|
|
}
|
|
|
|
export function parseRawSessionConversationRef(
|
|
sessionKey: string | undefined | null,
|
|
): RawSessionConversationRef | null {
|
|
const raw = (sessionKey ?? "").trim();
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
|
|
const rawParts = raw.split(":").filter(Boolean);
|
|
const bodyStartIndex =
|
|
rawParts.length >= 3 && rawParts[0]?.trim().toLowerCase() === "agent" ? 2 : 0;
|
|
const parts = rawParts.slice(bodyStartIndex);
|
|
if (parts.length < 3) {
|
|
return null;
|
|
}
|
|
|
|
const channel = normalizeSessionConversationChannel(parts[0]);
|
|
const kind = parts[1]?.trim().toLowerCase();
|
|
if (!channel || (kind !== "group" && kind !== "channel")) {
|
|
return null;
|
|
}
|
|
|
|
const rawId = parts.slice(2).join(":").trim();
|
|
const prefix = rawParts
|
|
.slice(0, bodyStartIndex + 2)
|
|
.join(":")
|
|
.trim();
|
|
if (!rawId || !prefix) {
|
|
return null;
|
|
}
|
|
|
|
return { channel, kind, rawId, prefix };
|
|
}
|
|
|
|
export function resolveThreadParentSessionKey(
|
|
sessionKey: string | undefined | null,
|
|
): string | null {
|
|
const { baseSessionKey, threadId } = parseThreadSessionSuffix(sessionKey);
|
|
if (!threadId) {
|
|
return null;
|
|
}
|
|
const parent = baseSessionKey?.trim();
|
|
if (!parent) {
|
|
return null;
|
|
}
|
|
return parent;
|
|
}
|