mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-09 00:01:17 +00:00
fix: remove bundled channel startup reentry
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
f9276d60f9fed5813979adf2c91a9145a3fb4101c17ae214439cd47fc1cbd2ae plugin-sdk-api-baseline.json
|
||||
ee74172a0a8685cec1847095589ada102fac7877e92afe6b19206f88805fa62d plugin-sdk-api-baseline.jsonl
|
||||
8c38d3e2d0a61c02db70070e0d032b54b1de474000e1c1221332efc495e8681d plugin-sdk-api-baseline.json
|
||||
d057310712f83b27f64b53dbe45ef2e92795407e56503a255de0f29d915c1ee4 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
createPatchedAccountSetupAdapter,
|
||||
createSetupInputPresenceValidator,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
|
||||
const channel = "googlechat" as const;
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
setSetupChannelEnabled,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import type {
|
||||
ChannelSetupAdapter,
|
||||
ChannelSetupWizard,
|
||||
ChannelSetupWizardTextInput,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
|
||||
@@ -10,8 +10,7 @@ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-runtime";
|
||||
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { chunkText } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { chunkText, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
|
||||
import { buildOutboundBaseSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
buildBaseChannelStatusSummary,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createActionGate } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { createActionGate } from "openclaw/plugin-sdk/channel-actions";
|
||||
import type { ChannelMessageActionName } from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { ChannelToolSend } from "openclaw/plugin-sdk/tool-send";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input-runtime";
|
||||
import {
|
||||
createAllowlistSetupWizardProxy,
|
||||
createAccountScopedAllowFromSection,
|
||||
@@ -6,18 +7,17 @@ import {
|
||||
createStandardChannelSetupStatus,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
createEnvPatchedAccountSetupAdapter,
|
||||
hasConfiguredSecretInput,
|
||||
type OpenClawConfig,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
setSetupChannelEnabled,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import {
|
||||
type ChannelSetupAdapter,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type ChannelSetupWizardAllowFromEntry,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { inspectSlackAccount } from "./account-inspect.js";
|
||||
import { listSlackAccountIds, resolveSlackAccount, type ResolvedSlackAccount } from "./accounts.js";
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
parseMentionOrPrefixedId,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import type {
|
||||
ChannelSetupWizard,
|
||||
ChannelSetupWizardAllowFromEntry,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import {
|
||||
resolveDefaultSlackAccountId,
|
||||
|
||||
@@ -4,11 +4,9 @@ import {
|
||||
adaptScopedAccountAccessor,
|
||||
createScopedChannelConfigAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
formatDocsLink,
|
||||
hasConfiguredSecretInput,
|
||||
patchChannelConfigForAccount,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input-runtime";
|
||||
import { patchChannelConfigForAccount } from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { inspectSlackAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listSlackAccountIds,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Implements the ChannelPlugin interface following the LINE pattern.
|
||||
*/
|
||||
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/account-resolution";
|
||||
import {
|
||||
createHybridChannelConfigAdapter,
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
|
||||
import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
||||
import { listAccountIds, resolveAccount } from "./accounts.js";
|
||||
import { synologyChatApprovalAuth } from "./approval-auth.js";
|
||||
import { sendMessage, sendFileUrl } from "./client.js";
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
splitSetupEntries,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type { ChannelSetupAdapter, ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import type { ChannelSetupAdapter, ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatCliCommand, formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { resolveDefaultTelegramAccountId, resolveTelegramAccount } from "./accounts.js";
|
||||
import { lookupTelegramChatId } from "./api-fetch.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { chunkText } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { chunkText } from "openclaw/plugin-sdk/reply-chunking";
|
||||
import { createWhatsAppOutboundBase } from "./outbound-base.js";
|
||||
import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js";
|
||||
import { getWhatsAppRuntime } from "./runtime.js";
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createDelegatedSetupWizardProxy,
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import {
|
||||
listWhatsAppAccountIds,
|
||||
resolveDefaultWhatsAppAccountId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup";
|
||||
import { createPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup-runtime";
|
||||
|
||||
const channel = "zalouser" as const;
|
||||
|
||||
|
||||
@@ -10,13 +10,16 @@ const state = vi.hoisted(() => ({
|
||||
loadSessionStoreMock: vi.fn(),
|
||||
resolveStorePathMock: vi.fn(),
|
||||
updateSessionStoreMock: vi.fn(),
|
||||
piEmbeddedModuleImported: false,
|
||||
}));
|
||||
|
||||
vi.mock("./pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: (...args: unknown[]) => state.abortEmbeddedPiRunMock(...args),
|
||||
}));
|
||||
vi.mock("./pi-embedded.js", () => {
|
||||
state.piEmbeddedModuleImported = true;
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("./pi-embedded-runner/runs.js", () => ({
|
||||
abortEmbeddedPiRun: (...args: unknown[]) => state.abortEmbeddedPiRunMock(...args),
|
||||
requestEmbeddedRunModelSwitch: (...args: unknown[]) =>
|
||||
state.requestEmbeddedRunModelSwitchMock(...args),
|
||||
consumeEmbeddedRunModelSwitch: (...args: unknown[]) =>
|
||||
@@ -56,6 +59,7 @@ describe("live model switch", () => {
|
||||
state.abortEmbeddedPiRunMock.mockReset().mockReturnValue(false);
|
||||
state.requestEmbeddedRunModelSwitchMock.mockReset();
|
||||
state.consumeEmbeddedRunModelSwitchMock.mockReset();
|
||||
state.piEmbeddedModuleImported = false;
|
||||
state.resolveDefaultModelForAgentMock
|
||||
.mockReset()
|
||||
.mockReturnValue({ provider: "anthropic", model: "claude-opus-4-6" });
|
||||
@@ -247,6 +251,12 @@ describe("live model switch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not import the broad pi-embedded barrel on module load", async () => {
|
||||
await loadModule();
|
||||
|
||||
expect(state.piEmbeddedModuleImported).toBe(false);
|
||||
});
|
||||
|
||||
it("treats auth-profile-source changes as no-op when no auth profile is selected", async () => {
|
||||
const { hasDifferentLiveSessionModelSelection } = await loadModule();
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import { LiveSessionModelSwitchError } from "./live-model-switch-error.js";
|
||||
import { resolveDefaultModelForAgent, resolvePersistedModelRef } from "./model-selection.js";
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
consumeEmbeddedRunModelSwitch,
|
||||
requestEmbeddedRunModelSwitch,
|
||||
type EmbeddedRunModelSwitchRequest,
|
||||
} from "./pi-embedded-runner/runs.js";
|
||||
import { abortEmbeddedPiRun } from "./pi-embedded.js";
|
||||
export { LiveSessionModelSwitchError } from "./live-model-switch-error.js";
|
||||
export type LiveSessionModelSelection = EmbeddedRunModelSwitchRequest;
|
||||
|
||||
|
||||
@@ -279,7 +279,6 @@ function getBundledChannelState(): BundledChannelState {
|
||||
if (cachedBundledChannelState) {
|
||||
return cachedBundledChannelState;
|
||||
}
|
||||
|
||||
const entries = loadGeneratedBundledChannelEntries();
|
||||
const plugins = entries.map(({ entry }) => entry.channelPlugin);
|
||||
const setupPlugins = entries.flatMap(({ setupEntry }) => {
|
||||
|
||||
34
src/channels/plugins/setup-helpers.import-safety.test.ts
Normal file
34
src/channels/plugins/setup-helpers.import-safety.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { importFreshModule } from "../../../test/helpers/import-fresh.ts";
|
||||
import { clearSetupPromotionRuntimeModuleCache } from "./setup-helpers.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock("../../plugins/discovery.js");
|
||||
clearSetupPromotionRuntimeModuleCache();
|
||||
});
|
||||
|
||||
describe("setup helper import safety", () => {
|
||||
it("does not load contract-surface discovery on module import", async () => {
|
||||
const state = {
|
||||
discoveryLoaded: false,
|
||||
};
|
||||
|
||||
vi.doMock("../../plugins/discovery.js", () => {
|
||||
state.discoveryLoaded = true;
|
||||
throw new Error("contract surface discovery should stay lazy on import");
|
||||
});
|
||||
|
||||
const helpers = await importFreshModule<typeof import("./setup-helpers.js")>(
|
||||
import.meta.url,
|
||||
"./setup-helpers.js?scope=import-safety",
|
||||
);
|
||||
|
||||
expect(state.discoveryLoaded).toBe(false);
|
||||
expect(
|
||||
helpers.createPatchedAccountSetupAdapter({
|
||||
channelKey: "demo-setup",
|
||||
buildPatch: () => ({}),
|
||||
}),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import {
|
||||
applySetupAccountConfigPatch,
|
||||
clearSetupPromotionRuntimeModuleCache,
|
||||
createEnvPatchedAccountSetupAdapter,
|
||||
createPatchedAccountSetupAdapter,
|
||||
moveSingleAccountChannelSectionToDefaultAccount,
|
||||
@@ -13,6 +14,10 @@ function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearSetupPromotionRuntimeModuleCache();
|
||||
});
|
||||
|
||||
describe("applySetupAccountConfigPatch", () => {
|
||||
it("patches top-level config for default account and enables channel", () => {
|
||||
const next = applySetupAccountConfigPatch({
|
||||
|
||||
@@ -1,10 +1,53 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import { z, type ZodType } from "zod";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { getBundledChannelContractSurfaceEntries } from "./contract-surfaces.js";
|
||||
import type { ChannelSetupAdapter } from "./types.adapters.js";
|
||||
import type { ChannelSetupInput } from "./types.core.js";
|
||||
|
||||
const SETUP_PROMOTION_SURFACE_MODULE_BASENAMES = [
|
||||
"contract-surfaces.ts",
|
||||
"contract-surfaces.js",
|
||||
] as const;
|
||||
|
||||
type SetupPromotionRuntimeModule = Pick<
|
||||
typeof import("./contract-surfaces.js"),
|
||||
"getBundledChannelContractSurfaceEntries"
|
||||
>;
|
||||
|
||||
let cachedSetupPromotionRuntimeModule: SetupPromotionRuntimeModule | null = null;
|
||||
|
||||
export function clearSetupPromotionRuntimeModuleCache(): void {
|
||||
cachedSetupPromotionRuntimeModule = null;
|
||||
}
|
||||
|
||||
function resolveSetupPromotionRuntimeModulePath(): string {
|
||||
for (const basename of SETUP_PROMOTION_SURFACE_MODULE_BASENAMES) {
|
||||
const candidatePath = fileURLToPath(new URL(basename, import.meta.url));
|
||||
const resolvedPath = candidatePath.replace(
|
||||
`${path.sep}dist-runtime${path.sep}`,
|
||||
`${path.sep}dist${path.sep}`,
|
||||
);
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
return resolvedPath;
|
||||
}
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
throw new Error("missing setup promotion runtime module");
|
||||
}
|
||||
|
||||
function loadSetupPromotionRuntimeModule(): SetupPromotionRuntimeModule {
|
||||
cachedSetupPromotionRuntimeModule ??= createJiti(import.meta.url)(
|
||||
resolveSetupPromotionRuntimeModulePath(),
|
||||
) as SetupPromotionRuntimeModule;
|
||||
return cachedSetupPromotionRuntimeModule;
|
||||
}
|
||||
|
||||
type ChannelSectionBase = {
|
||||
name?: string;
|
||||
defaultAccount?: string;
|
||||
@@ -415,9 +458,9 @@ type ChannelSetupPromotionSurface = {
|
||||
};
|
||||
|
||||
function getChannelSetupPromotionSurface(channelKey: string): ChannelSetupPromotionSurface | null {
|
||||
const entry = getBundledChannelContractSurfaceEntries().find(
|
||||
(candidate) => candidate.pluginId === channelKey,
|
||||
);
|
||||
const entry = loadSetupPromotionRuntimeModule()
|
||||
.getBundledChannelContractSurfaceEntries()
|
||||
.find((candidate) => candidate.pluginId === channelKey);
|
||||
if (!entry || !entry.surface || typeof entry.surface !== "object") {
|
||||
return null;
|
||||
}
|
||||
@@ -473,14 +516,22 @@ export function resolveSingleAccountPromotionTarget(params: {
|
||||
channelKey: string;
|
||||
channel: ChannelSectionBase;
|
||||
}): string {
|
||||
const accounts = params.channel.accounts ?? {};
|
||||
const resolveExistingAccountId = (targetAccountId: string): string => {
|
||||
const normalizedTargetAccountId = normalizeAccountId(targetAccountId);
|
||||
const matchedAccountId = Object.keys(accounts).find(
|
||||
(accountId) => normalizeAccountId(accountId) === normalizedTargetAccountId,
|
||||
);
|
||||
return matchedAccountId ?? normalizedTargetAccountId;
|
||||
};
|
||||
const surface = getChannelSetupPromotionSurface(params.channelKey);
|
||||
const resolved = surface?.resolveSingleAccountPromotionTarget?.({
|
||||
channel: params.channel,
|
||||
});
|
||||
if (typeof resolved === "string" && resolved.trim()) {
|
||||
return normalizeAccountId(resolved);
|
||||
return resolveExistingAccountId(resolved);
|
||||
}
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
return resolveExistingAccountId(DEFAULT_ACCOUNT_ID);
|
||||
}
|
||||
|
||||
function cloneIfObject<T>(value: T): T {
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
|
||||
import { mapFailoverReasonToProbeStatus } from "./list.probe.js";
|
||||
|
||||
describe("mapFailoverReasonToProbeStatus", () => {
|
||||
it("does not import the embedded runner on module load", async () => {
|
||||
vi.doMock("../../agents/pi-embedded.js", () => {
|
||||
throw new Error("pi-embedded should stay lazy for probe imports");
|
||||
});
|
||||
try {
|
||||
await importFreshModule<typeof import("./list.probe.js")>(
|
||||
import.meta.url,
|
||||
`./list.probe.js?scope=${Math.random().toString(36).slice(2)}`,
|
||||
);
|
||||
} finally {
|
||||
vi.doUnmock("../../agents/pi-embedded.js");
|
||||
}
|
||||
});
|
||||
|
||||
it("maps auth_permanent to auth", () => {
|
||||
expect(mapFailoverReasonToProbeStatus("auth_permanent")).toBe("auth");
|
||||
});
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
normalizeProviderId,
|
||||
parseModelRef,
|
||||
} from "../../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
@@ -33,6 +32,13 @@ import { DEFAULT_PROVIDER, formatMs } from "./shared.js";
|
||||
|
||||
const PROBE_PROMPT = "Reply with OK. Do not use tools.";
|
||||
|
||||
let embeddedRunnerModulePromise: Promise<typeof import("../../agents/pi-embedded.js")> | undefined;
|
||||
|
||||
function loadEmbeddedRunnerModule() {
|
||||
embeddedRunnerModulePromise ??= import("../../agents/pi-embedded.js");
|
||||
return embeddedRunnerModulePromise;
|
||||
}
|
||||
|
||||
export type AuthProbeStatus =
|
||||
| "ok"
|
||||
| "auth"
|
||||
@@ -450,6 +456,7 @@ async function probeTarget(params: {
|
||||
latencyMs: Date.now() - start,
|
||||
});
|
||||
try {
|
||||
const { runEmbeddedPiAgent } = await loadEmbeddedRunnerModule();
|
||||
await runEmbeddedPiAgent({
|
||||
sessionId,
|
||||
sessionFile,
|
||||
|
||||
@@ -23,15 +23,19 @@ export {
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
createStandardChannelSetupStatus,
|
||||
mergeAllowFromEntries,
|
||||
noteChannelLookupFailure,
|
||||
noteChannelLookupSummary,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
promptResolvedAllowFrom,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
promptParsedAllowFromForAccount,
|
||||
resolveEntriesWithOptionalToken,
|
||||
resolveSetupAccountId,
|
||||
setAccountAllowFromForChannel,
|
||||
setSetupChannelEnabled,
|
||||
splitSetupEntries,
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
|
||||
export { createAllowlistSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js";
|
||||
|
||||
Reference in New Issue
Block a user