make matrix-js atomic and add poll voting support

This commit is contained in:
Gustavo Madeira Santana
2026-03-04 11:25:08 -05:00
parent eff8c7e15f
commit a4324c45a3
11 changed files with 704 additions and 34 deletions

View File

@@ -0,0 +1,69 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/matrix-js";
import { beforeEach, describe, expect, it } from "vitest";
import { matrixMessageActions } from "./actions.js";
import { setMatrixRuntime } from "./runtime.js";
import type { CoreConfig } from "./types.js";
const runtimeStub = {
config: {
loadConfig: () => ({}),
},
media: {
loadWebMedia: async () => {
throw new Error("not used");
},
mediaKindFromMime: () => "image",
isVoiceCompatibleAudio: () => false,
getImageMetadata: async () => null,
resizeToJpeg: async () => Buffer.from(""),
},
state: {
resolveStateDir: () => "/tmp/openclaw-matrix-js-test",
},
channel: {
text: {
resolveTextChunkLimit: () => 4000,
resolveChunkMode: () => "length",
chunkMarkdownText: (text: string) => (text ? [text] : []),
chunkMarkdownTextWithMode: (text: string) => (text ? [text] : []),
resolveMarkdownTableMode: () => "code",
convertMarkdownTables: (text: string) => text,
},
},
} as unknown as PluginRuntime;
function createConfiguredMatrixJsConfig(): CoreConfig {
return {
channels: {
"matrix-js": {
enabled: true,
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "token",
},
},
} as CoreConfig;
}
describe("matrixMessageActions", () => {
beforeEach(() => {
setMatrixRuntime(runtimeStub);
});
it("exposes poll create but only handles poll votes inside the plugin", () => {
const listActions = matrixMessageActions.listActions;
const supportsAction = matrixMessageActions.supportsAction;
expect(listActions).toBeTypeOf("function");
expect(supportsAction).toBeTypeOf("function");
const actions = listActions!({
cfg: createConfiguredMatrixJsConfig(),
} as never);
expect(actions).toContain("poll");
expect(actions).toContain("poll-vote");
expect(supportsAction!({ action: "poll" } as never)).toBe(false);
expect(supportsAction!({ action: "poll-vote" } as never)).toBe(true);
});
});

View File

