UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

609 lines (606 loc) 23.2 kB
import { Yt as resolveStateDir } from "./entry.js"; import { Bt as DEFAULT_CONTEXT_TOKENS, H as resolveDefaultModelForAgent, Ht as DEFAULT_PROVIDER, R as parseModelRef, T as resolveOpenClawAgentDir, V as resolveConfiguredModelRef, Vt as DEFAULT_MODEL } from "./auth-profiles-DFa1zzNy.js"; import { l as normalizeAgentId, u as normalizeMainKey, v as isCronRunSessionKey, x as parseAgentSessionKey } from "./session-key-BGiG_JcT.js"; import { c as resolveAgentWorkspaceDir, l as resolveDefaultAgentId } from "./agent-scope-RzK9Zcks.js"; import { i as loadConfig } from "./config-B2kL1ciP.js"; import { A as resolveFreshSessionTotalTokens, M as resolveAgentMainSessionKey, P as resolveMainSessionKey, R as buildGroupDisplayName, a as loadSessionStore, j as canonicalizeMainSessionAlias, q as normalizeSessionDeliveryFields, y as readSessionTitleFieldsFromTranscript } from "./sessions-BD5dyLxb.js"; import { c as resolveStorePath } from "./paths-CXpciDEv.js"; import { t as ensureOpenClawModelsJson } from "./models-config-5zJ39mUb.js"; import path from "node:path"; import fs from "node:fs"; //#region src/agents/context.ts function applyDiscoveredContextWindows(params) { for (const model of params.models) { if (!model?.id) continue; const contextWindow = typeof model.contextWindow === "number" ? Math.trunc(model.contextWindow) : void 0; if (!contextWindow || contextWindow <= 0) continue; const existing = params.cache.get(model.id); if (existing === void 0 || contextWindow < existing) params.cache.set(model.id, contextWindow); } } function applyConfiguredContextWindows(params) { const providers = params.modelsConfig?.providers; if (!providers || typeof providers !== "object") return; for (const provider of Object.values(providers)) { if (!Array.isArray(provider?.models)) continue; for (const model of provider.models) { const modelId = typeof model?.id === "string" ? model.id : void 0; const contextWindow = typeof model?.contextWindow === "number" ? model.contextWindow : void 0; if (!modelId || !contextWindow || contextWindow <= 0) continue; params.cache.set(modelId, contextWindow); } } } const MODEL_CACHE = /* @__PURE__ */ new Map(); (async () => { let cfg; try { cfg = loadConfig(); } catch { return; } try { await ensureOpenClawModelsJson(cfg); } catch {} try { const { discoverAuthStorage, discoverModels } = await import("./pi-model-discovery-Do3xMEtM.js").then((n) => n.r); const agentDir = resolveOpenClawAgentDir(); const modelRegistry = discoverModels(discoverAuthStorage(agentDir), agentDir); applyDiscoveredContextWindows({ cache: MODEL_CACHE, models: typeof modelRegistry.getAvailable === "function" ? modelRegistry.getAvailable() : modelRegistry.getAll() }); } catch {} applyConfiguredContextWindows({ cache: MODEL_CACHE, modelsConfig: cfg.models }); })().catch(() => {}); function lookupContextTokens(modelId) { if (!modelId) return; return MODEL_CACHE.get(modelId); } //#endregion //#region src/gateway/session-utils.ts const DERIVED_TITLE_MAX_LEN = 60; const AVATAR_MAX_BYTES = 2 * 1024 * 1024; const AVATAR_DATA_RE = /^data:/i; const AVATAR_HTTP_RE = /^https?:\/\//i; const AVATAR_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i; const WINDOWS_ABS_RE = /^[a-zA-Z]:[\\/]/; const AVATAR_MIME_BY_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".svg": "image/svg+xml", ".bmp": "image/bmp", ".tif": "image/tiff", ".tiff": "image/tiff" }; function resolveAvatarMime(filePath) { return AVATAR_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? "application/octet-stream"; } function isWorkspaceRelativePath(value) { if (!value) return false; if (value.startsWith("~")) return false; if (AVATAR_SCHEME_RE.test(value) && !WINDOWS_ABS_RE.test(value)) return false; return true; } function resolveIdentityAvatarUrl(cfg, agentId, avatar) { if (!avatar) return; const trimmed = avatar.trim(); if (!trimmed) return; if (AVATAR_DATA_RE.test(trimmed) || AVATAR_HTTP_RE.test(trimmed)) return trimmed; if (!isWorkspaceRelativePath(trimmed)) return; const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const workspaceRoot = path.resolve(workspaceDir); const resolved = path.resolve(workspaceRoot, trimmed); const relative = path.relative(workspaceRoot, resolved); if (relative.startsWith("..") || path.isAbsolute(relative)) return; try { const stat = fs.statSync(resolved); if (!stat.isFile() || stat.size > AVATAR_MAX_BYTES) return; const buffer = fs.readFileSync(resolved); return `data:${resolveAvatarMime(resolved)};base64,${buffer.toString("base64")}`; } catch { return; } } function formatSessionIdPrefix(sessionId, updatedAt) { const prefix = sessionId.slice(0, 8); if (updatedAt && updatedAt > 0) return `${prefix} (${new Date(updatedAt).toISOString().slice(0, 10)})`; return prefix; } function truncateTitle(text, maxLen) { if (text.length <= maxLen) return text; const cut = text.slice(0, maxLen - 1); const lastSpace = cut.lastIndexOf(" "); if (lastSpace > maxLen * .6) return cut.slice(0, lastSpace) + "…"; return cut + "…"; } function deriveSessionTitle(entry, firstUserMessage) { if (!entry) return; if (entry.displayName?.trim()) return entry.displayName.trim(); if (entry.subject?.trim()) return entry.subject.trim(); if (firstUserMessage?.trim()) return truncateTitle(firstUserMessage.replace(/\s+/g, " ").trim(), DERIVED_TITLE_MAX_LEN); if (entry.sessionId) return formatSessionIdPrefix(entry.sessionId, entry.updatedAt); } function loadSessionEntry(sessionKey) { const cfg = loadConfig(); const sessionCfg = cfg.session; const canonicalKey = resolveSessionStoreKey({ cfg, sessionKey }); const agentId = resolveSessionStoreAgentId(cfg, canonicalKey); const storePath = resolveStorePath(sessionCfg?.store, { agentId }); const store = loadSessionStore(storePath); const match = findStoreMatch(store, canonicalKey, sessionKey.trim()); const legacyKey = match?.key !== canonicalKey ? match?.key : void 0; return { cfg, storePath, store, entry: match?.entry, canonicalKey, legacyKey }; } /** * Find a session entry by exact or case-insensitive key match. * Returns both the entry and the actual store key it was found under, * so callers can clean up legacy mixed-case keys when they differ from canonicalKey. */ function findStoreMatch(store, ...candidates) { for (const candidate of candidates) if (candidate && store[candidate]) return { entry: store[candidate], key: candidate }; const loweredSet = new Set(candidates.filter(Boolean).map((c) => c.toLowerCase())); for (const key of Object.keys(store)) if (loweredSet.has(key.toLowerCase())) return { entry: store[key], key }; } /** * Find all on-disk store keys that match the given key case-insensitively. * Returns every key from the store whose lowercased form equals the target's lowercased form. */ function findStoreKeysIgnoreCase(store, targetKey) { const lowered = targetKey.toLowerCase(); const matches = []; for (const key of Object.keys(store)) if (key.toLowerCase() === lowered) matches.push(key); return matches; } /** * Remove legacy key variants for one canonical session key. * Candidates can include aliases (for example, "agent:ops:main" when canonical is "agent:ops:work"). */ function pruneLegacyStoreKeys(params) { const keysToDelete = /* @__PURE__ */ new Set(); for (const candidate of params.candidates) { const trimmed = String(candidate ?? "").trim(); if (!trimmed) continue; if (trimmed !== params.canonicalKey) keysToDelete.add(trimmed); for (const match of findStoreKeysIgnoreCase(params.store, trimmed)) if (match !== params.canonicalKey) keysToDelete.add(match); } for (const key of keysToDelete) delete params.store[key]; } function classifySessionKey(key, entry) { if (key === "global") return "global"; if (key === "unknown") return "unknown"; if (entry?.chatType === "group" || entry?.chatType === "channel") return "group"; if (key.includes(":group:") || key.includes(":channel:")) return "group"; return "direct"; } function parseGroupKey(key) { const parts = (parseAgentSessionKey(key)?.rest ?? key).split(":").filter(Boolean); if (parts.length >= 3) { const [channel, kind, ...rest] = parts; if (kind === "group" || kind === "channel") return { channel, kind, id: rest.join(":") }; } return null; } function isStorePathTemplate(store) { return typeof store === "string" && store.includes("{agentId}"); } function listExistingAgentIdsFromDisk() { const root = resolveStateDir(); const agentsDir = path.join(root, "agents"); try { return fs.readdirSync(agentsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => normalizeAgentId(entry.name)).filter(Boolean); } catch { return []; } } function listConfiguredAgentIds(cfg) { const agents = cfg.agents?.list ?? []; if (agents.length > 0) { const ids = /* @__PURE__ */ new Set(); for (const entry of agents) if (entry?.id) ids.add(normalizeAgentId(entry.id)); const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); ids.add(defaultId); const sorted = Array.from(ids).filter(Boolean); sorted.sort((a, b) => a.localeCompare(b)); return sorted.includes(defaultId) ? [defaultId, ...sorted.filter((id) => id !== defaultId)] : sorted; } const ids = /* @__PURE__ */ new Set(); const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); ids.add(defaultId); for (const id of listExistingAgentIdsFromDisk()) ids.add(id); const sorted = Array.from(ids).filter(Boolean); sorted.sort((a, b) => a.localeCompare(b)); if (sorted.includes(defaultId)) return [defaultId, ...sorted.filter((id) => id !== defaultId)]; return sorted; } function listAgentsForGateway(cfg) { const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); const mainKey = normalizeMainKey(cfg.session?.mainKey); const scope = cfg.session?.scope ?? "per-sender"; const configuredById = /* @__PURE__ */ new Map(); for (const entry of cfg.agents?.list ?? []) { if (!entry?.id) continue; const identity = entry.identity ? { name: entry.identity.name?.trim() || void 0, theme: entry.identity.theme?.trim() || void 0, emoji: entry.identity.emoji?.trim() || void 0, avatar: entry.identity.avatar?.trim() || void 0, avatarUrl: resolveIdentityAvatarUrl(cfg, normalizeAgentId(entry.id), entry.identity.avatar?.trim()) } : void 0; configuredById.set(normalizeAgentId(entry.id), { name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : void 0, identity }); } const explicitIds = new Set((cfg.agents?.list ?? []).map((entry) => entry?.id ? normalizeAgentId(entry.id) : "").filter(Boolean)); const allowedIds = explicitIds.size > 0 ? new Set([...explicitIds, defaultId]) : null; let agentIds = listConfiguredAgentIds(cfg).filter((id) => allowedIds ? allowedIds.has(id) : true); if (mainKey && !agentIds.includes(mainKey) && (!allowedIds || allowedIds.has(mainKey))) agentIds = [...agentIds, mainKey]; return { defaultId, mainKey, scope, agents: agentIds.map((id) => { const meta = configuredById.get(id); return { id, name: meta?.name, identity: meta?.identity }; }) }; } function canonicalizeSessionKeyForAgent(agentId, key) { const lowered = key.toLowerCase(); if (lowered === "global" || lowered === "unknown") return lowered; if (lowered.startsWith("agent:")) return lowered; return `agent:${normalizeAgentId(agentId)}:${lowered}`; } function resolveDefaultStoreAgentId(cfg) { return normalizeAgentId(resolveDefaultAgentId(cfg)); } function resolveSessionStoreKey(params) { const raw = params.sessionKey.trim(); if (!raw) return raw; const rawLower = raw.toLowerCase(); if (rawLower === "global" || rawLower === "unknown") return rawLower; const parsed = parseAgentSessionKey(raw); if (parsed) { const agentId = normalizeAgentId(parsed.agentId); const lowered = raw.toLowerCase(); const canonical = canonicalizeMainSessionAlias({ cfg: params.cfg, agentId, sessionKey: lowered }); if (canonical !== lowered) return canonical; return lowered; } const lowered = raw.toLowerCase(); const rawMainKey = normalizeMainKey(params.cfg.session?.mainKey); if (lowered === "main" || lowered === rawMainKey) return resolveMainSessionKey(params.cfg); return canonicalizeSessionKeyForAgent(resolveDefaultStoreAgentId(params.cfg), lowered); } function resolveSessionStoreAgentId(cfg, canonicalKey) { if (canonicalKey === "global" || canonicalKey === "unknown") return resolveDefaultStoreAgentId(cfg); const parsed = parseAgentSessionKey(canonicalKey); if (parsed?.agentId) return normalizeAgentId(parsed.agentId); return resolveDefaultStoreAgentId(cfg); } function canonicalizeSpawnedByForAgent(cfg, agentId, spawnedBy) { const raw = spawnedBy?.trim(); if (!raw) return; const lower = raw.toLowerCase(); if (lower === "global" || lower === "unknown") return lower; let result; if (raw.toLowerCase().startsWith("agent:")) result = raw.toLowerCase(); else result = `agent:${normalizeAgentId(agentId)}:${lower}`; const parsed = parseAgentSessionKey(result); return canonicalizeMainSessionAlias({ cfg, agentId: parsed?.agentId ? normalizeAgentId(parsed.agentId) : agentId, sessionKey: result }); } function resolveGatewaySessionStoreTarget(params) { const key = params.key.trim(); const canonicalKey = resolveSessionStoreKey({ cfg: params.cfg, sessionKey: key }); const agentId = resolveSessionStoreAgentId(params.cfg, canonicalKey); const storeConfig = params.cfg.session?.store; const storePath = resolveStorePath(storeConfig, { agentId }); if (canonicalKey === "global" || canonicalKey === "unknown") return { agentId, storePath, canonicalKey, storeKeys: key && key !== canonicalKey ? [canonicalKey, key] : [key] }; const storeKeys = /* @__PURE__ */ new Set(); storeKeys.add(canonicalKey); if (key && key !== canonicalKey) storeKeys.add(key); if (params.scanLegacyKeys !== false) { const scanTargets = new Set(storeKeys); if (canonicalKey === resolveAgentMainSessionKey({ cfg: params.cfg, agentId })) scanTargets.add(`agent:${agentId}:main`); const store = params.store ?? loadSessionStore(storePath); for (const seed of scanTargets) for (const legacyKey of findStoreKeysIgnoreCase(store, seed)) storeKeys.add(legacyKey); } return { agentId, storePath, canonicalKey, storeKeys: Array.from(storeKeys) }; } function mergeSessionEntryIntoCombined(params) { const { cfg, combined, entry, agentId, canonicalKey } = params; const existing = combined[canonicalKey]; if (existing && (existing.updatedAt ?? 0) > (entry.updatedAt ?? 0)) combined[canonicalKey] = { ...entry, ...existing, spawnedBy: canonicalizeSpawnedByForAgent(cfg, agentId, existing.spawnedBy ?? entry.spawnedBy) }; else combined[canonicalKey] = { ...existing, ...entry, spawnedBy: canonicalizeSpawnedByForAgent(cfg, agentId, entry.spawnedBy ?? existing?.spawnedBy) }; } function loadCombinedSessionStoreForGateway(cfg) { const storeConfig = cfg.session?.store; if (storeConfig && !isStorePathTemplate(storeConfig)) { const storePath = resolveStorePath(storeConfig); const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg)); const store = loadSessionStore(storePath); const combined = {}; for (const [key, entry] of Object.entries(store)) mergeSessionEntryIntoCombined({ cfg, combined, entry, agentId: defaultAgentId, canonicalKey: canonicalizeSessionKeyForAgent(defaultAgentId, key) }); return { storePath, store: combined }; } const agentIds = listConfiguredAgentIds(cfg); const combined = {}; for (const agentId of agentIds) { const store = loadSessionStore(resolveStorePath(storeConfig, { agentId })); for (const [key, entry] of Object.entries(store)) mergeSessionEntryIntoCombined({ cfg, combined, entry, agentId, canonicalKey: canonicalizeSessionKeyForAgent(agentId, key) }); } return { storePath: typeof storeConfig === "string" && storeConfig.trim() ? storeConfig.trim() : "(multiple)", store: combined }; } function getSessionDefaults(cfg) { const resolved = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL }); const contextTokens = cfg.agents?.defaults?.contextTokens ?? lookupContextTokens(resolved.model) ?? DEFAULT_CONTEXT_TOKENS; return { modelProvider: resolved.provider ?? null, model: resolved.model ?? null, contextTokens: contextTokens ?? null }; } function resolveSessionModelRef(cfg, entry, agentId) { const resolved = agentId ? resolveDefaultModelForAgent({ cfg, agentId }) : resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL }); let provider = resolved.provider; let model = resolved.model; const runtimeModel = entry?.model?.trim(); const runtimeProvider = entry?.modelProvider?.trim(); if (runtimeModel) { const parsedRuntime = parseModelRef(runtimeModel, runtimeProvider || provider || DEFAULT_PROVIDER); if (parsedRuntime) { provider = parsedRuntime.provider; model = parsedRuntime.model; } else { provider = runtimeProvider || provider; model = runtimeModel; } return { provider, model }; } const storedModelOverride = entry?.modelOverride?.trim(); if (storedModelOverride) { const overrideProvider = entry?.providerOverride?.trim() || provider || DEFAULT_PROVIDER; const parsedOverride = parseModelRef(storedModelOverride, overrideProvider); if (parsedOverride) { provider = parsedOverride.provider; model = parsedOverride.model; } else { provider = overrideProvider; model = storedModelOverride; } } return { provider, model }; } function listSessionsFromStore(params) { const { cfg, storePath, store, opts } = params; const now = Date.now(); const includeGlobal = opts.includeGlobal === true; const includeUnknown = opts.includeUnknown === true; const includeDerivedTitles = opts.includeDerivedTitles === true; const includeLastMessage = opts.includeLastMessage === true; const spawnedBy = typeof opts.spawnedBy === "string" ? opts.spawnedBy : ""; const label = typeof opts.label === "string" ? opts.label.trim() : ""; const agentId = typeof opts.agentId === "string" ? normalizeAgentId(opts.agentId) : ""; const search = typeof opts.search === "string" ? opts.search.trim().toLowerCase() : ""; const activeMinutes = typeof opts.activeMinutes === "number" && Number.isFinite(opts.activeMinutes) ? Math.max(1, Math.floor(opts.activeMinutes)) : void 0; let sessions = Object.entries(store).filter(([key]) => { if (isCronRunSessionKey(key)) return false; if (!includeGlobal && key === "global") return false; if (!includeUnknown && key === "unknown") return false; if (agentId) { if (key === "global" || key === "unknown") return false; const parsed = parseAgentSessionKey(key); if (!parsed) return false; return normalizeAgentId(parsed.agentId) === agentId; } return true; }).filter(([key, entry]) => { if (!spawnedBy) return true; if (key === "unknown" || key === "global") return false; return entry?.spawnedBy === spawnedBy; }).filter(([, entry]) => { if (!label) return true; return entry?.label === label; }).map(([key, entry]) => { const updatedAt = entry?.updatedAt ?? null; const total = resolveFreshSessionTotalTokens(entry); const totalTokensFresh = typeof entry?.totalTokens === "number" ? entry?.totalTokensFresh !== false : false; const parsed = parseGroupKey(key); const channel = entry?.channel ?? parsed?.channel; const subject = entry?.subject; const groupChannel = entry?.groupChannel; const space = entry?.space; const id = parsed?.id; const origin = entry?.origin; const originLabel = origin?.label; const displayName = entry?.displayName ?? (channel ? buildGroupDisplayName({ provider: channel, subject, groupChannel, space, id, key }) : void 0) ?? entry?.label ?? originLabel; const deliveryFields = normalizeSessionDeliveryFields(entry); const resolvedModel = resolveSessionModelRef(cfg, entry, normalizeAgentId(parseAgentSessionKey(key)?.agentId ?? resolveDefaultAgentId(cfg))); const modelProvider = resolvedModel.provider ?? DEFAULT_PROVIDER; const model = resolvedModel.model ?? DEFAULT_MODEL; return { key, entry, kind: classifySessionKey(key, entry), label: entry?.label, displayName, channel, subject, groupChannel, space, chatType: entry?.chatType, origin, updatedAt, sessionId: entry?.sessionId, systemSent: entry?.systemSent, abortedLastRun: entry?.abortedLastRun, thinkingLevel: entry?.thinkingLevel, verboseLevel: entry?.verboseLevel, reasoningLevel: entry?.reasoningLevel, elevatedLevel: entry?.elevatedLevel, sendPolicy: entry?.sendPolicy, inputTokens: entry?.inputTokens, outputTokens: entry?.outputTokens, totalTokens: total, totalTokensFresh, responseUsage: entry?.responseUsage, modelProvider, model, contextTokens: entry?.contextTokens, deliveryContext: deliveryFields.deliveryContext, lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel, lastTo: deliveryFields.lastTo ?? entry?.lastTo, lastAccountId: deliveryFields.lastAccountId ?? entry?.lastAccountId }; }).toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)); if (search) sessions = sessions.filter((s) => { return [ s.displayName, s.label, s.subject, s.sessionId, s.key ].some((f) => typeof f === "string" && f.toLowerCase().includes(search)); }); if (activeMinutes !== void 0) { const cutoff = now - activeMinutes * 6e4; sessions = sessions.filter((s) => (s.updatedAt ?? 0) >= cutoff); } if (typeof opts.limit === "number" && Number.isFinite(opts.limit)) { const limit = Math.max(1, Math.floor(opts.limit)); sessions = sessions.slice(0, limit); } const finalSessions = sessions.map((s) => { const { entry, ...rest } = s; let derivedTitle; let lastMessagePreview; if (entry?.sessionId) { if (includeDerivedTitles || includeLastMessage) { const parsed = parseAgentSessionKey(s.key); const agentId = parsed && parsed.agentId ? normalizeAgentId(parsed.agentId) : resolveDefaultAgentId(cfg); const fields = readSessionTitleFieldsFromTranscript(entry.sessionId, storePath, entry.sessionFile, agentId); if (includeDerivedTitles) derivedTitle = deriveSessionTitle(entry, fields.firstUserMessage); if (includeLastMessage && fields.lastMessagePreview) lastMessagePreview = fields.lastMessagePreview; } } return { ...rest, derivedTitle, lastMessagePreview }; }); return { ts: now, path: storePath, count: finalSessions.length, defaults: getSessionDefaults(cfg), sessions: finalSessions }; } //#endregion export { loadCombinedSessionStoreForGateway as a, resolveGatewaySessionStoreTarget as c, listSessionsFromStore as i, resolveSessionModelRef as l, classifySessionKey as n, loadSessionEntry as o, listAgentsForGateway as r, pruneLegacyStoreKeys as s, canonicalizeSpawnedByForAgent as t, lookupContextTokens as u };