mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 21:20:43 +00:00
337 lines
10 KiB
TypeScript
337 lines
10 KiB
TypeScript
import type { EffectiveToolInventoryResult } from "../../agents/tools-effective-inventory.types.js";
|
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
|
import { logDebug, logWarn } from "../../logger.js";
|
|
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
|
|
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
|
import { ADMIN_SCOPE } from "../method-scopes.js";
|
|
import {
|
|
ErrorCodes,
|
|
errorShape,
|
|
formatValidationErrors,
|
|
validateToolsEffectiveParams,
|
|
} from "../protocol/index.js";
|
|
import {
|
|
deliveryContextFromSession,
|
|
getActivePluginChannelRegistryVersion,
|
|
getActivePluginRegistryVersion,
|
|
listAgentIds,
|
|
loadSessionEntry,
|
|
resolveEffectiveToolInventory,
|
|
resolveReplyToMode,
|
|
resolveRuntimeConfigCacheKey,
|
|
resolveSessionAgentId,
|
|
resolveSessionModelRef,
|
|
} from "./tools-effective.runtime.js";
|
|
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
|
|
|
const TOOLS_EFFECTIVE_FRESH_TTL_MS = 10_000;
|
|
const TOOLS_EFFECTIVE_STALE_TTL_MS = 120_000;
|
|
const TOOLS_EFFECTIVE_SLOW_LOG_MS = 250;
|
|
const TOOLS_EFFECTIVE_CACHE_LIMIT = 128;
|
|
|
|
let nowForToolsEffectiveCache = () => Date.now();
|
|
|
|
type TrustedToolsEffectiveContext = {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
sessionKey: string;
|
|
senderIsOwner: boolean;
|
|
modelProvider?: string;
|
|
modelId?: string;
|
|
messageProvider?: string;
|
|
accountId?: string;
|
|
currentChannelId?: string;
|
|
currentThreadTs?: string;
|
|
groupId?: string | null;
|
|
groupChannel?: string | null;
|
|
groupSpace?: string | null;
|
|
replyToMode?: "off" | "first" | "all" | "batched";
|
|
};
|
|
|
|
type ToolsEffectiveCacheEntry = {
|
|
value: EffectiveToolInventoryResult;
|
|
createdAtMs: number;
|
|
};
|
|
|
|
const toolsEffectiveCache = new Map<string, ToolsEffectiveCacheEntry>();
|
|
const toolsEffectiveInflight = new Map<string, Promise<EffectiveToolInventoryResult>>();
|
|
|
|
function resolveRequestedAgentIdOrRespondError(params: {
|
|
rawAgentId: unknown;
|
|
cfg: OpenClawConfig;
|
|
respond: RespondFn;
|
|
}) {
|
|
const knownAgents = listAgentIds(params.cfg);
|
|
const requestedAgentId = normalizeOptionalString(params.rawAgentId) ?? "";
|
|
if (!requestedAgentId) {
|
|
return undefined;
|
|
}
|
|
if (!knownAgents.includes(requestedAgentId)) {
|
|
params.respond(
|
|
false,
|
|
undefined,
|
|
errorShape(ErrorCodes.INVALID_REQUEST, `unknown agent id "${requestedAgentId}"`),
|
|
);
|
|
return null;
|
|
}
|
|
return requestedAgentId;
|
|
}
|
|
|
|
function optionalCacheString(value: string | undefined | null): string {
|
|
return value?.trim() ?? "";
|
|
}
|
|
|
|
function buildToolsEffectiveCacheKey(params: {
|
|
sessionKey: string;
|
|
context: TrustedToolsEffectiveContext;
|
|
}): string {
|
|
const context = params.context;
|
|
return JSON.stringify({
|
|
v: 1,
|
|
config: resolveRuntimeConfigCacheKey(context.cfg),
|
|
pluginRegistry: getActivePluginRegistryVersion(),
|
|
channelRegistry: getActivePluginChannelRegistryVersion(),
|
|
sessionKey: params.sessionKey,
|
|
agentId: context.agentId,
|
|
senderIsOwner: context.senderIsOwner,
|
|
modelProvider: optionalCacheString(context.modelProvider),
|
|
modelId: optionalCacheString(context.modelId),
|
|
messageProvider: optionalCacheString(context.messageProvider),
|
|
accountId: optionalCacheString(context.accountId),
|
|
currentChannelId: optionalCacheString(context.currentChannelId),
|
|
currentThreadTs: optionalCacheString(context.currentThreadTs),
|
|
groupId: optionalCacheString(context.groupId),
|
|
groupChannel: optionalCacheString(context.groupChannel),
|
|
groupSpace: optionalCacheString(context.groupSpace),
|
|
replyToMode: optionalCacheString(context.replyToMode),
|
|
});
|
|
}
|
|
|
|
function trimToolsEffectiveCache(): void {
|
|
while (toolsEffectiveCache.size > TOOLS_EFFECTIVE_CACHE_LIMIT) {
|
|
const oldest = toolsEffectiveCache.keys().next().value;
|
|
if (typeof oldest !== "string") {
|
|
return;
|
|
}
|
|
toolsEffectiveCache.delete(oldest);
|
|
}
|
|
}
|
|
|
|
function cacheToolsEffectiveResult(key: string, value: EffectiveToolInventoryResult): void {
|
|
toolsEffectiveCache.delete(key);
|
|
toolsEffectiveCache.set(key, { value, createdAtMs: nowForToolsEffectiveCache() });
|
|
trimToolsEffectiveCache();
|
|
}
|
|
|
|
function scheduleToolsEffectiveRefresh(
|
|
key: string,
|
|
context: TrustedToolsEffectiveContext,
|
|
): Promise<EffectiveToolInventoryResult> {
|
|
const existing = toolsEffectiveInflight.get(key);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
const startedAt = nowForToolsEffectiveCache();
|
|
const task = new Promise<EffectiveToolInventoryResult>((resolve, reject) => {
|
|
setImmediate(() => {
|
|
try {
|
|
const value = resolveEffectiveToolInventory({
|
|
cfg: context.cfg,
|
|
agentId: context.agentId,
|
|
sessionKey: context.sessionKey,
|
|
messageProvider: context.messageProvider,
|
|
modelProvider: context.modelProvider,
|
|
modelId: context.modelId,
|
|
senderIsOwner: context.senderIsOwner,
|
|
currentChannelId: context.currentChannelId,
|
|
currentThreadTs: context.currentThreadTs,
|
|
accountId: context.accountId,
|
|
groupId: context.groupId,
|
|
groupChannel: context.groupChannel,
|
|
groupSpace: context.groupSpace,
|
|
replyToMode: context.replyToMode,
|
|
});
|
|
cacheToolsEffectiveResult(key, value);
|
|
const durationMs = nowForToolsEffectiveCache() - startedAt;
|
|
if (durationMs >= TOOLS_EFFECTIVE_SLOW_LOG_MS) {
|
|
logDebug(
|
|
`tools-effective: refresh durationMs=${durationMs} agent=${context.agentId} session=${context.sessionKey} tools=${value.groups.reduce((sum, group) => sum + group.tools.length, 0)}`,
|
|
);
|
|
}
|
|
resolve(value);
|
|
} catch (err) {
|
|
reject(err);
|
|
} finally {
|
|
toolsEffectiveInflight.delete(key);
|
|
}
|
|
});
|
|
});
|
|
toolsEffectiveInflight.set(key, task);
|
|
return task;
|
|
}
|
|
|
|
function refreshToolsEffectiveInBackground(
|
|
key: string,
|
|
context: TrustedToolsEffectiveContext,
|
|
): void {
|
|
void scheduleToolsEffectiveRefresh(key, context).catch((err) => {
|
|
logWarn(`tools-effective: background refresh failed: ${String(err)}`);
|
|
});
|
|
}
|
|
|
|
async function resolveCachedToolsEffective(params: {
|
|
sessionKey: string;
|
|
context: TrustedToolsEffectiveContext;
|
|
}): Promise<EffectiveToolInventoryResult> {
|
|
const key = buildToolsEffectiveCacheKey(params);
|
|
const now = nowForToolsEffectiveCache();
|
|
const cached = toolsEffectiveCache.get(key);
|
|
if (cached) {
|
|
const ageMs = now - cached.createdAtMs;
|
|
if (ageMs < TOOLS_EFFECTIVE_FRESH_TTL_MS) {
|
|
return cached.value;
|
|
}
|
|
if (ageMs < TOOLS_EFFECTIVE_STALE_TTL_MS) {
|
|
refreshToolsEffectiveInBackground(key, params.context);
|
|
return cached.value;
|
|
}
|
|
}
|
|
return scheduleToolsEffectiveRefresh(key, params.context);
|
|
}
|
|
|
|
function resolveTrustedToolsEffectiveContext(params: {
|
|
sessionKey: string;
|
|
requestedAgentId?: string;
|
|
senderIsOwner: boolean;
|
|
respond: RespondFn;
|
|
}) {
|
|
const loaded = loadSessionEntry(params.sessionKey);
|
|
if (!loaded.entry) {
|
|
params.respond(
|
|
false,
|
|
undefined,
|
|
errorShape(ErrorCodes.INVALID_REQUEST, `unknown session key "${params.sessionKey}"`),
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const sessionAgentId = resolveSessionAgentId({
|
|
sessionKey: loaded.canonicalKey ?? params.sessionKey,
|
|
config: loaded.cfg,
|
|
});
|
|
if (params.requestedAgentId && params.requestedAgentId !== sessionAgentId) {
|
|
params.respond(
|
|
false,
|
|
undefined,
|
|
errorShape(
|
|
ErrorCodes.INVALID_REQUEST,
|
|
`agent id "${params.requestedAgentId}" does not match session agent "${sessionAgentId}"`,
|
|
),
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const delivery = deliveryContextFromSession(loaded.entry);
|
|
const resolvedModel = resolveSessionModelRef(loaded.cfg, loaded.entry, sessionAgentId);
|
|
return {
|
|
cfg: loaded.cfg,
|
|
agentId: sessionAgentId,
|
|
sessionKey: params.sessionKey,
|
|
senderIsOwner: params.senderIsOwner,
|
|
modelProvider: resolvedModel.provider,
|
|
modelId: resolvedModel.model,
|
|
messageProvider:
|
|
delivery?.channel ??
|
|
loaded.entry.lastChannel ??
|
|
loaded.entry.channel ??
|
|
loaded.entry.origin?.provider,
|
|
accountId: delivery?.accountId ?? loaded.entry.lastAccountId ?? loaded.entry.origin?.accountId,
|
|
currentChannelId: delivery?.to,
|
|
currentThreadTs:
|
|
delivery?.threadId != null
|
|
? stringifyRouteThreadId(delivery.threadId)
|
|
: loaded.entry.lastThreadId != null
|
|
? stringifyRouteThreadId(loaded.entry.lastThreadId)
|
|
: loaded.entry.origin?.threadId != null
|
|
? stringifyRouteThreadId(loaded.entry.origin.threadId)
|
|
: undefined,
|
|
groupId: loaded.entry.groupId,
|
|
groupChannel: loaded.entry.groupChannel,
|
|
groupSpace: loaded.entry.space,
|
|
replyToMode: resolveReplyToMode(
|
|
loaded.cfg,
|
|
delivery?.channel ??
|
|
loaded.entry.lastChannel ??
|
|
loaded.entry.channel ??
|
|
loaded.entry.origin?.provider,
|
|
delivery?.accountId ?? loaded.entry.lastAccountId ?? loaded.entry.origin?.accountId,
|
|
loaded.entry.chatType ?? loaded.entry.origin?.chatType,
|
|
),
|
|
};
|
|
}
|
|
|
|
export const toolsEffectiveHandlers: GatewayRequestHandlers = {
|
|
"tools.effective": async ({ params, respond, client, context }) => {
|
|
if (!validateToolsEffectiveParams(params)) {
|
|
respond(
|
|
false,
|
|
undefined,
|
|
errorShape(
|
|
ErrorCodes.INVALID_REQUEST,
|
|
`invalid tools.effective params: ${formatValidationErrors(validateToolsEffectiveParams.errors)}`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
const cfg = context.getRuntimeConfig();
|
|
const requestedAgentId = resolveRequestedAgentIdOrRespondError({
|
|
rawAgentId: params.agentId,
|
|
cfg,
|
|
respond,
|
|
});
|
|
if (requestedAgentId === null) {
|
|
return;
|
|
}
|
|
const trustedContext = resolveTrustedToolsEffectiveContext({
|
|
sessionKey: params.sessionKey,
|
|
requestedAgentId,
|
|
senderIsOwner: Array.isArray(client?.connect?.scopes)
|
|
? client.connect.scopes.includes(ADMIN_SCOPE)
|
|
: false,
|
|
respond,
|
|
});
|
|
if (!trustedContext) {
|
|
return;
|
|
}
|
|
try {
|
|
respond(
|
|
true,
|
|
await resolveCachedToolsEffective({
|
|
sessionKey: params.sessionKey,
|
|
context: trustedContext,
|
|
}),
|
|
undefined,
|
|
);
|
|
} catch (err) {
|
|
respond(
|
|
false,
|
|
undefined,
|
|
errorShape(ErrorCodes.UNAVAILABLE, `tools.effective failed: ${String(err)}`),
|
|
);
|
|
}
|
|
},
|
|
};
|
|
|
|
export const __testing = {
|
|
resetToolsEffectiveCacheForTest() {
|
|
toolsEffectiveCache.clear();
|
|
toolsEffectiveInflight.clear();
|
|
},
|
|
setToolsEffectiveNowForTest(now: () => number) {
|
|
nowForToolsEffectiveCache = now;
|
|
},
|
|
resetToolsEffectiveNowForTest() {
|
|
nowForToolsEffectiveCache = () => Date.now();
|
|
},
|
|
} as const;
|