mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 06:30:21 +00:00
Plugins: remove shared extension boundary debt
This commit is contained in:
135
src/plugin-sdk/extension-shared.ts
Normal file
135
src/plugin-sdk/extension-shared.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { z } from "zod";
|
||||
import { runPassiveAccountLifecycle } from "./channel-runtime.js";
|
||||
import { createLoggerBackedRuntime } from "./runtime.js";
|
||||
|
||||
type PassiveChannelStatusSnapshot = {
|
||||
configured?: boolean;
|
||||
running?: boolean;
|
||||
lastStartAt?: number | null;
|
||||
lastStopAt?: number | null;
|
||||
lastError?: string | null;
|
||||
probe?: unknown;
|
||||
lastProbeAt?: number | null;
|
||||
};
|
||||
|
||||
type TrafficStatusSnapshot = {
|
||||
lastInboundAt?: number | null;
|
||||
lastOutboundAt?: number | null;
|
||||
};
|
||||
|
||||
type StoppableMonitor = {
|
||||
stop: () => void;
|
||||
};
|
||||
|
||||
type RequireOpenAllowFromFn = (params: {
|
||||
policy?: string;
|
||||
allowFrom?: Array<string | number>;
|
||||
ctx: z.RefinementCtx;
|
||||
path: Array<string | number>;
|
||||
message: string;
|
||||
}) => void;
|
||||
|
||||
export function buildPassiveChannelStatusSummary<TExtra extends object>(
|
||||
snapshot: PassiveChannelStatusSnapshot,
|
||||
extra?: TExtra,
|
||||
) {
|
||||
return {
|
||||
configured: snapshot.configured ?? false,
|
||||
...(extra ?? ({} as TExtra)),
|
||||
running: snapshot.running ?? false,
|
||||
lastStartAt: snapshot.lastStartAt ?? null,
|
||||
lastStopAt: snapshot.lastStopAt ?? null,
|
||||
lastError: snapshot.lastError ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPassiveProbedChannelStatusSummary<TExtra extends object>(
|
||||
snapshot: PassiveChannelStatusSnapshot,
|
||||
extra?: TExtra,
|
||||
) {
|
||||
return {
|
||||
...buildPassiveChannelStatusSummary(snapshot, extra),
|
||||
probe: snapshot.probe,
|
||||
lastProbeAt: snapshot.lastProbeAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTrafficStatusSummary<TSnapshot extends TrafficStatusSnapshot>(
|
||||
snapshot?: TSnapshot | null,
|
||||
) {
|
||||
return {
|
||||
lastInboundAt: snapshot?.lastInboundAt ?? null,
|
||||
lastOutboundAt: snapshot?.lastOutboundAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runStoppablePassiveMonitor<TMonitor extends StoppableMonitor>(params: {
|
||||
abortSignal: AbortSignal;
|
||||
start: () => Promise<TMonitor>;
|
||||
}): Promise<void> {
|
||||
await runPassiveAccountLifecycle({
|
||||
abortSignal: params.abortSignal,
|
||||
start: params.start,
|
||||
stop: async (monitor) => {
|
||||
monitor.stop();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveLoggerBackedRuntime<TRuntime>(
|
||||
runtime: TRuntime | undefined,
|
||||
logger: Parameters<typeof createLoggerBackedRuntime>[0]["logger"],
|
||||
): TRuntime {
|
||||
return (
|
||||
runtime ??
|
||||
(createLoggerBackedRuntime({
|
||||
logger,
|
||||
exitError: () => new Error("Runtime exit not available"),
|
||||
}) as TRuntime)
|
||||
);
|
||||
}
|
||||
|
||||
export function requireChannelOpenAllowFrom(params: {
|
||||
channel: string;
|
||||
policy?: string;
|
||||
allowFrom?: Array<string | number>;
|
||||
ctx: z.RefinementCtx;
|
||||
requireOpenAllowFrom: RequireOpenAllowFromFn;
|
||||
}) {
|
||||
params.requireOpenAllowFrom({
|
||||
policy: params.policy,
|
||||
allowFrom: params.allowFrom,
|
||||
ctx: params.ctx,
|
||||
path: ["allowFrom"],
|
||||
message: `channels.${params.channel}.dmPolicy="open" requires channels.${params.channel}.allowFrom to include "*"`,
|
||||
});
|
||||
}
|
||||
|
||||
export function readStatusIssueFields<TField extends string>(
|
||||
value: unknown,
|
||||
fields: readonly TField[],
|
||||
): Record<TField, unknown> | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const result = {} as Record<TField, unknown>;
|
||||
for (const field of fields) {
|
||||
result[field] = record[field];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function coerceStatusIssueAccountId(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : typeof value === "number" ? String(value) : undefined;
|
||||
}
|
||||
|
||||
export function createDeferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
// Narrow public testing surface for plugin authors.
|
||||
// Keep this list additive and limited to helpers we are willing to support.
|
||||
|
||||
@@ -7,3 +11,79 @@ export type { OpenClawConfig } from "../config/config.js";
|
||||
export type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
export type { RuntimeEnv } from "../runtime.js";
|
||||
export type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
||||
|
||||
export async function createWindowsCmdShimFixture(params: {
|
||||
shimPath: string;
|
||||
scriptPath: string;
|
||||
shimLine: string;
|
||||
}): Promise<void> {
|
||||
await fs.mkdir(path.dirname(params.scriptPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(params.shimPath), { recursive: true });
|
||||
await fs.writeFile(params.scriptPath, "module.exports = {};\n", "utf8");
|
||||
await fs.writeFile(params.shimPath, `@echo off\r\n${params.shimLine}\r\n`, "utf8");
|
||||
}
|
||||
|
||||
type ResolveTargetMode = "explicit" | "implicit" | "heartbeat";
|
||||
|
||||
type ResolveTargetResult = {
|
||||
ok: boolean;
|
||||
to?: string;
|
||||
error?: unknown;
|
||||
};
|
||||
|
||||
type ResolveTargetFn = (params: {
|
||||
to?: string;
|
||||
mode: ResolveTargetMode;
|
||||
allowFrom: string[];
|
||||
}) => ResolveTargetResult;
|
||||
|
||||
export function installCommonResolveTargetErrorCases(params: {
|
||||
resolveTarget: ResolveTargetFn;
|
||||
implicitAllowFrom: string[];
|
||||
}) {
|
||||
const { resolveTarget, implicitAllowFrom } = params;
|
||||
|
||||
it("should error on normalization failure with allowlist (implicit mode)", () => {
|
||||
const result = resolveTarget({
|
||||
to: "invalid-target",
|
||||
mode: "implicit",
|
||||
allowFrom: implicitAllowFrom,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
|
||||
it("should error when no target provided with allowlist", () => {
|
||||
const result = resolveTarget({
|
||||
to: undefined,
|
||||
mode: "implicit",
|
||||
allowFrom: implicitAllowFrom,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
|
||||
it("should error when no target and no allowlist", () => {
|
||||
const result = resolveTarget({
|
||||
to: undefined,
|
||||
mode: "explicit",
|
||||
allowFrom: [],
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle whitespace-only target", () => {
|
||||
const result = resolveTarget({
|
||||
to: " ",
|
||||
mode: "explicit",
|
||||
allowFrom: [],
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user