mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 06:51:38 +00:00
refactor(meetings): share plugin entry registration (#113007)
This commit is contained in:
committed by
GitHub
parent
852548ce05
commit
d124fb235f
@@ -1,32 +1,19 @@
|
||||
import {
|
||||
readNonNegativeIntegerParam,
|
||||
readPositiveIntegerParam,
|
||||
} from "openclaw/plugin-sdk/channel-actions";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
callGatewayFromCli,
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
type GatewayRequestHandlerOptions,
|
||||
} from "openclaw/plugin-sdk/gateway-runtime";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { jsonResult as json } from "openclaw/plugin-sdk/tool-results";
|
||||
import { MeetingPlatformAdapter } from "openclaw/plugin-sdk/meeting-runtime";
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import { Type } from "typebox";
|
||||
import {
|
||||
resolveTeamsMeetingsConfig,
|
||||
resolveTeamsMeetingsGatewayOperationTimeoutMs,
|
||||
type TeamsMeetingsConfig,
|
||||
type TeamsMeetingsMode,
|
||||
type TeamsMeetingsTransport,
|
||||
} from "./src/config.js";
|
||||
import { handleTeamsMeetingsNodeHostCommand } from "./src/node-host.js";
|
||||
import { createTeamsMeetingsNodeInvokePolicy } from "./src/node-invoke-policy.js";
|
||||
import { TeamsMeetingsRuntime } from "./src/runtime.js";
|
||||
import { TEAMS_MEETINGS_NODE_COMMAND } from "./src/transports/teams-meetings-platform-constants.js";
|
||||
import { normalizeTeamsMeetingUrl } from "./src/transports/teams-meetings-urls.js";
|
||||
import type { TeamsMeetingsJoinRequest } from "./src/transports/types.js";
|
||||
|
||||
const loadTeamsMeetingsCli = createLazyRuntimeModule(() => import("./src/cli.js"));
|
||||
|
||||
@@ -71,384 +58,72 @@ const TeamsMeetingsToolSchema = Type.Object({
|
||||
message: Type.Optional(Type.String({ description: "Instructions to speak" })),
|
||||
});
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
class TeamsMeetingsInvalidRequestError extends Error {}
|
||||
|
||||
function invalidRequest(message: string): TeamsMeetingsInvalidRequestError {
|
||||
return new TeamsMeetingsInvalidRequestError(message);
|
||||
}
|
||||
|
||||
function normalizeTransport(value: unknown): TeamsMeetingsTransport | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "chrome" || value === "chrome-node") {
|
||||
return value;
|
||||
}
|
||||
throw invalidRequest("transport must be chrome or chrome-node");
|
||||
}
|
||||
|
||||
function normalizeMode(value: unknown): TeamsMeetingsMode | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "agent" || value === "bidi" || value === "transcribe") {
|
||||
return value;
|
||||
}
|
||||
throw invalidRequest("mode must be agent, bidi, or transcribe");
|
||||
}
|
||||
|
||||
function requireString(value: unknown, name: string): string {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
if (!normalized) {
|
||||
throw invalidRequest(`${name} required`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function readSinceIndex(raw: Record<string, unknown>): number | undefined {
|
||||
try {
|
||||
return readNonNegativeIntegerParam(raw, "sinceIndex");
|
||||
} catch (error) {
|
||||
throw invalidRequest(formatErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
function keepTrustedToolAgentId(
|
||||
raw: Record<string, unknown>,
|
||||
client: GatewayRequestHandlerOptions["client"],
|
||||
): Record<string, unknown> {
|
||||
const { agentId: rawAgentId, ...rest } = raw;
|
||||
if (client?.internal?.pluginRuntimeOwnerId !== "teams-meetings") {
|
||||
return rest;
|
||||
}
|
||||
const agentId = normalizeOptionalString(rawAgentId);
|
||||
return agentId ? { ...rest, agentId } : rest;
|
||||
}
|
||||
|
||||
function trustedToolAgentId(
|
||||
raw: Record<string, unknown>,
|
||||
client: GatewayRequestHandlerOptions["client"],
|
||||
): string | undefined {
|
||||
return normalizeOptionalString(keepTrustedToolAgentId(raw, client).agentId);
|
||||
}
|
||||
|
||||
function joinRequest(raw: Record<string, unknown>, options?: { allowTimeout?: boolean }) {
|
||||
if (!options?.allowTimeout && raw.timeoutMs !== undefined) {
|
||||
throw invalidRequest("timeoutMs is supported only by testSpeech or testListen");
|
||||
}
|
||||
let url: string;
|
||||
let timeoutMs: number | undefined;
|
||||
try {
|
||||
url = normalizeTeamsMeetingUrl(requireString(raw.url, "url"));
|
||||
timeoutMs = readPositiveIntegerParam(raw, "timeoutMs");
|
||||
} catch (error) {
|
||||
if (error instanceof TeamsMeetingsInvalidRequestError) {
|
||||
throw error;
|
||||
}
|
||||
throw invalidRequest(formatErrorMessage(error));
|
||||
}
|
||||
return {
|
||||
url,
|
||||
transport: normalizeTransport(raw.transport),
|
||||
mode: normalizeMode(raw.mode),
|
||||
message: normalizeOptionalString(raw.message),
|
||||
requesterSessionKey: normalizeOptionalString(raw.requesterSessionKey),
|
||||
agentId: normalizeOptionalString(raw.agentId),
|
||||
timeoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
type ToolAction = "join" | "leave" | "status" | "transcript" | "speak";
|
||||
|
||||
function gatewayMethod(action: ToolAction): string {
|
||||
return `teamsmeetings.${action}`;
|
||||
}
|
||||
|
||||
function readErrorDetails(error: unknown): unknown {
|
||||
return error && typeof error === "object" && "details" in error
|
||||
? (error as { details?: unknown }).details
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function callGatewayFromTool(params: {
|
||||
action: ToolAction;
|
||||
config: TeamsMeetingsConfig;
|
||||
raw: Record<string, unknown>;
|
||||
runtime?: OpenClawPluginApi["runtime"];
|
||||
}) {
|
||||
try {
|
||||
if (params.runtime) {
|
||||
return await params.runtime.gateway.request(gatewayMethod(params.action), params.raw, {
|
||||
timeoutMs: resolveTeamsMeetingsGatewayOperationTimeoutMs(params.config),
|
||||
scopes: ["operator.admin"],
|
||||
});
|
||||
}
|
||||
return await callGatewayFromCli(
|
||||
gatewayMethod(params.action),
|
||||
{
|
||||
json: true,
|
||||
timeout: String(resolveTeamsMeetingsGatewayOperationTimeoutMs(params.config)),
|
||||
},
|
||||
params.raw,
|
||||
{ progress: false, scopes: ["operator.admin"] },
|
||||
);
|
||||
} catch (error) {
|
||||
const details = readErrorDetails(error);
|
||||
if (details && typeof details === "object") {
|
||||
return details;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "teams-meetings",
|
||||
name: "Microsoft Teams meetings",
|
||||
description: "Join Microsoft Teams meetings as a Chrome browser guest",
|
||||
configSchema: teamsMeetingsConfigSchema,
|
||||
register(api: OpenClawPluginApi) {
|
||||
const config = teamsMeetingsConfigSchema.parse(api.pluginConfig);
|
||||
let runtime: TeamsMeetingsRuntime | undefined;
|
||||
|
||||
const ensureRuntime = async () => {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Microsoft Teams meetings plugin disabled in plugin config");
|
||||
export default definePluginEntry(
|
||||
MeetingPlatformAdapter.createPluginEntry<
|
||||
TeamsMeetingsConfig,
|
||||
TeamsMeetingsJoinRequest,
|
||||
TeamsMeetingsRuntime
|
||||
>({
|
||||
id: "teams-meetings",
|
||||
name: "Microsoft Teams meetings",
|
||||
description: "Join Microsoft Teams meetings as a Chrome browser guest",
|
||||
configSchema: teamsMeetingsConfigSchema,
|
||||
disabledMessage: "Microsoft Teams meetings plugin disabled in plugin config",
|
||||
gatewayMethodPrefix: "teamsmeetings",
|
||||
invalidRequest: (message) => new TeamsMeetingsInvalidRequestError(message),
|
||||
isInvalidRequest: (error) => error instanceof TeamsMeetingsInvalidRequestError,
|
||||
normalizeUrl: normalizeTeamsMeetingUrl,
|
||||
resolveGatewayTimeoutMs: resolveTeamsMeetingsGatewayOperationTimeoutMs,
|
||||
normalizeRequesterSessionKey: (value) =>
|
||||
typeof value === "string" && value.trim() ? value.trim() : undefined,
|
||||
normalizeToolAgentId: (agentId) => (agentId ? normalizeAgentId(agentId) : undefined),
|
||||
resolveToolRuntime: async (api, agentId) => {
|
||||
const trustedRouting = Boolean(agentId && agentId !== "main");
|
||||
const useRuntime = trustedRouting ? await api.runtime.gateway.isAvailable() : false;
|
||||
if (trustedRouting && !useRuntime) {
|
||||
throw new Error(
|
||||
"Per-agent Microsoft Teams meeting routing requires a Gateway-hosted agent run.",
|
||||
);
|
||||
}
|
||||
runtime ??= new TeamsMeetingsRuntime({
|
||||
return useRuntime ? api.runtime : undefined;
|
||||
},
|
||||
unknownActionMessage: "unknown teams_meetings action",
|
||||
toolName: "teams_meetings",
|
||||
toolLabel: "Microsoft Teams meetings",
|
||||
toolDescription:
|
||||
"Join and manage Microsoft Teams meeting browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.",
|
||||
toolParameters: TeamsMeetingsToolSchema,
|
||||
createRuntime: ({ api, config }) =>
|
||||
new TeamsMeetingsRuntime({
|
||||
config,
|
||||
fullConfig: api.config,
|
||||
runtime: api.runtime,
|
||||
logger: api.logger,
|
||||
});
|
||||
return runtime;
|
||||
};
|
||||
|
||||
const sendError = (
|
||||
respond: GatewayRequestHandlerOptions["respond"],
|
||||
error: unknown,
|
||||
code: Parameters<typeof errorShape>[0] = ErrorCodes.UNAVAILABLE,
|
||||
) => {
|
||||
const payload = { error: formatErrorMessage(error) };
|
||||
respond(false, payload, errorShape(code, payload.error, { details: payload }));
|
||||
};
|
||||
const sendRequestError = (respond: GatewayRequestHandlerOptions["respond"], error: unknown) =>
|
||||
sendError(
|
||||
respond,
|
||||
error,
|
||||
error instanceof TeamsMeetingsInvalidRequestError
|
||||
? ErrorCodes.INVALID_REQUEST
|
||||
: ErrorCodes.UNAVAILABLE,
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"teamsmeetings.join",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = keepTrustedToolAgentId(asRecord(params), client);
|
||||
respond(true, await (await ensureRuntime()).join(joinRequest(raw)));
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"teamsmeetings.leave",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false }
|
||||
: await rt.leave(sessionId),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"teamsmeetings.status",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId
|
||||
? await rt.statusForAgent(agentId, normalizeOptionalString(raw.sessionId))
|
||||
: await rt.status(normalizeOptionalString(raw.sessionId)),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"teamsmeetings.transcript",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const sinceIndex = readSinceIndex(raw);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false }
|
||||
: await rt.transcript(sessionId, sinceIndex === undefined ? {} : { sinceIndex }),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"teamsmeetings.speak",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false, spoken: false }
|
||||
: await rt.speak(sessionId, normalizeOptionalString(raw.message)),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"teamsmeetings.setup",
|
||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
respond(
|
||||
true,
|
||||
await (
|
||||
await ensureRuntime()
|
||||
).setupStatus({
|
||||
mode: normalizeMode(params?.mode),
|
||||
transport: normalizeTransport(params?.transport),
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
for (const [method, run] of [
|
||||
[
|
||||
"teamsmeetings.testSpeech",
|
||||
(rt: TeamsMeetingsRuntime, raw: Record<string, unknown>) =>
|
||||
rt.testSpeech(joinRequest(raw, { allowTimeout: true })),
|
||||
],
|
||||
[
|
||||
"teamsmeetings.testListen",
|
||||
(rt: TeamsMeetingsRuntime, raw: Record<string, unknown>) =>
|
||||
rt.testListen(joinRequest(raw, { allowTimeout: true })),
|
||||
],
|
||||
] as const) {
|
||||
api.registerGatewayMethod(
|
||||
method,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = keepTrustedToolAgentId(asRecord(params), client);
|
||||
respond(true, await run(await ensureRuntime(), raw));
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
api.registerTool(
|
||||
(toolContext) => ({
|
||||
name: "teams_meetings",
|
||||
label: "Microsoft Teams meetings",
|
||||
description:
|
||||
"Join and manage Microsoft Teams meeting browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.",
|
||||
parameters: TeamsMeetingsToolSchema,
|
||||
async execute(_toolCallId, params) {
|
||||
const raw = asRecord(params);
|
||||
const action = raw.action as ToolAction;
|
||||
const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
|
||||
const contextAgentId =
|
||||
toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
|
||||
const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : undefined;
|
||||
try {
|
||||
if (!(["join", "leave", "status", "transcript", "speak"] as const).includes(action)) {
|
||||
throw new Error("unknown teams_meetings action");
|
||||
}
|
||||
const trustedRouting = Boolean(agentId && agentId !== "main");
|
||||
const useRuntime = trustedRouting ? await api.runtime.gateway.isAvailable() : false;
|
||||
if (trustedRouting && !useRuntime) {
|
||||
throw new Error(
|
||||
"Per-agent Microsoft Teams meeting routing requires a Gateway-hosted agent run.",
|
||||
);
|
||||
}
|
||||
return json(
|
||||
await callGatewayFromTool({
|
||||
action,
|
||||
config,
|
||||
raw: {
|
||||
...raw,
|
||||
...(requesterSessionKey ? { requesterSessionKey } : {}),
|
||||
...(useRuntime ? { agentId } : {}),
|
||||
},
|
||||
runtime: useRuntime ? api.runtime : undefined,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return json({ error: formatErrorMessage(error) });
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ name: "teams_meetings" },
|
||||
);
|
||||
|
||||
api.registerNodeHostCommand({
|
||||
command: TEAMS_MEETINGS_NODE_COMMAND,
|
||||
cap: "teams-meetings",
|
||||
dangerous: true,
|
||||
handle: handleTeamsMeetingsNodeHostCommand,
|
||||
});
|
||||
api.registerNodeInvokePolicy(createTeamsMeetingsNodeInvokePolicy(config));
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const cli = await loadTeamsMeetingsCli();
|
||||
cli.registerTeamsMeetingsCli({ program, config });
|
||||
},
|
||||
{
|
||||
commands: ["teamsmeetings"],
|
||||
descriptors: [
|
||||
{
|
||||
name: "teamsmeetings",
|
||||
description: "Join and manage Microsoft Teams meeting guests",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
nodeCommand: TEAMS_MEETINGS_NODE_COMMAND,
|
||||
cap: "teams-meetings",
|
||||
nodeHandler: handleTeamsMeetingsNodeHostCommand,
|
||||
createNodePolicy: createTeamsMeetingsNodeInvokePolicy,
|
||||
registerNodeWhen: () => true,
|
||||
registerCli: (api, config) => {
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const cli = await loadTeamsMeetingsCli();
|
||||
cli.registerTeamsMeetingsCli({ program, config });
|
||||
},
|
||||
{
|
||||
commands: ["teamsmeetings"],
|
||||
descriptors: [
|
||||
{
|
||||
name: "teamsmeetings",
|
||||
description: "Join and manage Microsoft Teams meeting guests",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,34 +1,18 @@
|
||||
import {
|
||||
readNonNegativeIntegerParam,
|
||||
readPositiveIntegerParam,
|
||||
} from "openclaw/plugin-sdk/channel-actions";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
callGatewayFromCli,
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
type GatewayRequestHandlerOptions,
|
||||
} from "openclaw/plugin-sdk/gateway-runtime";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { jsonResult as json } from "openclaw/plugin-sdk/tool-results";
|
||||
import { MeetingPlatformAdapter } from "openclaw/plugin-sdk/meeting-runtime";
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import { Type } from "typebox";
|
||||
import {
|
||||
resolveZoomMeetingsConfig,
|
||||
resolveZoomMeetingsGatewayOperationTimeoutMs,
|
||||
type ZoomMeetingsConfig,
|
||||
type ZoomMeetingsMode,
|
||||
type ZoomMeetingsTransport,
|
||||
} from "./src/config.js";
|
||||
import {
|
||||
ZoomMeetingsInvalidRequestError,
|
||||
zoomMeetingsInvalidRequest as invalidRequest,
|
||||
} from "./src/errors.js";
|
||||
import { ZoomMeetingsInvalidRequestError, zoomMeetingsInvalidRequest } from "./src/errors.js";
|
||||
import { handleZoomMeetingsNodeHostCommand } from "./src/node-host.js";
|
||||
import { createZoomMeetingsNodeInvokePolicy } from "./src/node-invoke-policy.js";
|
||||
import { ZoomMeetingsRuntime } from "./src/runtime.js";
|
||||
import type { ZoomMeetingsJoinRequest } from "./src/transports/types.js";
|
||||
import { ZOOM_MEETINGS_NODE_COMMAND } from "./src/transports/zoom-meetings-platform-constants.js";
|
||||
import { normalizeZoomMeetingUrl } from "./src/transports/zoom-meetings-urls.js";
|
||||
|
||||
@@ -75,382 +59,66 @@ const ZoomMeetingsToolSchema = Type.Object({
|
||||
message: Type.Optional(Type.String({ description: "Instructions to speak" })),
|
||||
});
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function normalizeTransport(value: unknown): ZoomMeetingsTransport | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "chrome" || value === "chrome-node") {
|
||||
return value;
|
||||
}
|
||||
throw invalidRequest("transport must be chrome or chrome-node");
|
||||
}
|
||||
|
||||
function normalizeMode(value: unknown): ZoomMeetingsMode | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "agent" || value === "bidi" || value === "transcribe") {
|
||||
return value;
|
||||
}
|
||||
throw invalidRequest("mode must be agent, bidi, or transcribe");
|
||||
}
|
||||
|
||||
function requireString(value: unknown, name: string): string {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
if (!normalized) {
|
||||
throw invalidRequest(`${name} required`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function readSinceIndex(raw: Record<string, unknown>): number | undefined {
|
||||
try {
|
||||
return readNonNegativeIntegerParam(raw, "sinceIndex");
|
||||
} catch (error) {
|
||||
throw invalidRequest(formatErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
function keepTrustedToolContext(
|
||||
raw: Record<string, unknown>,
|
||||
client: GatewayRequestHandlerOptions["client"],
|
||||
): Record<string, unknown> {
|
||||
const { agentId: rawAgentId, requesterSessionKey: rawRequesterSessionKey, ...rest } = raw;
|
||||
if (client?.internal?.pluginRuntimeOwnerId !== "zoom-meetings") {
|
||||
return rest;
|
||||
}
|
||||
const agentId = normalizeOptionalString(rawAgentId);
|
||||
const requesterSessionKey = normalizeOptionalString(rawRequesterSessionKey);
|
||||
return {
|
||||
...rest,
|
||||
...(agentId ? { agentId } : {}),
|
||||
...(requesterSessionKey ? { requesterSessionKey } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function trustedToolAgentId(
|
||||
raw: Record<string, unknown>,
|
||||
client: GatewayRequestHandlerOptions["client"],
|
||||
): string | undefined {
|
||||
return normalizeOptionalString(keepTrustedToolContext(raw, client).agentId);
|
||||
}
|
||||
|
||||
function joinRequest(raw: Record<string, unknown>, options?: { allowTimeout?: boolean }) {
|
||||
if (!options?.allowTimeout && raw.timeoutMs !== undefined) {
|
||||
throw invalidRequest("timeoutMs is supported only by testSpeech or testListen");
|
||||
}
|
||||
let url: string;
|
||||
let timeoutMs: number | undefined;
|
||||
try {
|
||||
url = normalizeZoomMeetingUrl(requireString(raw.url, "url"));
|
||||
timeoutMs = readPositiveIntegerParam(raw, "timeoutMs");
|
||||
} catch (error) {
|
||||
if (error instanceof ZoomMeetingsInvalidRequestError) {
|
||||
throw error;
|
||||
}
|
||||
throw invalidRequest(formatErrorMessage(error));
|
||||
}
|
||||
return {
|
||||
url,
|
||||
transport: normalizeTransport(raw.transport),
|
||||
mode: normalizeMode(raw.mode),
|
||||
message: normalizeOptionalString(raw.message),
|
||||
requesterSessionKey: normalizeOptionalString(raw.requesterSessionKey),
|
||||
agentId: normalizeOptionalString(raw.agentId),
|
||||
timeoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
type ToolAction = "join" | "leave" | "status" | "transcript" | "speak";
|
||||
|
||||
function gatewayMethod(action: ToolAction): string {
|
||||
return `zoommeetings.${action}`;
|
||||
}
|
||||
|
||||
function readErrorDetails(error: unknown): unknown {
|
||||
return error && typeof error === "object" && "details" in error
|
||||
? (error as { details?: unknown }).details
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function callGatewayFromTool(params: {
|
||||
action: ToolAction;
|
||||
config: ZoomMeetingsConfig;
|
||||
raw: Record<string, unknown>;
|
||||
runtime?: OpenClawPluginApi["runtime"];
|
||||
}) {
|
||||
try {
|
||||
if (params.runtime) {
|
||||
return await params.runtime.gateway.request(gatewayMethod(params.action), params.raw, {
|
||||
timeoutMs: resolveZoomMeetingsGatewayOperationTimeoutMs(params.config),
|
||||
scopes: ["operator.admin"],
|
||||
});
|
||||
}
|
||||
return await callGatewayFromCli(
|
||||
gatewayMethod(params.action),
|
||||
{
|
||||
json: true,
|
||||
timeout: String(resolveZoomMeetingsGatewayOperationTimeoutMs(params.config)),
|
||||
},
|
||||
params.raw,
|
||||
{ progress: false, scopes: ["operator.admin"] },
|
||||
);
|
||||
} catch (error) {
|
||||
const details = readErrorDetails(error);
|
||||
if (details && typeof details === "object") {
|
||||
return details;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "zoom-meetings",
|
||||
name: "Zoom meetings",
|
||||
description: "Join Zoom meetings as a Chrome browser guest",
|
||||
configSchema: zoomMeetingsConfigSchema,
|
||||
register(api: OpenClawPluginApi) {
|
||||
const config = zoomMeetingsConfigSchema.parse(api.pluginConfig);
|
||||
let runtime: ZoomMeetingsRuntime | undefined;
|
||||
|
||||
const ensureRuntime = async () => {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Zoom meetings plugin disabled in plugin config");
|
||||
export default definePluginEntry(
|
||||
MeetingPlatformAdapter.createPluginEntry<
|
||||
ZoomMeetingsConfig,
|
||||
ZoomMeetingsJoinRequest,
|
||||
ZoomMeetingsRuntime
|
||||
>({
|
||||
id: "zoom-meetings",
|
||||
name: "Zoom meetings",
|
||||
description: "Join Zoom meetings as a Chrome browser guest",
|
||||
configSchema: zoomMeetingsConfigSchema,
|
||||
disabledMessage: "Zoom meetings plugin disabled in plugin config",
|
||||
gatewayMethodPrefix: "zoommeetings",
|
||||
invalidRequest: zoomMeetingsInvalidRequest,
|
||||
isInvalidRequest: (error) => error instanceof ZoomMeetingsInvalidRequestError,
|
||||
normalizeUrl: normalizeZoomMeetingUrl,
|
||||
resolveGatewayTimeoutMs: resolveZoomMeetingsGatewayOperationTimeoutMs,
|
||||
normalizeRequesterSessionKey: (value, trustedOwner) =>
|
||||
trustedOwner && typeof value === "string" && value.trim() ? value.trim() : undefined,
|
||||
normalizeToolAgentId: (agentId) => normalizeAgentId(agentId),
|
||||
resolveToolRuntime: async (api) => {
|
||||
if (!(await api.runtime.gateway.isAvailable())) {
|
||||
throw new Error("Zoom meeting tools require a Gateway-hosted agent run.");
|
||||
}
|
||||
runtime ??= new ZoomMeetingsRuntime({
|
||||
return api.runtime;
|
||||
},
|
||||
unknownActionMessage: "unknown zoom_meetings action",
|
||||
toolName: "zoom_meetings",
|
||||
toolLabel: "Zoom meetings",
|
||||
toolDescription:
|
||||
"Join and manage Zoom meeting browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.",
|
||||
toolParameters: ZoomMeetingsToolSchema,
|
||||
createRuntime: ({ api, config }) =>
|
||||
new ZoomMeetingsRuntime({
|
||||
config,
|
||||
fullConfig: api.config,
|
||||
runtime: api.runtime,
|
||||
logger: api.logger,
|
||||
});
|
||||
return runtime;
|
||||
};
|
||||
|
||||
const sendError = (
|
||||
respond: GatewayRequestHandlerOptions["respond"],
|
||||
error: unknown,
|
||||
code: Parameters<typeof errorShape>[0] = ErrorCodes.UNAVAILABLE,
|
||||
) => {
|
||||
const payload = { error: formatErrorMessage(error) };
|
||||
respond(false, payload, errorShape(code, payload.error, { details: payload }));
|
||||
};
|
||||
const sendRequestError = (respond: GatewayRequestHandlerOptions["respond"], error: unknown) =>
|
||||
sendError(
|
||||
respond,
|
||||
error,
|
||||
error instanceof ZoomMeetingsInvalidRequestError
|
||||
? ErrorCodes.INVALID_REQUEST
|
||||
: ErrorCodes.UNAVAILABLE,
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"zoommeetings.join",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = keepTrustedToolContext(asRecord(params), client);
|
||||
respond(true, await (await ensureRuntime()).join(joinRequest(raw)));
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"zoommeetings.leave",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false }
|
||||
: await rt.leave(sessionId),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"zoommeetings.status",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId
|
||||
? await rt.statusForAgent(agentId, normalizeOptionalString(raw.sessionId))
|
||||
: await rt.status(normalizeOptionalString(raw.sessionId)),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"zoommeetings.transcript",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const sinceIndex = readSinceIndex(raw);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false }
|
||||
: await rt.transcript(sessionId, sinceIndex === undefined ? {} : { sinceIndex }),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"zoommeetings.speak",
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false, spoken: false }
|
||||
: await rt.speak(sessionId, normalizeOptionalString(raw.message)),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"zoommeetings.setup",
|
||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
respond(
|
||||
true,
|
||||
await (
|
||||
await ensureRuntime()
|
||||
).setupStatus({
|
||||
mode: normalizeMode(params?.mode),
|
||||
transport: normalizeTransport(params?.transport),
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
for (const [method, run] of [
|
||||
[
|
||||
"zoommeetings.testSpeech",
|
||||
(rt: ZoomMeetingsRuntime, raw: Record<string, unknown>) =>
|
||||
rt.testSpeech(joinRequest(raw, { allowTimeout: true })),
|
||||
],
|
||||
[
|
||||
"zoommeetings.testListen",
|
||||
(rt: ZoomMeetingsRuntime, raw: Record<string, unknown>) =>
|
||||
rt.testListen(joinRequest(raw, { allowTimeout: true })),
|
||||
],
|
||||
] as const) {
|
||||
api.registerGatewayMethod(
|
||||
method,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = keepTrustedToolContext(asRecord(params), client);
|
||||
respond(true, await run(await ensureRuntime(), raw));
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
api.registerTool(
|
||||
(toolContext) => ({
|
||||
name: "zoom_meetings",
|
||||
label: "Zoom meetings",
|
||||
description:
|
||||
"Join and manage Zoom meeting browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.",
|
||||
parameters: ZoomMeetingsToolSchema,
|
||||
async execute(_toolCallId, params) {
|
||||
const raw = asRecord(params);
|
||||
const action = raw.action as ToolAction;
|
||||
const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
|
||||
const contextAgentId =
|
||||
toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
|
||||
const agentId = normalizeAgentId(contextAgentId);
|
||||
try {
|
||||
if (!(["join", "leave", "status", "transcript", "speak"] as const).includes(action)) {
|
||||
throw new Error("unknown zoom_meetings action");
|
||||
}
|
||||
const useRuntime = await api.runtime.gateway.isAvailable();
|
||||
if (!useRuntime) {
|
||||
throw new Error("Zoom meeting tools require a Gateway-hosted agent run.");
|
||||
}
|
||||
return json(
|
||||
await callGatewayFromTool({
|
||||
action,
|
||||
config,
|
||||
raw: {
|
||||
...raw,
|
||||
...(requesterSessionKey ? { requesterSessionKey } : {}),
|
||||
agentId,
|
||||
},
|
||||
runtime: api.runtime,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return json({ error: formatErrorMessage(error) });
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ name: "zoom_meetings" },
|
||||
);
|
||||
|
||||
if (config.enabled) {
|
||||
api.registerNodeHostCommand({
|
||||
command: ZOOM_MEETINGS_NODE_COMMAND,
|
||||
cap: "zoom-meetings",
|
||||
dangerous: true,
|
||||
handle: handleZoomMeetingsNodeHostCommand,
|
||||
});
|
||||
api.registerNodeInvokePolicy(createZoomMeetingsNodeInvokePolicy(config));
|
||||
}
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const cli = await loadZoomMeetingsCli();
|
||||
cli.registerZoomMeetingsCli({ program, config });
|
||||
},
|
||||
{
|
||||
commands: ["zoommeetings"],
|
||||
descriptors: [
|
||||
{
|
||||
name: "zoommeetings",
|
||||
description: "Join and manage Zoom meeting guests",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
nodeCommand: ZOOM_MEETINGS_NODE_COMMAND,
|
||||
cap: "zoom-meetings",
|
||||
nodeHandler: handleZoomMeetingsNodeHostCommand,
|
||||
createNodePolicy: createZoomMeetingsNodeInvokePolicy,
|
||||
registerNodeWhen: (config) => config.enabled,
|
||||
registerCli: (api, config) => {
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const cli = await loadZoomMeetingsCli();
|
||||
cli.registerZoomMeetingsCli({ program, config });
|
||||
},
|
||||
{
|
||||
commands: ["zoommeetings"],
|
||||
descriptors: [
|
||||
{
|
||||
name: "zoommeetings",
|
||||
description: "Join and manage Zoom meeting guests",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
MeetingPlatformAdapter as MeetingPlatformAdapterContract,
|
||||
MeetingPlatformRuntimeMetadata,
|
||||
} from "./platform-adapter-contract.js";
|
||||
import { createMeetingPluginEntryOptions } from "./plugin-entry.js";
|
||||
import { createMeetingRuntimeProbes } from "./runtime-probes.js";
|
||||
import type { MeetingBrowserHealth, MeetingTranscriptSnapshot } from "./session-types.js";
|
||||
import { createMeetingStatusCallSource } from "./status-call-source.js";
|
||||
@@ -362,6 +363,7 @@ export const MeetingPlatformAdapter = {
|
||||
createChromeTransport: createMeetingChromeTransport,
|
||||
createRuntimeProbes: createMeetingRuntimeProbes,
|
||||
createNodeHostHandler: createMeetingConfiguredNodeHost,
|
||||
createPluginEntry: createMeetingPluginEntryOptions,
|
||||
createStatusCallSource: createMeetingStatusCallSource,
|
||||
createStatusPreludeSource: createMeetingStatusPreludeSource,
|
||||
};
|
||||
|
||||
413
src/meeting-bot/plugin-entry.ts
Normal file
413
src/meeting-bot/plugin-entry.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { TObject } from "typebox";
|
||||
import { ErrorCodes, errorShape } from "../../packages/gateway-protocol/src/schema/error-codes.js";
|
||||
import { readNonNegativeIntegerParam, readPositiveIntegerParam } from "../agents/tools/common.js";
|
||||
import { jsonResult } from "../agents/tools/common.js";
|
||||
import { callGatewayFromCli } from "../cli/gateway-rpc.js";
|
||||
import type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import type { OpenClawPluginApi } from "../plugins/plugin-api.types.js";
|
||||
import type { OpenClawPluginConfigSchema } from "../plugins/plugin-config-schema.types.js";
|
||||
import type { OpenClawPluginNodeInvokePolicy } from "../plugins/plugin-registration.types.js";
|
||||
import { parseAgentSessionKey } from "../sessions/session-key-utils.js";
|
||||
|
||||
type MeetingToolAction = "join" | "leave" | "status" | "transcript" | "speak";
|
||||
type MeetingMode = "agent" | "bidi" | "transcribe";
|
||||
type MeetingTransport = "chrome" | "chrome-node";
|
||||
|
||||
type MeetingJoinRequest = {
|
||||
agentId?: string;
|
||||
message?: string;
|
||||
mode?: MeetingMode;
|
||||
requesterSessionKey?: string;
|
||||
timeoutMs?: number;
|
||||
transport?: MeetingTransport;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type MeetingPluginConfig = {
|
||||
enabled: boolean;
|
||||
chromeNode: { node?: string };
|
||||
};
|
||||
|
||||
type MeetingPluginRuntime<Request extends MeetingJoinRequest> = {
|
||||
join(request: Request): Promise<unknown>;
|
||||
leave(sessionId: string): Promise<unknown>;
|
||||
ownsSession(agentId: string, sessionId: string): boolean;
|
||||
setupStatus(params: { mode?: MeetingMode; transport?: MeetingTransport }): Promise<unknown>;
|
||||
speak(sessionId: string, message?: string): Promise<unknown>;
|
||||
status(sessionId?: string): Promise<unknown>;
|
||||
statusForAgent(agentId: string, sessionId?: string): Promise<unknown>;
|
||||
testListen(request: Request): Promise<unknown>;
|
||||
testSpeech(request: Request): Promise<unknown>;
|
||||
transcript(sessionId: string, options: { sinceIndex?: number }): Promise<unknown>;
|
||||
};
|
||||
|
||||
type MeetingPluginEntryOptions<
|
||||
Config extends MeetingPluginConfig,
|
||||
Request extends MeetingJoinRequest,
|
||||
Runtime extends MeetingPluginRuntime<Request>,
|
||||
> = {
|
||||
cap: string;
|
||||
configSchema: OpenClawPluginConfigSchema & { parse(value: unknown): Config };
|
||||
createNodePolicy(config: Config): OpenClawPluginNodeInvokePolicy;
|
||||
createRuntime(params: { api: OpenClawPluginApi; config: Config }): Runtime;
|
||||
description: string;
|
||||
disabledMessage: string;
|
||||
gatewayMethodPrefix: string;
|
||||
id: string;
|
||||
invalidRequest(message: string): Error;
|
||||
isInvalidRequest(error: unknown): boolean;
|
||||
name: string;
|
||||
nodeCommand: string;
|
||||
nodeHandler(paramsJSON?: string | null): Promise<string>;
|
||||
normalizeRequesterSessionKey(value: unknown, trustedOwner: boolean): string | undefined;
|
||||
normalizeToolAgentId(agentId: string | undefined): string | undefined;
|
||||
normalizeUrl(url: string): string;
|
||||
registerCli(api: OpenClawPluginApi, config: Config): void;
|
||||
registerNodeWhen(config: Config): boolean;
|
||||
resolveGatewayTimeoutMs(config: Config): number;
|
||||
resolveToolRuntime(
|
||||
api: OpenClawPluginApi,
|
||||
agentId: string | undefined,
|
||||
): Promise<OpenClawPluginApi["runtime"] | undefined>;
|
||||
toolDescription: string;
|
||||
toolLabel: string;
|
||||
toolName: string;
|
||||
toolParameters: TObject;
|
||||
unknownActionMessage: string;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function readErrorDetails(error: unknown): unknown {
|
||||
return error && typeof error === "object" && "details" in error
|
||||
? (error as { details?: unknown }).details
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function createMeetingPluginEntryOptions<
|
||||
Config extends MeetingPluginConfig,
|
||||
Request extends MeetingJoinRequest,
|
||||
Runtime extends MeetingPluginRuntime<Request>,
|
||||
>(options: MeetingPluginEntryOptions<Config, Request, Runtime>) {
|
||||
const invalidRequest = (message: string): never => {
|
||||
throw options.invalidRequest(message);
|
||||
};
|
||||
const normalizeTransport = (value: unknown): MeetingTransport | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "chrome" || value === "chrome-node") {
|
||||
return value;
|
||||
}
|
||||
return invalidRequest("transport must be chrome or chrome-node");
|
||||
};
|
||||
const normalizeMode = (value: unknown): MeetingMode | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "agent" || value === "bidi" || value === "transcribe") {
|
||||
return value;
|
||||
}
|
||||
return invalidRequest("mode must be agent, bidi, or transcribe");
|
||||
};
|
||||
const requireString = (value: unknown, name: string): string => {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
return normalized ?? invalidRequest(`${name} required`);
|
||||
};
|
||||
const readSinceIndex = (raw: Record<string, unknown>): number | undefined => {
|
||||
try {
|
||||
return readNonNegativeIntegerParam(raw, "sinceIndex");
|
||||
} catch (error) {
|
||||
return invalidRequest(formatErrorMessage(error));
|
||||
}
|
||||
};
|
||||
const keepTrustedToolContext = (
|
||||
raw: Record<string, unknown>,
|
||||
client: GatewayRequestHandlerOptions["client"],
|
||||
): Record<string, unknown> => {
|
||||
const { agentId: rawAgentId, requesterSessionKey: rawRequesterSessionKey, ...rest } = raw;
|
||||
const trustedOwner = client?.internal?.pluginRuntimeOwnerId === options.id;
|
||||
const agentId = trustedOwner ? normalizeOptionalString(rawAgentId) : undefined;
|
||||
const requesterSessionKey = options.normalizeRequesterSessionKey(
|
||||
rawRequesterSessionKey,
|
||||
trustedOwner,
|
||||
);
|
||||
return {
|
||||
...rest,
|
||||
...(agentId ? { agentId } : {}),
|
||||
...(requesterSessionKey ? { requesterSessionKey } : {}),
|
||||
};
|
||||
};
|
||||
const trustedToolAgentId = (
|
||||
raw: Record<string, unknown>,
|
||||
client: GatewayRequestHandlerOptions["client"],
|
||||
) => normalizeOptionalString(keepTrustedToolContext(raw, client).agentId);
|
||||
const joinRequest = (
|
||||
raw: Record<string, unknown>,
|
||||
joinOptions?: { allowTimeout?: boolean },
|
||||
): Request => {
|
||||
if (!joinOptions?.allowTimeout && raw.timeoutMs !== undefined) {
|
||||
return invalidRequest("timeoutMs is supported only by testSpeech or testListen");
|
||||
}
|
||||
try {
|
||||
return {
|
||||
url: options.normalizeUrl(requireString(raw.url, "url")),
|
||||
transport: normalizeTransport(raw.transport),
|
||||
mode: normalizeMode(raw.mode),
|
||||
message: normalizeOptionalString(raw.message),
|
||||
requesterSessionKey: normalizeOptionalString(raw.requesterSessionKey),
|
||||
agentId: normalizeOptionalString(raw.agentId),
|
||||
timeoutMs: readPositiveIntegerParam(raw, "timeoutMs"),
|
||||
} as Request;
|
||||
} catch (error) {
|
||||
if (options.isInvalidRequest(error)) {
|
||||
throw error;
|
||||
}
|
||||
return invalidRequest(formatErrorMessage(error));
|
||||
}
|
||||
};
|
||||
const gatewayMethod = (action: MeetingToolAction) => `${options.gatewayMethodPrefix}.${action}`;
|
||||
const callGatewayFromTool = async (params: {
|
||||
action: MeetingToolAction;
|
||||
config: Config;
|
||||
raw: Record<string, unknown>;
|
||||
runtime?: OpenClawPluginApi["runtime"];
|
||||
}) => {
|
||||
try {
|
||||
const timeoutMs = options.resolveGatewayTimeoutMs(params.config);
|
||||
if (params.runtime) {
|
||||
return await params.runtime.gateway.request(gatewayMethod(params.action), params.raw, {
|
||||
timeoutMs,
|
||||
scopes: ["operator.admin"],
|
||||
});
|
||||
}
|
||||
return await callGatewayFromCli(
|
||||
gatewayMethod(params.action),
|
||||
{ json: true, timeout: String(timeoutMs) },
|
||||
params.raw,
|
||||
{ progress: false, scopes: ["operator.admin"] },
|
||||
);
|
||||
} catch (error) {
|
||||
const details = readErrorDetails(error);
|
||||
if (details && typeof details === "object") {
|
||||
return details;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
description: options.description,
|
||||
configSchema: options.configSchema,
|
||||
register(api: OpenClawPluginApi) {
|
||||
const config = options.configSchema.parse(api.pluginConfig) as Config;
|
||||
let runtime: Runtime | undefined;
|
||||
const ensureRuntime = async () => {
|
||||
if (!config.enabled) {
|
||||
throw new Error(options.disabledMessage);
|
||||
}
|
||||
runtime ??= options.createRuntime({ api, config });
|
||||
return runtime;
|
||||
};
|
||||
const sendError = (
|
||||
respond: GatewayRequestHandlerOptions["respond"],
|
||||
error: unknown,
|
||||
code: Parameters<typeof errorShape>[0] = ErrorCodes.UNAVAILABLE,
|
||||
) => {
|
||||
const payload = { error: formatErrorMessage(error) };
|
||||
respond(false, payload, errorShape(code, payload.error, { details: payload }));
|
||||
};
|
||||
const sendRequestError = (respond: GatewayRequestHandlerOptions["respond"], error: unknown) =>
|
||||
sendError(
|
||||
respond,
|
||||
error,
|
||||
options.isInvalidRequest(error) ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE,
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
`${options.gatewayMethodPrefix}.join`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = keepTrustedToolContext(asRecord(params), client);
|
||||
respond(true, await (await ensureRuntime()).join(joinRequest(raw)));
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
api.registerGatewayMethod(
|
||||
`${options.gatewayMethodPrefix}.leave`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false }
|
||||
: await rt.leave(sessionId),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
api.registerGatewayMethod(
|
||||
`${options.gatewayMethodPrefix}.status`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId
|
||||
? await rt.statusForAgent(agentId, normalizeOptionalString(raw.sessionId))
|
||||
: await rt.status(normalizeOptionalString(raw.sessionId)),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
api.registerGatewayMethod(
|
||||
`${options.gatewayMethodPrefix}.transcript`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const sinceIndex = readSinceIndex(raw);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false }
|
||||
: await rt.transcript(sessionId, sinceIndex === undefined ? {} : { sinceIndex }),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
api.registerGatewayMethod(
|
||||
`${options.gatewayMethodPrefix}.speak`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
true,
|
||||
agentId && !rt.ownsSession(agentId, sessionId)
|
||||
? { found: false, spoken: false }
|
||||
: await rt.speak(sessionId, normalizeOptionalString(raw.message)),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
api.registerGatewayMethod(
|
||||
`${options.gatewayMethodPrefix}.setup`,
|
||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
respond(
|
||||
true,
|
||||
await (
|
||||
await ensureRuntime()
|
||||
).setupStatus({
|
||||
mode: normalizeMode(params?.mode),
|
||||
transport: normalizeTransport(params?.transport),
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
for (const [method, run] of [
|
||||
[
|
||||
`${options.gatewayMethodPrefix}.testSpeech`,
|
||||
(rt: Runtime, raw: Record<string, unknown>) =>
|
||||
rt.testSpeech(joinRequest(raw, { allowTimeout: true })),
|
||||
],
|
||||
[
|
||||
`${options.gatewayMethodPrefix}.testListen`,
|
||||
(rt: Runtime, raw: Record<string, unknown>) =>
|
||||
rt.testListen(joinRequest(raw, { allowTimeout: true })),
|
||||
],
|
||||
] as const) {
|
||||
api.registerGatewayMethod(
|
||||
method,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = keepTrustedToolContext(asRecord(params), client);
|
||||
respond(true, await run(await ensureRuntime(), raw));
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
api.registerTool(
|
||||
(toolContext) => ({
|
||||
name: options.toolName,
|
||||
label: options.toolLabel,
|
||||
description: options.toolDescription,
|
||||
parameters: options.toolParameters,
|
||||
async execute(_toolCallId, params) {
|
||||
const raw = asRecord(params);
|
||||
const action = raw.action as MeetingToolAction;
|
||||
const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
|
||||
const contextAgentId =
|
||||
toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
|
||||
const agentId = options.normalizeToolAgentId(contextAgentId);
|
||||
try {
|
||||
if (!(["join", "leave", "status", "transcript", "speak"] as const).includes(action)) {
|
||||
throw new Error(options.unknownActionMessage);
|
||||
}
|
||||
const runtimeForTool = await options.resolveToolRuntime(api, agentId);
|
||||
return jsonResult(
|
||||
await callGatewayFromTool({
|
||||
action,
|
||||
config,
|
||||
raw: {
|
||||
...raw,
|
||||
...(requesterSessionKey ? { requesterSessionKey } : {}),
|
||||
...(runtimeForTool && agentId ? { agentId } : {}),
|
||||
},
|
||||
runtime: runtimeForTool,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return jsonResult({ error: formatErrorMessage(error) });
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ name: options.toolName },
|
||||
);
|
||||
if (options.registerNodeWhen(config)) {
|
||||
api.registerNodeHostCommand({
|
||||
command: options.nodeCommand,
|
||||
cap: options.cap,
|
||||
dangerous: true,
|
||||
handle: (paramsJSON) => options.nodeHandler(paramsJSON),
|
||||
});
|
||||
api.registerNodeInvokePolicy(options.createNodePolicy(config));
|
||||
}
|
||||
options.registerCli(api, config);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user