UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

533 lines (532 loc) 18.1 kB
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { Fr as validateSessionTypingParams, Mr as validateSessionSuggestionsAddParams, Nr as validateSessionSuggestionsListParams, Pr as validateSessionSuggestionsResolveParams } from "./src-BiL5aQto.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import { a as listSessionSuggestions, i as finalizeSessionSuggestionClaim, n as addSessionSuggestion, o as releaseSessionSuggestionDispatch, r as claimSessionSuggestionDispatch, t as SESSION_SUGGESTION_DISPATCH_CLAIM_TTL_MS } from "./sessions-9nxpeTwt.js"; import { f as resolveSessionWorkStartError, u as isSessionWorkStartInvalidatedError } from "./lifecycle-BaroCBMc.js"; import { h as gatewayClientSessionCreator, i as operatorSessionCap, n as hasOperatorBoundary } from "./operator-role-policy-wsr1DeJv.js"; import { n as resolveSessionSubscriptionKeys } from "./session-subscription-keys-BwUDpvzj.js"; import { i as tryResolveSessionCompatibilityOwnerAgentId, n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js"; import { t as presenceUserKey } from "./presence-user-ulXqGuvR.js"; import { a as updateTypingConnections, i as liveViewerIdentities, n as broadcastTypingThrottled, t as TYPING_THROTTLE_MS } from "./session-typing-state-C5klAPZ6.js"; import { M as authorizeSessionSharingTarget, P as canManageSessionSharing, R as resolveSessionSharingRole, V as resolveSessionVisibility, k as authorizeIncognitoSessionTarget, r as createSessionListEntryFilter, z as resolveSessionSharingTarget } from "./session-sharing-B7MI8hNo.js"; import { t as handleChatSend } from "./chat-send-handler-Dpw17Gmy.js"; import { t as assertValidParams } from "./validation-pzrlzFvo.js"; import { m as sessionObserverScopeKey } from "./session-observer-model-D5MYTAfa.js"; //#region src/gateway/server-methods/sessions-suggestions-access.ts function requireSuggestionTarget(params) { const cfg = params.context.getRuntimeConfig(); const requestedAgent = resolveRequestedSessionAgentId(cfg, params.sessionKey, params.agentId); if (!requestedAgent.ok) { params.respond(false, void 0, requestedAgent.error); return null; } const target = resolveSessionSharingTarget({ cfg, sessionKey: params.sessionKey, agentId: requestedAgent.agentId }); if (!target || hasOperatorBoundary(params.client, cfg) && createSessionListEntryFilter({ client: params.client, cfg })?.(target.storeKey, target.entry) === false) { params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unknown session: ${params.sessionKey}`)); return null; } return target; } function requireVisibleSuggestionRole(params) { const role = resolveSessionSharingRole({ client: params.client, cfg: params.cfg, target: params.target }); const incognitoError = authorizeIncognitoSessionTarget({ client: params.client, sessionKey: params.sessionKey, target: params.target }); if (incognitoError) { params.respond(false, void 0, incognitoError); return null; } if (resolveSessionVisibility(params.target.entry) !== "draft") return role; const error = authorizeSessionSharingTarget({ client: params.client, cfg: params.cfg, target: params.target }); if (!error) return role; params.respond(false, void 0, error); return null; } function publishSuggestion(context, target, requestedSessionKey, event) { context.broadcast("session.suggestion", event, { sessionKeys: [.../* @__PURE__ */ new Set([ requestedSessionKey, target.canonicalKey, target.storeKey ])].toSorted(), agentId: event.suggestion.agentId }); } //#endregion //#region src/gateway/server-methods/sessions-suggestions.ts function suggestionScope(target) { return { agentId: target.agentId, sessionKey: target.storeKey, storePath: target.storePath }; } function protocolSuggestion(target, suggestion) { return { id: suggestion.id, sessionKey: target.canonicalKey, agentId: target.agentId, author: { type: "human", id: suggestion.authorId, ...suggestion.authorLabel ? { label: suggestion.authorLabel } : {} }, text: suggestion.text, createdAt: suggestion.createdAt, state: suggestion.state }; } function resolutionState(resolution) { return resolution === "dismiss" ? "dismissed" : "accepted"; } function respondSessionSuggestionSessionChanged(respond, sessionKey) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "session changed before suggestion resolution could be finalized", { retryable: false, details: { code: "SESSION_SUGGESTION_SESSION_CHANGED", sessionKey } })); } function runSessionSuggestionMutation(params) { try { return { ok: true, value: params.mutate() }; } catch (error) { if (!isSessionWorkStartInvalidatedError(error)) throw error; respondSessionSuggestionSessionChanged(params.respond, params.sessionKey); return { ok: false }; } } function attributedSuggestionClient(client, suggestion) { const label = suggestion.authorLabel ?? suggestion.authorId; return { ...client, internal: { ...client.internal, syntheticClient: true, senderAttribution: { id: suggestion.authorId, identity: { type: "profile", id: suggestion.authorId }, name: `Suggested by ${label}` } } }; } async function dispatchSuggestion(params) { let response; const chatParams = { sessionKey: params.target.canonicalKey, agentId: params.target.agentId, sessionId: params.target.entry.sessionId, message: params.suggestion.text, ...params.resolution === "queue" ? { queueMode: "followup" } : { queueMode: "steer" }, idempotencyKey: `session-suggestion:${params.suggestion.id}` }; await handleChatSend({ req: { ...params.req, method: "chat.send", params: chatParams }, params: chatParams, client: attributedSuggestionClient(params.client, params.suggestion), isWebchatConnect: params.isWebchatConnect, respond: (...args) => { response = args; }, context: params.context }); return response?.[0] === true ? { ok: true } : { ok: false, error: response?.[2] }; } const sessionSuggestionHandlers = { "session.suggestions.add": ({ params, respond, client, context }) => { if (!assertValidParams(params, validateSessionSuggestionsAddParams, "session.suggestions.add", respond)) return; const cfg = context.getRuntimeConfig(); const target = requireSuggestionTarget({ client, context, ...params, respond }); const author = gatewayClientSessionCreator(client); if (!target) return; const role = requireVisibleSuggestionRole({ client, cfg, sessionKey: params.sessionKey, target, respond }); if (role === null) return; if (role === "viewer" && operatorSessionCap(client, cfg) === "view") { respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, "your operator role permits viewing sessions only")); return; } const lifecycleError = resolveSessionWorkStartError(target.canonicalKey, target.entry); if (lifecycleError) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, lifecycleError)); return; } if (!author) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "identified suggestion author required")); return; } if (resolveSessionVisibility(target.entry) !== "suggest") { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "session is not accepting suggestions")); return; } const text = params.text; if (!text.trim()) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "suggestion text is required")); return; } let suggestion; try { suggestion = addSessionSuggestion(suggestionScope(target), { authorId: author.id, authorLabel: author.label, text, expectedSessionId: target.entry.sessionId }); } catch (error) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error instanceof Error ? error.message : "suggestion could not be stored")); return; } const projected = protocolSuggestion(target, suggestion); publishSuggestion(context, target, params.sessionKey, { action: "added", suggestion: projected }); respond(true, { suggestion: projected }); }, "session.suggestions.list": ({ params, respond, client, context }) => { if (!assertValidParams(params, validateSessionSuggestionsListParams, "session.suggestions.list", respond)) return; const cfg = context.getRuntimeConfig(); const target = requireSuggestionTarget({ client, context, ...params, respond }); if (!target) return; const role = requireVisibleSuggestionRole({ client, cfg, sessionKey: params.sessionKey, target, respond }); if (role === null) return; const identity = gatewayClientSessionCreator(client); respond(true, { role, suggestions: (role === "viewer" ? identity ? listSessionSuggestions(suggestionScope(target), { authorId: identity.id }) : [] : listSessionSuggestions(suggestionScope(target)).filter((suggestion) => suggestion.state === "pending" || suggestion.authorId === identity?.id)).map((suggestion) => protocolSuggestion(target, suggestion)) }); }, "session.suggestions.resolve": async ({ params, respond, client, context, req, isWebchatConnect }) => { if (!assertValidParams(params, validateSessionSuggestionsResolveParams, "session.suggestions.resolve", respond)) return; const cfg = context.getRuntimeConfig(); const target = requireSuggestionTarget({ client, context, ...params, respond }); if (!target) return; const role = requireVisibleSuggestionRole({ client, cfg, sessionKey: params.sessionKey, target, respond }); if (role === null) return; if (role !== "owner" && role !== "admin") { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "session owner or operator.admin required")); return; } const resolution = params.resolution; const dispatching = resolution === "send" || resolution === "queue"; if (resolution !== "dismiss") { const lifecycleError = resolveSessionWorkStartError(target.canonicalKey, target.entry); if (lifecycleError) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, lifecycleError)); return; } } if (dispatching && !client) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "connected client required for suggestion dispatch")); return; } const scope = suggestionScope(target); const claimResult = runSessionSuggestionMutation({ respond, sessionKey: params.sessionKey, mutate: () => claimSessionSuggestionDispatch(scope, { id: params.id, resolution, expectedSessionId: target.entry.sessionId }) }); if (!claimResult.ok) return; const claim = claimResult.value; if (!claim) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "pending suggestion not found")); return; } if (claim.kind === "busy") { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "suggestion resolution is already in progress", { retryable: true, retryAfterMs: SESSION_SUGGESTION_DISPATCH_CLAIM_TTL_MS })); return; } if (claim.kind === "mismatch") { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `suggestion dispatch recovery must retry the original ${claim.resolution} action`)); return; } if (dispatching && client) { let dispatched; try { dispatched = await dispatchSuggestion({ context, client, req, isWebchatConnect, target, suggestion: claim.suggestion, resolution }); } catch (error) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, error instanceof Error ? error.message : "suggestion dispatch outcome is unknown", { retryable: true, retryAfterMs: SESSION_SUGGESTION_DISPATCH_CLAIM_TTL_MS })); return; } if (!dispatched.ok) { let releaseResult; try { releaseResult = runSessionSuggestionMutation({ respond, sessionKey: params.sessionKey, mutate: () => releaseSessionSuggestionDispatch(scope, { id: claim.suggestion.id, token: claim.token, expectedSessionId: target.entry.sessionId }) }); } catch (error) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, error instanceof Error ? error.message : "suggestion dispatch outcome is unknown", { retryable: true, retryAfterMs: SESSION_SUGGESTION_DISPATCH_CLAIM_TTL_MS })); return; } if (!releaseResult.ok) return; respond(false, void 0, dispatched.error ?? errorShape(ErrorCodes.INVALID_REQUEST, "suggestion dispatch failed")); return; } } const currentTarget = resolveSessionSharingTarget({ cfg: context.getRuntimeConfig(), sessionKey: params.sessionKey, agentId: target.agentId }); if (!currentTarget || currentTarget.entry.sessionId !== target.entry.sessionId) { respondSessionSuggestionSessionChanged(respond, params.sessionKey); return; } const finalizeResult = runSessionSuggestionMutation({ respond, sessionKey: params.sessionKey, mutate: () => finalizeSessionSuggestionClaim(scope, { id: claim.suggestion.id, token: claim.token, state: resolutionState(resolution), expectedSessionId: target.entry.sessionId }) }); if (!finalizeResult.ok) return; const suggestion = finalizeResult.value; if (!suggestion) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "suggestion resolution could not be finalized", { retryable: true })); return; } const projected = protocolSuggestion(target, suggestion); publishSuggestion(context, target, params.sessionKey, { action: "resolved", suggestion: projected }); respond(true, { suggestion: projected }); }, "session.typing": ({ params: requestParams, respond, client, context }) => { const params = typeof requestParams.preview === "string" ? { ...requestParams, preview: Array.from(requestParams.preview.trim()).slice(0, 400).join("") } : requestParams; if (!assertValidParams(params, validateSessionTypingParams, "session.typing", respond)) return; const cfg = context.getRuntimeConfig(); const target = requireSuggestionTarget({ client, context, ...params, respond }); const actor = gatewayClientSessionCreator(client); if (!target) return; const incognitoError = authorizeIncognitoSessionTarget({ client, sessionKey: params.sessionKey, target }); if (incognitoError) { respond(false, void 0, incognitoError); return; } if (params.sessionId !== target.entry.sessionId) { respond(true, { ok: true, broadcast: false }); return; } if (!actor) { respond(true, { ok: true, broadcast: false }); return; } const role = resolveSessionSharingRole({ client, cfg, target }); const visibility = resolveSessionVisibility(target.entry); if (role === "viewer" && operatorSessionCap(client, cfg) === "view") { respond(true, { ok: true, broadcast: false }); return; } if (visibility === "draft" && !canManageSessionSharing(role)) { respond(true, { ok: true, broadcast: false }); return; } if (role === "viewer" && visibility !== "shared" && visibility !== "suggest") { respond(true, { ok: true, broadcast: false }); return; } if (params.typing) context.recordClientActivity?.(client); const sessionKeys = /* @__PURE__ */ new Set([ params.sessionKey, target.canonicalKey, target.storeKey, sessionObserverScopeKey(target.canonicalKey, target.agentId) ]); const now = Date.now(); const typingKey = `${actor.id}\0${target.agentId}\0${target.canonicalKey}\0${target.entry.sessionId}`; const { typing: effectiveTyping, preview } = updateTypingConnections({ key: typingKey, connectionId: client?.connId ?? actor.id, typing: params.typing, ...params.typing && params.preview ? { preview: params.preview } : {}, now }); if (!params.typing && effectiveTyping) { respond(true, { ok: true, broadcast: false }); return; } respond(true, { ok: true, broadcast: broadcastTypingThrottled({ key: typingKey, typing: effectiveTyping, signature: `${effectiveTyping}\0${preview ?? ""}`, intervalMs: preview ? 250 : TYPING_THROTTLE_MS, now, emit: () => { const current = resolveSessionSharingTarget({ cfg: context.getRuntimeConfig(), sessionKey: params.sessionKey, agentId: target.agentId }); if (!current || current.entry.sessionId !== target.entry.sessionId) return false; const currentCfg = context.getRuntimeConfig(); const currentRole = resolveSessionSharingRole({ client, cfg: currentCfg, target: current }); const currentVisibility = resolveSessionVisibility(current.entry); if (currentRole === "viewer" && operatorSessionCap(client, currentCfg) === "view") return false; if (currentVisibility === "draft" && !canManageSessionSharing(currentRole)) return false; if (currentRole === "viewer" && currentVisibility !== "shared" && currentVisibility !== "suggest") return false; const liveIdentities = liveViewerIdentities(sessionKeys); const actorKey = presenceUserKey({ id: actor.id, identity: { type: "profile", id: actor.id } }); if (liveIdentities.size < 2 || !liveIdentities.has(actorKey)) return false; const event = { sessionKey: target.canonicalKey, sessionId: current.entry.sessionId, agentId: target.agentId, actor, typing: effectiveTyping, ...preview ? { preview } : {}, ts: Date.now() }; context.broadcast("session.typing", event, { sessionKeys: resolveSessionSubscriptionKeys(current.canonicalKey, current.agentId, current.canonicalKey === "global" ? tryResolveSessionCompatibilityOwnerAgentId(context.getRuntimeConfig(), current.canonicalKey) : void 0), agentId: target.agentId, dropIfSlow: true }); return true; } }) }); } }; //#endregion export { sessionSuggestionHandlers };