@@ -11,6 +11,26 @@ import { resolveMatrixAccount } from "./matrix/accounts.js";
import { handleMatrixAction } from "./tool-actions.js";
import type { CoreConfig } from "./types.js";
const MATRIX_JS_PLUGIN_HANDLED_ACTIONS = new Set<ChannelMessageActionName>([
"send",
"poll-vote",
"react",
"reactions",
"read",
"edit",
"delete",
"pin",
"unpin",
"list-pins",
"member-info",
"channel-info",
"permissions",
]);
function createMatrixJsExposedActions() {
return new Set<ChannelMessageActionName>(["poll", ...MATRIX_JS_PLUGIN_HANDLED_ACTIONS]);
}
export const matrixMessageActions: ChannelMessageActionAdapter = {
listActions: ({ cfg }) => {
const account = resolveMatrixAccount({ cfg: cfg as CoreConfig });
@@ -18,7 +38,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
return [];
}
const gate = createActionGate((cfg as CoreConfig).channels?.["matrix-js"]?.actions);
const actions = new Set<ChannelMessageActionName>(["send", "poll"]);
const actions = createMatrixJsExposedActions();
if (gate("reactions")) {
actions.add("react");
actions.add("reactions");
@@ -44,7 +64,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
}
return Array.from(actions);
},
supportsAction: ({ action }) => action !== "poll",
supportsAction: ({ action }) => MATRIX_JS_PLUGIN_HANDLED_ACTIONS.has(action),
extractToolSend: ({ args }): ChannelToolSend | null => {
const action = typeof args.action === "string" ? args.action.trim() : "";
if (action !== "sendMessage") {
@@ -85,6 +105,16 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
);
}
if (action === "poll-vote") {
return await handleMatrixAction(
{
...params,
action: "pollVote",
},
cfg as CoreConfig,
);
}
if (action === "react") {
const messageId = readStringParam(params, "messageId", { required: true });
const emoji = readStringParam(params, "emoji", { allowEmpty: true });

View File

@@ -9,6 +9,7 @@ export {
deleteMatrixMessage,
readMatrixMessages,
} from "./actions/messages.js";
export { voteMatrixPoll } from "./actions/polls.js";
export { listMatrixReactions, removeMatrixReactions } from "./actions/reactions.js";
export { pinMatrixMessage, unpinMatrixMessage, listMatrixPins } from "./actions/pins.js";
export { getMatrixMemberInfo, getMatrixRoomInfo } from "./actions/room.js";

View File

@@ -0,0 +1,111 @@
import {
buildPollResponseContent,
isPollStartType,
parsePollStart,
type PollStartContent,
} from "../poll-types.js";
import { resolveMatrixRoomId } from "../send.js";
import { withResolvedActionClient } from "./client.js";
import type { MatrixActionClientOpts } from "./types.js";
function normalizeOptionIndexes(indexes: number[]): number[] {
const normalized = indexes
.map((index) => Math.trunc(index))
.filter((index) => Number.isFinite(index) && index > 0);
return Array.from(new Set(normalized));
}
function normalizeOptionIds(optionIds: string[]): string[] {
return Array.from(
new Set(optionIds.map((optionId) => optionId.trim()).filter((optionId) => optionId.length > 0)),
);
}
function resolveSelectedAnswerIds(params: {
optionIds?: string[];
optionIndexes?: number[];
pollContent: PollStartContent;
}): { answerIds: string[]; labels: string[]; maxSelections: number } {
const parsed = parsePollStart(params.pollContent);
if (!parsed) {
throw new Error("Matrix poll vote requires a valid poll start event.");
}
const selectedById = normalizeOptionIds(params.optionIds ?? []);
const selectedByIndex = normalizeOptionIndexes(params.optionIndexes ?? []).map((index) => {
const answer = parsed.answers[index - 1];
if (!answer) {
throw new Error(
`Matrix poll option index ${index} is out of range for a poll with ${parsed.answers.length} options.`,
);
}
return answer.id;
});
const answerIds = normalizeOptionIds([...selectedById, ...selectedByIndex]);
if (answerIds.length === 0) {
throw new Error("Matrix poll vote requires at least one poll option id or index.");
}
if (answerIds.length > parsed.maxSelections) {
throw new Error(
`Matrix poll allows at most ${parsed.maxSelections} selection${parsed.maxSelections === 1 ? "" : "s"}.`,
);
}
const answerMap = new Map(parsed.answers.map((answer) => [answer.id, answer.text] as const));
const labels = answerIds.map((answerId) => {
const label = answerMap.get(answerId);
if (!label) {
throw new Error(
`Matrix poll option id "${answerId}" is not valid for poll ${parsed.question}.`,
);
}
return label;
});
return {
answerIds,
labels,
maxSelections: parsed.maxSelections,
};
}
export async function voteMatrixPoll(
roomId: string,
pollId: string,
opts: MatrixActionClientOpts & {
optionId?: string;
optionIds?: string[];
optionIndex?: number;
optionIndexes?: number[];
} = {},
) {
return await withResolvedActionClient(opts, async (client) => {
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
const pollEvent = await client.getEvent(resolvedRoom, pollId);
const eventType = typeof pollEvent.type === "string" ? pollEvent.type : "";
if (!isPollStartType(eventType)) {
throw new Error(`Event ${pollId} is not a Matrix poll start event.`);
}
const { answerIds, labels, maxSelections } = resolveSelectedAnswerIds({
optionIds: [...(opts.optionIds ?? []), ...(opts.optionId ? [opts.optionId] : [])],
optionIndexes: [
...(opts.optionIndexes ?? []),
...(opts.optionIndex !== undefined ? [opts.optionIndex] : []),
],
pollContent: pollEvent.content as PollStartContent,
});
const content = buildPollResponseContent(pollId, answerIds);
const eventId = await client.sendEvent(resolvedRoom, "m.poll.response", content);
return {
eventId: eventId ?? null,
roomId: resolvedRoom,
pollId,
answerIds,
labels,
maxSelections,
};
});
}

View File

@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { parsePollStartContent } from "./poll-types.js";
import {
buildPollResponseContent,
buildPollStartContent,
parsePollStart,
parsePollStartContent,
} from "./poll-types.js";
describe("parsePollStartContent", () => {
it("parses legacy m.poll payloads", () => {
@@ -18,4 +23,73 @@ describe("parsePollStartContent", () => {
expect(summary?.question).toBe("Lunch?");
expect(summary?.answers).toEqual(["Yes", "No"]);
});
it("preserves answer ids when parsing poll start content", () => {
const parsed = parsePollStart({
"m.poll.start": {
question: { "m.text": "Lunch?" },
kind: "m.poll.disclosed",
max_selections: 1,
answers: [
{ id: "a1", "m.text": "Yes" },
{ id: "a2", "m.text": "No" },
],
},
});
expect(parsed).toMatchObject({
question: "Lunch?",
answers: [
{ id: "a1", text: "Yes" },
{ id: "a2", text: "No" },
],
maxSelections: 1,
});
});
it("caps invalid remote max selections to the available answer count", () => {
const parsed = parsePollStart({
"m.poll.start": {
question: { "m.text": "Lunch?" },
kind: "m.poll.undisclosed",
max_selections: 99,
answers: [
{ id: "a1", "m.text": "Yes" },
{ id: "a2", "m.text": "No" },
],
},
});
expect(parsed?.maxSelections).toBe(2);
});
});
describe("buildPollStartContent", () => {
it("preserves the requested multiselect cap instead of widening to all answers", () => {
const content = buildPollStartContent({
question: "Lunch?",
options: ["Pizza", "Sushi", "Tacos"],
maxSelections: 2,
});
expect(content["m.poll.start"]?.max_selections).toBe(2);
expect(content["m.poll.start"]?.kind).toBe("m.poll.undisclosed");
});
});
describe("buildPollResponseContent", () => {
it("builds a poll response payload with a reference relation", () => {
expect(buildPollResponseContent("$poll", ["a2"])).toEqual({
"m.poll.response": {
answers: ["a2"],
},
"org.matrix.msc3381.poll.response": {
answers: ["a2"],
},
"m.relates_to": {
rel_type: "m.reference",
event_id: "$poll",
},
});
});
});

View File

@@ -7,7 +7,7 @@
* - m.poll.end - Closes a poll
*/
import type { PollInput } from "openclaw/plugin-sdk/matrix-js";
import { normalizePollInput, type PollInput } from "openclaw/plugin-sdk/matrix-js";
export const M_POLL_START = "m.poll.start" as const;
export const M_POLL_RESPONSE = "m.poll.response" as const;
@@ -42,6 +42,11 @@ export type PollAnswer = {
id: string;
} & TextContent;
export type PollParsedAnswer = {
id: string;
text: string;
};
export type PollStartSubtype = {
question: TextContent;
kind?: PollKind;
@@ -72,6 +77,26 @@ export type PollSummary = {
maxSelections: number;
};
export type ParsedPollStart = {
question: string;
answers: PollParsedAnswer[];
kind: PollKind;
maxSelections: number;
};
export type PollResponseSubtype = {
answers: string[];
};
export type PollResponseContent = {
[M_POLL_RESPONSE]?: PollResponseSubtype;
[ORG_POLL_RESPONSE]?: PollResponseSubtype;
"m.relates_to": {
rel_type: "m.reference";
event_id: string;
};
};
export function isPollStartType(eventType: string): boolean {
return (POLL_START_TYPES as readonly string[]).includes(eventType);
}
@@ -83,7 +108,7 @@ export function getTextContent(text?: TextContent): string {
return text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body ?? "";
}
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
export function parsePollStart(content: PollStartContent): ParsedPollStart | null {
const poll =
(content as Record<string, PollStartSubtype | undefined>)[M_POLL_START] ??
(content as Record<string, PollStartSubtype | undefined>)[ORG_POLL_START] ??
@@ -92,24 +117,50 @@ export function parsePollStartContent(content: PollStartContent): PollSummary |
return null;
}
const question = getTextContent(poll.question);
const question = getTextContent(poll.question).trim();
if (!question) {
return null;
}
const answers = poll.answers
.map((answer) => getTextContent(answer))
.filter((a) => a.trim().length > 0);
.map((answer) => ({
id: answer.id,
text: getTextContent(answer).trim(),
}))
.filter((answer) => answer.id.trim().length > 0 && answer.text.length > 0);
if (answers.length === 0) {
return null;
}
const maxSelectionsRaw = poll.max_selections;
const maxSelections =
typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw)
? Math.floor(maxSelectionsRaw)
: 1;
return {
question,
answers,
kind: poll.kind ?? "m.poll.disclosed",
maxSelections: Math.min(Math.max(maxSelections, 1), answers.length),
};
}
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
const parsed = parsePollStart(content);
if (!parsed) {
return null;
}
return {
eventId: "",
roomId: "",
sender: "",
senderName: "",
question,
answers,
kind: poll.kind ?? "m.poll.disclosed",
maxSelections: poll.max_selections ?? 1,
question: parsed.question,
answers: parsed.answers.map((answer) => answer.text),
kind: parsed.kind,
maxSelections: parsed.maxSelections,
};
}
@@ -138,30 +189,44 @@ function buildPollFallbackText(question: string, answers: string[]): string {
}
export function buildPollStartContent(poll: PollInput): PollStartContent {
const question = poll.question.trim();
const answers = poll.options
.map((option) => option.trim())
.filter((option) => option.length > 0)
.map((option, idx) => ({
id: `answer${idx + 1}`,
...buildTextContent(option),
}));
const normalized = normalizePollInput(poll);
const answers = normalized.options.map((option, idx) => ({
id: `answer${idx + 1}`,
...buildTextContent(option),
}));
const isMultiple = (poll.maxSelections ?? 1) > 1;
const maxSelections = isMultiple ? Math.max(1, answers.length) : 1;
const isMultiple = normalized.maxSelections > 1;
const fallbackText = buildPollFallbackText(
question,
normalized.question,
answers.map((answer) => getTextContent(answer)),
);
return {
[M_POLL_START]: {
question: buildTextContent(question),
question: buildTextContent(normalized.question),
kind: isMultiple ? "m.poll.undisclosed" : "m.poll.disclosed",
max_selections: maxSelections,
max_selections: normalized.maxSelections,
answers,
},
"m.text": fallbackText,
"org.matrix.msc1767.text": fallbackText,
};
}
export function buildPollResponseContent(
pollEventId: string,
answerIds: string[],
): PollResponseContent {
return {
[M_POLL_RESPONSE]: {
answers: answerIds,
},
[ORG_POLL_RESPONSE]: {
answers: answerIds,
},
"m.relates_to": {
rel_type: "m.reference",
event_id: pollEventId,
},
};
}

View File

@@ -35,22 +35,28 @@ const runtimeStub = {
} as unknown as PluginRuntime;
let sendMessageMatrix: typeof import("./send.js").sendMessageMatrix;
let voteMatrixPoll: typeof import("./actions/polls.js").voteMatrixPoll;
const makeClient = () => {
const sendMessage = vi.fn().mockResolvedValue("evt1");
const sendEvent = vi.fn().mockResolvedValue("evt-poll-vote");
const getEvent = vi.fn();
const uploadContent = vi.fn().mockResolvedValue("mxc://example/file");
const client = {
sendMessage,
sendEvent,
getEvent,
uploadContent,
getUserId: vi.fn().mockResolvedValue("@bot:example.org"),
} as unknown as import("./sdk.js").MatrixClient;
return { client, sendMessage, uploadContent };
return { client, sendMessage, sendEvent, getEvent, uploadContent };
};
describe("sendMessageMatrix media", () => {
beforeAll(async () => {
setMatrixRuntime(runtimeStub);
({ sendMessageMatrix } = await import("./send.js"));
({ voteMatrixPoll } = await import("./actions/polls.js"));
});
beforeEach(() => {
@@ -196,6 +202,7 @@ describe("sendMessageMatrix threads", () => {
beforeAll(async () => {
setMatrixRuntime(runtimeStub);
({ sendMessageMatrix } = await import("./send.js"));
({ voteMatrixPoll } = await import("./actions/polls.js"));
});
beforeEach(() => {
@@ -226,3 +233,114 @@ describe("sendMessageMatrix threads", () => {
});
});
});
describe("voteMatrixPoll", () => {
beforeAll(async () => {
setMatrixRuntime(runtimeStub);
({ voteMatrixPoll } = await import("./actions/polls.js"));
});
beforeEach(() => {
vi.clearAllMocks();
setMatrixRuntime(runtimeStub);
});
it("maps 1-based option indexes to Matrix poll answer ids", async () => {
const { client, getEvent, sendEvent } = makeClient();
getEvent.mockResolvedValue({
type: "m.poll.start",
content: {
"m.poll.start": {
question: { "m.text": "Lunch?" },
max_selections: 1,
answers: [
{ id: "a1", "m.text": "Pizza" },
{ id: "a2", "m.text": "Sushi" },
],
},
},
});
const result = await voteMatrixPoll("room:!room:example", "$poll", {
client,
optionIndex: 2,
});
expect(sendEvent).toHaveBeenCalledWith("!room:example", "m.poll.response", {
"m.poll.response": { answers: ["a2"] },
"org.matrix.msc3381.poll.response": { answers: ["a2"] },
"m.relates_to": {
rel_type: "m.reference",
event_id: "$poll",
},
});
expect(result).toMatchObject({
eventId: "evt-poll-vote",
roomId: "!room:example",
pollId: "$poll",
answerIds: ["a2"],
labels: ["Sushi"],
});
});
it("rejects out-of-range option indexes", async () => {
const { client, getEvent } = makeClient();
getEvent.mockResolvedValue({
type: "m.poll.start",
content: {
"m.poll.start": {
question: { "m.text": "Lunch?" },
max_selections: 1,
answers: [{ id: "a1", "m.text": "Pizza" }],
},
},
});
await expect(
voteMatrixPoll("room:!room:example", "$poll", {
client,
optionIndex: 2,
}),
).rejects.toThrow("out of range");
});
it("rejects votes that exceed the poll selection cap", async () => {
const { client, getEvent } = makeClient();
getEvent.mockResolvedValue({
type: "m.poll.start",
content: {
"m.poll.start": {
question: { "m.text": "Lunch?" },
max_selections: 1,
answers: [
{ id: "a1", "m.text": "Pizza" },
{ id: "a2", "m.text": "Sushi" },
],
},
},
});
await expect(
voteMatrixPoll("room:!room:example", "$poll", {
client,
optionIndexes: [1, 2],
}),
).rejects.toThrow("at most 1 selection");
});
it("rejects non-poll events before sending a response", async () => {
const { client, getEvent, sendEvent } = makeClient();
getEvent.mockResolvedValue({
type: "m.room.message",
content: { body: "hello" },
});
await expect(
voteMatrixPoll("room:!room:example", "$poll", {
client,
optionIndex: 1,
}),
).rejects.toThrow("is not a Matrix poll start event");
expect(sendEvent).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { handleMatrixAction } from "./tool-actions.js";
import type { CoreConfig } from "./types.js";
const mocks = vi.hoisted(() => ({
voteMatrixPoll: vi.fn(),
reactMatrixMessage: vi.fn(),
}));
vi.mock("./matrix/actions.js", async () => {
const actual = await vi.importActual<typeof import("./matrix/actions.js")>("./matrix/actions.js");
return {
...actual,
voteMatrixPoll: mocks.voteMatrixPoll,
};
});
vi.mock("./matrix/send.js", async () => {
const actual = await vi.importActual<typeof import("./matrix/send.js")>("./matrix/send.js");
return {
...actual,
reactMatrixMessage: mocks.reactMatrixMessage,
};
});
describe("handleMatrixAction pollVote", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.voteMatrixPoll.mockResolvedValue({
eventId: "evt-poll-vote",
roomId: "!room:example",
pollId: "$poll",
answerIds: ["a1", "a2"],
labels: ["Pizza", "Sushi"],
maxSelections: 2,
});
});
it("parses snake_case vote params and forwards normalized selectors", async () => {
const result = await handleMatrixAction(
{
action: "pollVote",
account_id: "main",
room_id: "!room:example",
poll_id: "$poll",
poll_option_id: "a1",
poll_option_ids: ["a2", ""],
poll_option_index: "2",
poll_option_indexes: ["1", "bogus"],
},
{} as CoreConfig,
);
expect(mocks.voteMatrixPoll).toHaveBeenCalledWith("!room:example", "$poll", {
accountId: "main",
optionIds: ["a2", "a1"],
optionIndexes: [1, 2],
});
expect(result.details).toMatchObject({
ok: true,
result: {
eventId: "evt-poll-vote",
answerIds: ["a1", "a2"],
},
});
});
it("rejects missing poll ids", async () => {
await expect(
handleMatrixAction(
{
action: "pollVote",
roomId: "!room:example",
pollOptionIndex: 1,
},
{} as CoreConfig,
),
).rejects.toThrow("pollId required");
});
});

View File

@@ -2,8 +2,10 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import {
createActionGate,
jsonResult,
readNumberArrayParam,
readNumberParam,
readReactionParams,
readStringArrayParam,
readStringParam,
} from "openclaw/plugin-sdk/matrix-js";
import {
@@ -34,6 +36,7 @@ import {
sendMatrixMessage,
startMatrixVerification,
unpinMatrixMessage,
voteMatrixPoll,
verifyMatrixRecoveryKey,
} from "./matrix/actions.js";
import { reactMatrixMessage } from "./matrix/send.js";
@@ -42,6 +45,7 @@ import type { CoreConfig } from "./types.js";
const messageActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]);
const reactionActions = new Set(["react", "reactions"]);
const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]);
const pollActions = new Set(["pollVote"]);
const verificationActions = new Set([
"encryptionStatus",
"verificationList",
@@ -104,6 +108,27 @@ export async function handleMatrixAction(
return jsonResult({ ok: true, reactions });
}
if (pollActions.has(action)) {
const roomId = readRoomId(params);
const pollId = readStringParam(params, "pollId", { required: true });
const optionId = readStringParam(params, "pollOptionId");
const optionIndex = readNumberParam(params, "pollOptionIndex", { integer: true });
const optionIds = [
...(readStringArrayParam(params, "pollOptionIds") ?? []),
...(optionId ? [optionId] : []),
];
const optionIndexes = [
...(readNumberArrayParam(params, "pollOptionIndexes", { integer: true }) ?? []),
...(optionIndex !== undefined ? [optionIndex] : []),
];
const result = await voteMatrixPoll(roomId, pollId, {
accountId,
optionIds,
optionIndexes,
});
return jsonResult({ ok: true, result });
}
if (messageActions.has(action)) {
if (!isActionEnabled("messages")) {
throw new Error("Matrix messages are disabled.");

View File

@@ -1,11 +1,105 @@
// Matrix-js plugin-sdk surface.
// Reuse matrix exports to avoid drift between the two Matrix channel implementations.
// Narrow plugin-sdk surface for the bundled matrix-js plugin.
// Keep this list additive and scoped to symbols used under extensions/matrix-js.
export * from "./matrix.js";
export type { ChannelSetupInput } from "../channels/plugins/types.js";
export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js";
export {
createActionGate,
jsonResult,
readNumberArrayParam,
readNumberParam,
readReactionParams,
readStringArrayParam,
readStringParam,
} from "../agents/tools/common.js";
export type { ReplyPayload } from "../auto-reply/types.js";
export { resolveAllowlistMatchByCandidates } from "../channels/allowlist-match.js";
export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js";
export { resolveControlCommandGate } from "../channels/command-gating.js";
export type { NormalizedLocation } from "../channels/location.js";
export { formatLocationText, toLocationContext } from "../channels/location.js";
export { logInboundDrop, logTypingFailure } from "../channels/logging.js";
export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js";
export { formatAllowlistMatchMeta } from "../channels/plugins/allowlist-match.js";
export {
buildChannelKeyCandidates,
resolveChannelEntryMatch,
} from "../channels/plugins/channel-config.js";
export {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../channels/plugins/config-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../channels/plugins/onboarding-types.js";
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
export {
addWildcardAllowFrom,
mergeAllowFromEntries,
promptSingleChannelSecretInput,
} from "../channels/plugins/onboarding/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export { applyAccountNameToChannelSection } from "../channels/plugins/setup-helpers.js";
export { migrateBaseNameToDefaultAccount } from "../channels/plugins/setup-helpers.js";
export { promptAccountId } from "../channels/plugins/onboarding/helpers.js";
export { writeJsonFileAtomically } from "./json-store.js";
export type {
BaseProbeResult,
ChannelDirectoryEntry,
ChannelGroupContext,
ChannelMessageActionAdapter,
ChannelMessageActionContext,
ChannelMessageActionName,
ChannelOutboundAdapter,
ChannelResolveKind,
ChannelResolveResult,
ChannelToolSend,
} from "../channels/plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { ChannelSetupInput } from "../channels/plugins/types.js";
export { createReplyPrefixOptions } from "../channels/reply-prefix.js";
export { createTypingCallbacks } from "../channels/typing.js";
export type { OpenClawConfig } from "../config/config.js";
export {
GROUP_POLICY_BLOCKED_LABEL,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "../config/runtime-group-policy.js";
export type {
DmPolicy,
GroupPolicy,
GroupToolPolicyConfig,
MarkdownTableMode,
} from "../config/types.js";
export type { SecretInput } from "../config/types.secrets.js";
export {
hasConfiguredSecretInput,
normalizeResolvedSecretInputString,
normalizeSecretInputString,
} from "../config/types.secrets.js";
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
export { MarkdownConfigSchema } from "../config/zod-schema.core.js";
export { formatZonedTimestamp } from "../infra/format-time/format-datetime.js";
export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export type { PollInput } from "../polls.js";
export { normalizePollInput } from "../polls.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export type { RuntimeEnv } from "../runtime.js";
export {
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../security/dm-policy-shared.js";
export { formatDocsLink } from "../terminal/links.js";
export type { WizardPrompter } from "../wizard/prompts.js";
export { createScopedPairingAccess } from "./pairing-access.js";
export { writeJsonFileAtomically } from "./json-store.js";
export { formatResolvedUnresolvedNote } from "./resolution-notes.js";
export { runPluginCommandWithTimeout } from "./run-command.js";
export { createLoggerBackedRuntime } from "./runtime.js";
export { buildProbeChannelStatusSummary } from "./status-helpers.js";
export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js";
export { promptAccountId } from "../channels/plugins/onboarding/helpers.js";

View File

@@ -4,8 +4,10 @@
export {
createActionGate,
jsonResult,
readNumberArrayParam,
readNumberParam,
readReactionParams,
readStringArrayParam,
readStringParam,
} from "../agents/tools/common.js";
export type { ReplyPayload } from "../auto-reply/types.js";
@@ -89,6 +91,7 @@ export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export type { PollInput } from "../polls.js";
export { normalizePollInput } from "../polls.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export type { RuntimeEnv } from "../runtime.js";
export {