mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-05-01 06:36:23 +08:00
[codex] fix(ui): guard dreaming wiki plugin calls (#66140)
Merged via squash.
Prepared head SHA: 030562b044
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Outbound/delivery-queue: persist the originating outbound `session` context on queued delivery entries and replay it during recovery, so write-ahead-queued sends keep their original outbound media policy context after restart instead of evaluating against a missing session. (#66025) Thanks @eleqtrizit.
|
||||
- Auto-reply/queue: split collect-mode followup drains into contiguous groups by per-message authorization context (sender id, owner status, exec/bash-elevated overrides), so queued items from different senders or exec configs no longer execute under the last queued run's owner-only and exec-approval context. (#66024) Thanks @eleqtrizit.
|
||||
- Dreaming/memory-core: require a live queued Dreaming cron event before the heartbeat hook runs the sweep, so managed Dreaming no longer replays on later heartbeats after the scheduled run was already consumed. (#66139) Thanks @mbelinky.
|
||||
- Control UI/Dreaming: stop Imported Insights and Memory Palace from calling optional `memory-wiki` gateway methods when the plugin is off, and refresh config before wiki reloads so the Dreaming tab stops showing misleading unknown-method failures. (#66140) Thanks @mbelinky.
|
||||
|
||||
## 2026.4.12
|
||||
|
||||
|
||||
@@ -114,10 +114,11 @@ import {
|
||||
updateSkillEdit,
|
||||
updateSkillEnabled,
|
||||
} from "./controllers/skills.ts";
|
||||
import "./components/dashboard-header.ts";
|
||||
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
|
||||
import "./components/dashboard-header.ts";
|
||||
import { icons } from "./icons.ts";
|
||||
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
|
||||
import { isPluginEnabledInConfigSnapshot } from "./plugin-activation.ts";
|
||||
import { agentLogoUrl } from "./views/agents-utils.ts";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
@@ -236,32 +237,6 @@ function uniquePreserveOrder(values: string[]): string[] {
|
||||
return output;
|
||||
}
|
||||
|
||||
function isPluginExplicitlyEnabled(
|
||||
configSnapshot: AppViewState["configSnapshot"],
|
||||
pluginId: string,
|
||||
): boolean {
|
||||
const config = configSnapshot?.config;
|
||||
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
||||
return true;
|
||||
}
|
||||
const plugins =
|
||||
"plugins" in config && config.plugins && typeof config.plugins === "object"
|
||||
? (config.plugins as Record<string, unknown>)
|
||||
: null;
|
||||
if (plugins?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
const entries =
|
||||
plugins && "entries" in plugins && plugins.entries && typeof plugins.entries === "object"
|
||||
? (plugins.entries as Record<string, unknown>)
|
||||
: null;
|
||||
const entry = entries?.[pluginId];
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
return true;
|
||||
}
|
||||
return (entry as { enabled?: unknown }).enabled !== false;
|
||||
}
|
||||
|
||||
type DismissedUpdateBanner = {
|
||||
latestVersion: string;
|
||||
channel: string | null;
|
||||
@@ -457,12 +432,15 @@ export function renderApp(state: AppViewState) {
|
||||
const dreamingLoading = state.dreamingStatusLoading || state.dreamingModeSaving;
|
||||
const dreamingRefreshLoading = state.dreamingStatusLoading || state.dreamDiaryLoading;
|
||||
const refreshDreaming = () => {
|
||||
void Promise.all([
|
||||
loadDreamingStatus(state),
|
||||
loadDreamDiary(state),
|
||||
loadWikiImportInsights(state),
|
||||
loadWikiMemoryPalace(state),
|
||||
]);
|
||||
void (async () => {
|
||||
await loadConfig(state);
|
||||
await Promise.all([
|
||||
loadDreamingStatus(state),
|
||||
loadDreamDiary(state),
|
||||
loadWikiImportInsights(state),
|
||||
loadWikiMemoryPalace(state),
|
||||
]);
|
||||
})();
|
||||
};
|
||||
const openWikiPage = async (lookup: string) => {
|
||||
if (!state.client || !state.connected) {
|
||||
@@ -2019,7 +1997,11 @@ export function renderApp(state: AppViewState) {
|
||||
dreamDiaryError: state.dreamDiaryError,
|
||||
dreamDiaryPath: state.dreamDiaryPath,
|
||||
dreamDiaryContent: state.dreamDiaryContent,
|
||||
memoryWikiEnabled: isPluginExplicitlyEnabled(state.configSnapshot, "memory-wiki"),
|
||||
memoryWikiEnabled: isPluginEnabledInConfigSnapshot(
|
||||
state.configSnapshot,
|
||||
"memory-wiki",
|
||||
{ enabledByDefault: false },
|
||||
),
|
||||
wikiImportInsightsLoading: state.wikiImportInsightsLoading,
|
||||
wikiImportInsightsError: state.wikiImportInsightsError,
|
||||
wikiImportInsights: state.wikiImportInsights,
|
||||
@@ -2028,8 +2010,18 @@ export function renderApp(state: AppViewState) {
|
||||
wikiMemoryPalace: state.wikiMemoryPalace,
|
||||
onRefresh: refreshDreaming,
|
||||
onRefreshDiary: () => loadDreamDiary(state),
|
||||
onRefreshImports: () => loadWikiImportInsights(state),
|
||||
onRefreshMemoryPalace: () => loadWikiMemoryPalace(state),
|
||||
onRefreshImports: () => {
|
||||
void (async () => {
|
||||
await loadConfig(state);
|
||||
await loadWikiImportInsights(state);
|
||||
})();
|
||||
},
|
||||
onRefreshMemoryPalace: () => {
|
||||
void (async () => {
|
||||
await loadConfig(state);
|
||||
await loadWikiMemoryPalace(state);
|
||||
})();
|
||||
},
|
||||
onOpenConfig: () => openConfigFile(state),
|
||||
onOpenWikiPage: (lookup: string) => openWikiPage(lookup),
|
||||
onBackfillDiary: () => backfillDreamDiary(state),
|
||||
|
||||
@@ -227,6 +227,18 @@ describe("dreaming controller", () => {
|
||||
|
||||
it("loads and normalizes wiki import insights", async () => {
|
||||
const { state, request } = createState();
|
||||
state.configSnapshot = {
|
||||
hash: "hash-1",
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-wiki": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
request.mockResolvedValue({
|
||||
sourceType: "chatgpt",
|
||||
totalItems: 2,
|
||||
@@ -285,8 +297,44 @@ describe("dreaming controller", () => {
|
||||
expect(state.wikiImportInsightsLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("skips wiki import insights when memory-wiki is not enabled", async () => {
|
||||
const { state, request } = createState();
|
||||
state.configSnapshot = {
|
||||
hash: "hash-1",
|
||||
config: {
|
||||
plugins: {},
|
||||
},
|
||||
};
|
||||
state.wikiImportInsights = {
|
||||
sourceType: "chatgpt",
|
||||
totalItems: 1,
|
||||
totalClusters: 1,
|
||||
clusters: [],
|
||||
};
|
||||
state.wikiImportInsightsError = "unknown method: wiki.importInsights";
|
||||
|
||||
await loadWikiImportInsights(state);
|
||||
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expect(state.wikiImportInsights).toBeNull();
|
||||
expect(state.wikiImportInsightsError).toBeNull();
|
||||
expect(state.wikiImportInsightsLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("loads and normalizes the wiki memory palace", async () => {
|
||||
const { state, request } = createState();
|
||||
state.configSnapshot = {
|
||||
hash: "hash-1",
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-wiki": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
request.mockResolvedValue({
|
||||
totalItems: 2,
|
||||
totalClaims: 3,
|
||||
@@ -343,6 +391,31 @@ describe("dreaming controller", () => {
|
||||
expect(state.wikiMemoryPalaceLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("skips wiki memory palace when memory-wiki is not enabled", async () => {
|
||||
const { state, request } = createState();
|
||||
state.configSnapshot = {
|
||||
hash: "hash-1",
|
||||
config: {
|
||||
plugins: {},
|
||||
},
|
||||
};
|
||||
state.wikiMemoryPalace = {
|
||||
totalItems: 1,
|
||||
totalClaims: 1,
|
||||
totalQuestions: 0,
|
||||
totalContradictions: 0,
|
||||
clusters: [],
|
||||
};
|
||||
state.wikiMemoryPalaceError = "unknown method: wiki.palace";
|
||||
|
||||
await loadWikiMemoryPalace(state);
|
||||
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expect(state.wikiMemoryPalace).toBeNull();
|
||||
expect(state.wikiMemoryPalaceError).toBeNull();
|
||||
expect(state.wikiMemoryPalaceLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("patches config to update global dreaming enablement", async () => {
|
||||
const { state, request } = createState();
|
||||
state.configSnapshot = {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { GatewayBrowserClient } from "../gateway.ts";
|
||||
import { isPluginEnabledInConfigSnapshot } from "../plugin-activation.ts";
|
||||
import type { ConfigSnapshot } from "../types.ts";
|
||||
|
||||
export type DreamingPhaseId = "light" | "deep" | "rem";
|
||||
const DEFAULT_DREAM_DIARY_PATH = "DREAMS.md";
|
||||
const DEFAULT_DREAMING_PLUGIN_ID = "memory-core";
|
||||
const MEMORY_WIKI_PLUGIN_ID = "memory-wiki";
|
||||
|
||||
type DreamingPhaseStatusBase = {
|
||||
enabled: boolean;
|
||||
@@ -228,6 +230,12 @@ function confirmDreamingAction(message: string): boolean {
|
||||
return globalThis.confirm(message);
|
||||
}
|
||||
|
||||
function isMemoryWikiEnabled(state: DreamingState): boolean {
|
||||
return isPluginEnabledInConfigSnapshot(state.configSnapshot, MEMORY_WIKI_PLUGIN_ID, {
|
||||
enabledByDefault: false,
|
||||
});
|
||||
}
|
||||
|
||||
function buildDreamDiaryActionSuccessMessage(
|
||||
method:
|
||||
| "doctor.memory.backfillDreamDiary"
|
||||
@@ -740,6 +748,11 @@ export async function loadWikiImportInsights(state: DreamingState): Promise<void
|
||||
if (!state.client || !state.connected || state.wikiImportInsightsLoading) {
|
||||
return;
|
||||
}
|
||||
if (!isMemoryWikiEnabled(state)) {
|
||||
state.wikiImportInsights = null;
|
||||
state.wikiImportInsightsError = null;
|
||||
return;
|
||||
}
|
||||
state.wikiImportInsightsLoading = true;
|
||||
state.wikiImportInsightsError = null;
|
||||
try {
|
||||
@@ -759,6 +772,11 @@ export async function loadWikiMemoryPalace(state: DreamingState): Promise<void>
|
||||
if (!state.client || !state.connected || state.wikiMemoryPalaceLoading) {
|
||||
return;
|
||||
}
|
||||
if (!isMemoryWikiEnabled(state)) {
|
||||
state.wikiMemoryPalace = null;
|
||||
state.wikiMemoryPalaceError = null;
|
||||
return;
|
||||
}
|
||||
state.wikiMemoryPalaceLoading = true;
|
||||
state.wikiMemoryPalaceError = null;
|
||||
try {
|
||||
|
||||
80
ui/src/ui/plugin-activation.test.ts
Normal file
80
ui/src/ui/plugin-activation.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isPluginEnabledInConfigSnapshot } from "./plugin-activation.ts";
|
||||
|
||||
describe("isPluginEnabledInConfigSnapshot", () => {
|
||||
it("stays permissive when config has not loaded yet", () => {
|
||||
expect(
|
||||
isPluginEnabledInConfigSnapshot({ hash: "hash-1" }, "memory-wiki", {
|
||||
enabledByDefault: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("treats bundled default-off plugins as disabled when config is present but silent", () => {
|
||||
expect(
|
||||
isPluginEnabledInConfigSnapshot(
|
||||
{
|
||||
hash: "hash-1",
|
||||
config: {
|
||||
plugins: {},
|
||||
},
|
||||
},
|
||||
"memory-wiki",
|
||||
{
|
||||
enabledByDefault: false,
|
||||
},
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when the plugin is explicitly enabled", () => {
|
||||
expect(
|
||||
isPluginEnabledInConfigSnapshot(
|
||||
{
|
||||
hash: "hash-1",
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-wiki": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"memory-wiki",
|
||||
{ enabledByDefault: false },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when plugins.allow excludes the plugin", () => {
|
||||
expect(
|
||||
isPluginEnabledInConfigSnapshot(
|
||||
{
|
||||
hash: "hash-1",
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["memory-core"],
|
||||
entries: {
|
||||
"memory-wiki": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"memory-wiki",
|
||||
{ enabledByDefault: false },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps default-on plugins enabled when config is silent", () => {
|
||||
expect(
|
||||
isPluginEnabledInConfigSnapshot({ hash: "hash-1" }, "browser", {
|
||||
enabledByDefault: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
53
ui/src/ui/plugin-activation.ts
Normal file
53
ui/src/ui/plugin-activation.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { ConfigSnapshot } from "./types.ts";
|
||||
|
||||
type PluginActivationOptions = {
|
||||
enabledByDefault?: boolean;
|
||||
};
|
||||
|
||||
export function isPluginEnabledInConfigSnapshot(
|
||||
configSnapshot: ConfigSnapshot | null | undefined,
|
||||
pluginId: string,
|
||||
options?: PluginActivationOptions,
|
||||
): boolean {
|
||||
const enabledByDefault = options?.enabledByDefault ?? true;
|
||||
const config = configSnapshot?.config;
|
||||
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const plugins =
|
||||
"plugins" in config && config.plugins && typeof config.plugins === "object"
|
||||
? (config.plugins as Record<string, unknown>)
|
||||
: null;
|
||||
if (plugins?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const deny =
|
||||
Array.isArray(plugins?.deny) && plugins.deny.every((entry) => typeof entry === "string")
|
||||
? plugins.deny
|
||||
: [];
|
||||
if (deny.includes(pluginId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allow =
|
||||
Array.isArray(plugins?.allow) && plugins.allow.every((entry) => typeof entry === "string")
|
||||
? plugins.allow
|
||||
: [];
|
||||
if (allow.length > 0 && !allow.includes(pluginId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entries =
|
||||
plugins && "entries" in plugins && plugins.entries && typeof plugins.entries === "object"
|
||||
? (plugins.entries as Record<string, unknown>)
|
||||
: null;
|
||||
const entry = entries?.[pluginId];
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
return enabledByDefault;
|
||||
}
|
||||
|
||||
const enabled = (entry as { enabled?: unknown }).enabled;
|
||||
return typeof enabled === "boolean" ? enabled : enabledByDefault;
|
||||
}
|
||||
Reference in New Issue
Block a user