mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 14:13:15 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
415
openclaw/test/setup-openclaw-runtime.ts
Normal file
415
openclaw/test/setup-openclaw-runtime.ts
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
import { afterAll, afterEach, beforeAll } from "vitest";
|
||||
import type {
|
||||
ChannelId,
|
||||
ChannelOutboundAdapter,
|
||||
ChannelPlugin,
|
||||
} from "../src/channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../src/config/config.js";
|
||||
import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js";
|
||||
import type { PluginRegistry } from "../src/plugins/registry.js";
|
||||
import { installSharedTestSetup } from "./setup.shared.js";
|
||||
|
||||
installSharedTestSetup();
|
||||
|
||||
const WORKER_RUNTIME_STATE = Symbol.for("openclaw.testSetupRuntimeState");
|
||||
const WORKER_RUNTIME_HELPERS = Symbol.for("openclaw.testSetupRuntimeHelpers");
|
||||
type WorkerRuntimeState = {
|
||||
defaultPluginRegistry: PluginRegistry | null;
|
||||
materializedDefaultPluginRegistry: PluginRegistry | null;
|
||||
};
|
||||
type WorkerRuntimeHelpers = {
|
||||
clearPluginDiscoveryCache: typeof import("../src/plugins/discovery.js").clearPluginDiscoveryCache;
|
||||
clearPluginManifestRegistryCache: typeof import("../src/plugins/manifest-registry-state.js").clearPluginManifestRegistryCache;
|
||||
clearSessionStoreCaches: typeof import("../src/config/sessions/store-cache.js").clearSessionStoreCaches;
|
||||
drainFileLockStateForTest: typeof import("../src/infra/file-lock.js").drainFileLockStateForTest;
|
||||
drainSessionStoreLockQueuesForTest: typeof import("../src/config/sessions/store-lock-state.js").drainSessionStoreLockQueuesForTest;
|
||||
drainSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").drainSessionWriteLockStateForTest;
|
||||
resetContextWindowCacheForTest: typeof import("../src/agents/context-runtime-state.js").resetContextWindowCacheForTest;
|
||||
resetFileLockStateForTest: typeof import("../src/infra/file-lock.js").resetFileLockStateForTest;
|
||||
resetModelsJsonReadyCacheForTest: typeof import("../src/agents/models-config-state.js").resetModelsJsonReadyCacheForTest;
|
||||
resetPluginRuntimeStateForTest: typeof import("../src/plugins/runtime.js").resetPluginRuntimeStateForTest;
|
||||
resetSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").resetSessionWriteLockStateForTest;
|
||||
setActivePluginRegistry: typeof import("../src/plugins/runtime.js").setActivePluginRegistry;
|
||||
};
|
||||
|
||||
type ReplyToModeResolver = NonNullable<
|
||||
NonNullable<ChannelPlugin["threading"]>["resolveReplyToMode"]
|
||||
>;
|
||||
|
||||
const workerRuntimeState = (() => {
|
||||
const globalState = globalThis as typeof globalThis & {
|
||||
[WORKER_RUNTIME_STATE]?: WorkerRuntimeState;
|
||||
};
|
||||
if (!globalState[WORKER_RUNTIME_STATE]) {
|
||||
globalState[WORKER_RUNTIME_STATE] = {
|
||||
defaultPluginRegistry: null,
|
||||
materializedDefaultPluginRegistry: null,
|
||||
};
|
||||
}
|
||||
return globalState[WORKER_RUNTIME_STATE];
|
||||
})();
|
||||
|
||||
function loadWorkerRuntimeHelpers(): Promise<WorkerRuntimeHelpers> {
|
||||
const globalState = globalThis as typeof globalThis & {
|
||||
[WORKER_RUNTIME_HELPERS]?: Promise<WorkerRuntimeHelpers>;
|
||||
};
|
||||
globalState[WORKER_RUNTIME_HELPERS] ??= Promise.all([
|
||||
import("../src/agents/context-runtime-state.js"),
|
||||
import("../src/agents/models-config-state.js"),
|
||||
import("../src/agents/session-write-lock.js"),
|
||||
import("../src/config/sessions/store-cache.js"),
|
||||
import("../src/config/sessions/store-lock-state.js"),
|
||||
import("../src/infra/file-lock.js"),
|
||||
import("../src/plugins/discovery.js"),
|
||||
import("../src/plugins/manifest-registry-state.js"),
|
||||
import("../src/plugins/runtime.js"),
|
||||
]).then(
|
||||
([
|
||||
contextRuntimeState,
|
||||
modelsConfigState,
|
||||
sessionWriteLock,
|
||||
sessionStoreCache,
|
||||
sessionStoreLockState,
|
||||
fileLock,
|
||||
discovery,
|
||||
manifestRegistryState,
|
||||
pluginRuntime,
|
||||
]) => ({
|
||||
clearPluginDiscoveryCache: discovery.clearPluginDiscoveryCache,
|
||||
clearPluginManifestRegistryCache: manifestRegistryState.clearPluginManifestRegistryCache,
|
||||
clearSessionStoreCaches: sessionStoreCache.clearSessionStoreCaches,
|
||||
drainFileLockStateForTest: fileLock.drainFileLockStateForTest,
|
||||
drainSessionStoreLockQueuesForTest: sessionStoreLockState.drainSessionStoreLockQueuesForTest,
|
||||
drainSessionWriteLockStateForTest: sessionWriteLock.drainSessionWriteLockStateForTest,
|
||||
resetContextWindowCacheForTest: contextRuntimeState.resetContextWindowCacheForTest,
|
||||
resetFileLockStateForTest: fileLock.resetFileLockStateForTest,
|
||||
resetModelsJsonReadyCacheForTest: modelsConfigState.resetModelsJsonReadyCacheForTest,
|
||||
resetPluginRuntimeStateForTest: pluginRuntime.resetPluginRuntimeStateForTest,
|
||||
resetSessionWriteLockStateForTest: sessionWriteLock.resetSessionWriteLockStateForTest,
|
||||
setActivePluginRegistry: pluginRuntime.setActivePluginRegistry,
|
||||
}),
|
||||
);
|
||||
return globalState[WORKER_RUNTIME_HELPERS];
|
||||
}
|
||||
|
||||
const pickSendFn = (id: ChannelId, deps?: OutboundSendDeps) => {
|
||||
return deps?.[id] as ((...args: unknown[]) => Promise<unknown>) | undefined;
|
||||
};
|
||||
|
||||
function createTopLevelChannelReplyToModeResolverForTest(channelId: string): ReplyToModeResolver {
|
||||
return ({ cfg }) => {
|
||||
const channelConfig = (
|
||||
cfg.channels as Record<string, { replyToMode?: "off" | "first" | "all" }> | undefined
|
||||
)?.[channelId];
|
||||
return channelConfig?.replyToMode ?? "off";
|
||||
};
|
||||
}
|
||||
|
||||
function createTestRegistryForSetup(
|
||||
channels: Array<{ pluginId: string; plugin: ChannelPlugin; source: string }> = [],
|
||||
): PluginRegistry {
|
||||
return {
|
||||
plugins: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: channels as unknown as PluginRegistry["channels"],
|
||||
channelSetups: channels.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
plugin: entry.plugin,
|
||||
source: entry.source,
|
||||
enabled: true,
|
||||
})),
|
||||
providers: [],
|
||||
speechProviders: [],
|
||||
realtimeTranscriptionProviders: [],
|
||||
realtimeVoiceProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
videoGenerationProviders: [],
|
||||
webFetchProviders: [],
|
||||
webSearchProviders: [],
|
||||
memoryEmbeddingProviders: [],
|
||||
gatewayHandlers: {},
|
||||
gatewayMethodScopes: {},
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
reloads: [],
|
||||
nodeHostCommands: [],
|
||||
securityAuditCollectors: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
conversationBindingResolvedHandlers: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSlackStubReplyToMode(params: {
|
||||
cfg: OpenClawConfig;
|
||||
chatType?: string | null;
|
||||
}): "off" | "first" | "all" {
|
||||
const entry = (
|
||||
params.cfg.channels as
|
||||
| Record<
|
||||
string,
|
||||
{
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
replyToModeByChatType?: Partial<
|
||||
Record<"direct" | "group" | "channel", "off" | "first" | "all">
|
||||
>;
|
||||
dm?: { replyToMode?: "off" | "first" | "all" };
|
||||
}
|
||||
>
|
||||
| undefined
|
||||
)?.slack;
|
||||
const normalizedChatType = params.chatType?.trim().toLowerCase();
|
||||
if (
|
||||
normalizedChatType === "direct" ||
|
||||
normalizedChatType === "group" ||
|
||||
normalizedChatType === "channel"
|
||||
) {
|
||||
const byChatType = entry?.replyToModeByChatType?.[normalizedChatType];
|
||||
if (byChatType) {
|
||||
return byChatType;
|
||||
}
|
||||
if (normalizedChatType === "direct" && entry?.dm?.replyToMode) {
|
||||
return entry.dm.replyToMode;
|
||||
}
|
||||
}
|
||||
return entry?.replyToMode ?? "off";
|
||||
}
|
||||
|
||||
const createStubOutbound = (
|
||||
id: ChannelId,
|
||||
deliveryMode: ChannelOutboundAdapter["deliveryMode"] = "direct",
|
||||
): ChannelOutboundAdapter => ({
|
||||
deliveryMode,
|
||||
sendText: async ({ deps, to, text }) => {
|
||||
const send = pickSendFn(id, deps);
|
||||
if (send) {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const result = (await send(to, text, { verbose: false } as any)) as {
|
||||
messageId: string;
|
||||
};
|
||||
return { channel: id, ...result };
|
||||
}
|
||||
return { channel: id, messageId: "test" };
|
||||
},
|
||||
sendMedia: async ({ deps, to, text, mediaUrl }) => {
|
||||
const send = pickSendFn(id, deps);
|
||||
if (send) {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const result = (await send(to, text, { verbose: false, mediaUrl } as any)) as {
|
||||
messageId: string;
|
||||
};
|
||||
return { channel: id, ...result };
|
||||
}
|
||||
return { channel: id, messageId: "test" };
|
||||
},
|
||||
});
|
||||
|
||||
const createStubPlugin = (params: {
|
||||
id: ChannelId;
|
||||
label?: string;
|
||||
aliases?: string[];
|
||||
deliveryMode?: ChannelOutboundAdapter["deliveryMode"];
|
||||
preferSessionLookupForAnnounceTarget?: boolean;
|
||||
resolveReplyToMode?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
chatType?: string | null;
|
||||
}) => "off" | "first" | "all";
|
||||
}): ChannelPlugin => ({
|
||||
id: params.id,
|
||||
meta: {
|
||||
id: params.id,
|
||||
label: params.label ?? String(params.id),
|
||||
selectionLabel: params.label ?? String(params.id),
|
||||
docsPath: `/channels/${params.id}`,
|
||||
blurb: "test stub.",
|
||||
aliases: params.aliases,
|
||||
preferSessionLookupForAnnounceTarget: params.preferSessionLookupForAnnounceTarget,
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "group"] },
|
||||
threading: params.resolveReplyToMode
|
||||
? {
|
||||
resolveReplyToMode: params.resolveReplyToMode,
|
||||
}
|
||||
: undefined,
|
||||
config: {
|
||||
listAccountIds: (cfg: OpenClawConfig) => {
|
||||
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||
const entry = channels?.[params.id];
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return [];
|
||||
}
|
||||
const accounts = (entry as { accounts?: Record<string, unknown> }).accounts;
|
||||
const ids = accounts ? Object.keys(accounts).filter(Boolean) : [];
|
||||
return ids.length > 0 ? ids : ["default"];
|
||||
},
|
||||
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => {
|
||||
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||
const entry = channels?.[params.id];
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return {};
|
||||
}
|
||||
const accounts = (entry as { accounts?: Record<string, unknown> }).accounts;
|
||||
const match = accountId ? accounts?.[accountId] : undefined;
|
||||
return (match && typeof match === "object") || typeof match === "string" ? match : entry;
|
||||
},
|
||||
isConfigured: async (_account, cfg: OpenClawConfig) => {
|
||||
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||
return Boolean(channels?.[params.id]);
|
||||
},
|
||||
},
|
||||
outbound: createStubOutbound(params.id, params.deliveryMode),
|
||||
});
|
||||
|
||||
const createDefaultRegistry = () =>
|
||||
createTestRegistryForSetup([
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: createStubPlugin({
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
resolveReplyToMode: createTopLevelChannelReplyToModeResolverForTest("discord"),
|
||||
}),
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "slack",
|
||||
plugin: createStubPlugin({
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
resolveReplyToMode: ({ cfg, chatType }) => resolveSlackStubReplyToMode({ cfg, chatType }),
|
||||
}),
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "telegram",
|
||||
plugin: {
|
||||
...createStubPlugin({
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
resolveReplyToMode: createTopLevelChannelReplyToModeResolverForTest("telegram"),
|
||||
}),
|
||||
status: {
|
||||
buildChannelSummary: async () => ({
|
||||
configured: false,
|
||||
tokenSource: process.env.TELEGRAM_BOT_TOKEN ? "env" : "none",
|
||||
}),
|
||||
},
|
||||
},
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
plugin: createStubPlugin({
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
deliveryMode: "gateway",
|
||||
preferSessionLookupForAnnounceTarget: true,
|
||||
}),
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "signal",
|
||||
plugin: createStubPlugin({ id: "signal", label: "Signal" }),
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "imessage",
|
||||
plugin: createStubPlugin({ id: "imessage", label: "iMessage", aliases: ["imsg"] }),
|
||||
source: "test",
|
||||
},
|
||||
]);
|
||||
|
||||
function getDefaultPluginRegistry(): PluginRegistry {
|
||||
workerRuntimeState.materializedDefaultPluginRegistry ??= createDefaultRegistry();
|
||||
return workerRuntimeState.materializedDefaultPluginRegistry;
|
||||
}
|
||||
|
||||
function resolveDefaultPluginRegistryProxy(): PluginRegistry {
|
||||
workerRuntimeState.defaultPluginRegistry ??= new Proxy({} as PluginRegistry, {
|
||||
defineProperty(_target, property, attributes) {
|
||||
return Reflect.defineProperty(getDefaultPluginRegistry() as object, property, attributes);
|
||||
},
|
||||
deleteProperty(_target, property) {
|
||||
return Reflect.deleteProperty(getDefaultPluginRegistry() as object, property);
|
||||
},
|
||||
get(_target, property, receiver) {
|
||||
return Reflect.get(getDefaultPluginRegistry() as object, property, receiver);
|
||||
},
|
||||
getOwnPropertyDescriptor(_target, property) {
|
||||
return Reflect.getOwnPropertyDescriptor(getDefaultPluginRegistry() as object, property);
|
||||
},
|
||||
has(_target, property) {
|
||||
return Reflect.has(getDefaultPluginRegistry() as object, property);
|
||||
},
|
||||
ownKeys() {
|
||||
return Reflect.ownKeys(getDefaultPluginRegistry() as object);
|
||||
},
|
||||
set(_target, property, value, receiver) {
|
||||
return Reflect.set(getDefaultPluginRegistry() as object, property, value, receiver);
|
||||
},
|
||||
});
|
||||
return workerRuntimeState.defaultPluginRegistry;
|
||||
}
|
||||
|
||||
async function installDefaultPluginRegistry(): Promise<void> {
|
||||
const { resetPluginRuntimeStateForTest, setActivePluginRegistry } =
|
||||
await loadWorkerRuntimeHelpers();
|
||||
workerRuntimeState.materializedDefaultPluginRegistry = null;
|
||||
resetPluginRuntimeStateForTest();
|
||||
setActivePluginRegistry(resolveDefaultPluginRegistryProxy());
|
||||
}
|
||||
|
||||
// Some suites import channel/plugin consumers at module top level, before
|
||||
// Vitest runs hooks. Seed the lazy registry during setup module evaluation so
|
||||
// import-time lookups still see the default test registry.
|
||||
await installDefaultPluginRegistry();
|
||||
|
||||
beforeAll(async () => {
|
||||
await installDefaultPluginRegistry();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const {
|
||||
clearPluginDiscoveryCache,
|
||||
clearPluginManifestRegistryCache,
|
||||
clearSessionStoreCaches,
|
||||
drainFileLockStateForTest,
|
||||
drainSessionStoreLockQueuesForTest,
|
||||
drainSessionWriteLockStateForTest,
|
||||
resetContextWindowCacheForTest,
|
||||
resetFileLockStateForTest,
|
||||
resetModelsJsonReadyCacheForTest,
|
||||
resetSessionWriteLockStateForTest,
|
||||
} = await loadWorkerRuntimeHelpers();
|
||||
await drainSessionStoreLockQueuesForTest();
|
||||
clearSessionStoreCaches();
|
||||
await drainFileLockStateForTest();
|
||||
await drainSessionWriteLockStateForTest();
|
||||
resetFileLockStateForTest();
|
||||
resetContextWindowCacheForTest();
|
||||
resetModelsJsonReadyCacheForTest();
|
||||
resetSessionWriteLockStateForTest();
|
||||
clearPluginDiscoveryCache();
|
||||
clearPluginManifestRegistryCache();
|
||||
await installDefaultPluginRegistry();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const {
|
||||
clearPluginDiscoveryCache,
|
||||
clearPluginManifestRegistryCache,
|
||||
clearSessionStoreCaches,
|
||||
drainFileLockStateForTest,
|
||||
drainSessionWriteLockStateForTest,
|
||||
} = await loadWorkerRuntimeHelpers();
|
||||
clearSessionStoreCaches();
|
||||
await drainFileLockStateForTest();
|
||||
await drainSessionWriteLockStateForTest();
|
||||
clearPluginDiscoveryCache();
|
||||
clearPluginManifestRegistryCache();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue