refactor: centralize channel ui metadata
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
CHANNEL_MESSAGE_ACTION_NAMES,
|
||||
type ChannelMessageActionName,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import { BLUEBUBBLES_GROUP_ACTIONS } from "../../channels/plugins/bluebubbles-actions.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
@@ -25,14 +26,6 @@ import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
|
||||
|
||||
const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
|
||||
const BLUEBUBBLES_GROUP_ACTIONS = new Set<ChannelMessageActionName>([
|
||||
"renameGroup",
|
||||
"setGroupIcon",
|
||||
"addParticipant",
|
||||
"removeParticipant",
|
||||
"leaveGroup",
|
||||
]);
|
||||
|
||||
function buildRoutingSchema() {
|
||||
return {
|
||||
channel: Type.Optional(Type.String()),
|
||||
|
||||
29
src/channels/plugins/bluebubbles-actions.ts
Normal file
29
src/channels/plugins/bluebubbles-actions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ChannelMessageActionName } from "./types.js";
|
||||
|
||||
export type BlueBubblesActionSpec = {
|
||||
gate: string;
|
||||
groupOnly?: boolean;
|
||||
unsupportedOnMacOS26?: boolean;
|
||||
};
|
||||
|
||||
export const BLUEBUBBLES_ACTIONS = {
|
||||
react: { gate: "reactions" },
|
||||
edit: { gate: "edit", unsupportedOnMacOS26: true },
|
||||
unsend: { gate: "unsend" },
|
||||
reply: { gate: "reply" },
|
||||
sendWithEffect: { gate: "sendWithEffect" },
|
||||
renameGroup: { gate: "renameGroup", groupOnly: true },
|
||||
setGroupIcon: { gate: "setGroupIcon", groupOnly: true },
|
||||
addParticipant: { gate: "addParticipant", groupOnly: true },
|
||||
removeParticipant: { gate: "removeParticipant", groupOnly: true },
|
||||
leaveGroup: { gate: "leaveGroup", groupOnly: true },
|
||||
sendAttachment: { gate: "sendAttachment" },
|
||||
} as const satisfies Partial<Record<ChannelMessageActionName, BlueBubblesActionSpec>>;
|
||||
|
||||
export const BLUEBUBBLES_ACTION_NAMES = Object.keys(
|
||||
BLUEBUBBLES_ACTIONS,
|
||||
) as ChannelMessageActionName[];
|
||||
|
||||
export const BLUEBUBBLES_GROUP_ACTIONS = new Set<ChannelMessageActionName>(
|
||||
BLUEBUBBLES_ACTION_NAMES.filter((action) => BLUEBUBBLES_ACTIONS[action]?.groupOnly),
|
||||
);
|
||||
@@ -5,6 +5,22 @@ import type { PluginOrigin } from "../../plugins/types.js";
|
||||
import type { ClawdbotPackageManifest } from "../../plugins/manifest.js";
|
||||
import type { ChannelMeta } from "./types.js";
|
||||
|
||||
export type ChannelUiMetaEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
detailLabel: string;
|
||||
systemImage?: string;
|
||||
};
|
||||
|
||||
export type ChannelUiCatalog = {
|
||||
entries: ChannelUiMetaEntry[];
|
||||
order: string[];
|
||||
labels: Record<string, string>;
|
||||
detailLabels: Record<string, string>;
|
||||
systemImages: Record<string, string>;
|
||||
byId: Record<string, ChannelUiMetaEntry>;
|
||||
};
|
||||
|
||||
export type ChannelPluginCatalogEntry = {
|
||||
id: string;
|
||||
meta: ChannelMeta;
|
||||
@@ -116,6 +132,34 @@ function buildCatalogEntry(candidate: {
|
||||
return { id, meta, install };
|
||||
}
|
||||
|
||||
export function buildChannelUiCatalog(
|
||||
plugins: Array<{ id: string; meta: ChannelMeta }>,
|
||||
): ChannelUiCatalog {
|
||||
const entries: ChannelUiMetaEntry[] = plugins.map((plugin) => {
|
||||
const detailLabel = plugin.meta.detailLabel ?? plugin.meta.selectionLabel ?? plugin.meta.label;
|
||||
return {
|
||||
id: plugin.id,
|
||||
label: plugin.meta.label,
|
||||
detailLabel,
|
||||
...(plugin.meta.systemImage ? { systemImage: plugin.meta.systemImage } : {}),
|
||||
};
|
||||
});
|
||||
const order = entries.map((entry) => entry.id);
|
||||
const labels: Record<string, string> = {};
|
||||
const detailLabels: Record<string, string> = {};
|
||||
const systemImages: Record<string, string> = {};
|
||||
const byId: Record<string, ChannelUiMetaEntry> = {};
|
||||
for (const entry of entries) {
|
||||
labels[entry.id] = entry.label;
|
||||
detailLabels[entry.id] = entry.detailLabel;
|
||||
if (entry.systemImage) {
|
||||
systemImages[entry.id] = entry.systemImage;
|
||||
}
|
||||
byId[entry.id] = entry;
|
||||
}
|
||||
return { entries, order, labels, detailLabels, systemImages, byId };
|
||||
}
|
||||
|
||||
export function listChannelPluginCatalogEntries(
|
||||
options: CatalogOptions = {},
|
||||
): ChannelPluginCatalogEntry[] {
|
||||
|
||||
@@ -55,6 +55,16 @@ export const ChannelAccountSnapshotSchema = Type.Object(
|
||||
{ additionalProperties: true },
|
||||
);
|
||||
|
||||
export const ChannelUiMetaSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
label: NonEmptyString,
|
||||
detailLabel: NonEmptyString,
|
||||
systemImage: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ChannelsStatusResultSchema = Type.Object(
|
||||
{
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
@@ -62,6 +72,7 @@ export const ChannelsStatusResultSchema = Type.Object(
|
||||
channelLabels: Type.Record(NonEmptyString, NonEmptyString),
|
||||
channelDetailLabels: Type.Optional(Type.Record(NonEmptyString, NonEmptyString)),
|
||||
channelSystemImages: Type.Optional(Type.Record(NonEmptyString, NonEmptyString)),
|
||||
channelMeta: Type.Optional(Type.Array(ChannelUiMetaSchema)),
|
||||
channels: Type.Record(NonEmptyString, Type.Unknown()),
|
||||
channelAccounts: Type.Record(NonEmptyString, Type.Array(ChannelAccountSnapshotSchema)),
|
||||
channelDefaultAccountId: Type.Record(NonEmptyString, NonEmptyString),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
listChannelPlugins,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { buildChannelUiCatalog } from "../../channels/plugins/catalog.js";
|
||||
import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js";
|
||||
import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
@@ -188,24 +189,14 @@ export const channelsHandlers: GatewayRequestHandlers = {
|
||||
return { accounts, defaultAccountId, defaultAccount, resolvedAccounts };
|
||||
};
|
||||
|
||||
const channelLabels = Object.fromEntries(plugins.map((plugin) => [plugin.id, plugin.meta.label]));
|
||||
const channelDetailLabels = Object.fromEntries(
|
||||
plugins.map((plugin) => [
|
||||
plugin.id,
|
||||
plugin.meta.detailLabel ?? plugin.meta.selectionLabel ?? plugin.meta.label,
|
||||
]),
|
||||
);
|
||||
const channelSystemImages = Object.fromEntries(
|
||||
plugins.flatMap((plugin) =>
|
||||
plugin.meta.systemImage ? [[plugin.id, plugin.meta.systemImage]] : [],
|
||||
),
|
||||
);
|
||||
const uiCatalog = buildChannelUiCatalog(plugins);
|
||||
const payload: Record<string, unknown> = {
|
||||
ts: Date.now(),
|
||||
channelOrder: plugins.map((plugin) => plugin.id),
|
||||
channelLabels,
|
||||
channelDetailLabels,
|
||||
channelSystemImages,
|
||||
channelOrder: uiCatalog.order,
|
||||
channelLabels: uiCatalog.labels,
|
||||
channelDetailLabels: uiCatalog.detailLabels,
|
||||
channelSystemImages: uiCatalog.systemImages,
|
||||
channelMeta: uiCatalog.entries,
|
||||
channels: {} as Record<string, unknown>,
|
||||
channelAccounts: {} as Record<string, unknown>,
|
||||
channelDefaultAccountId: {} as Record<string, unknown>,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export { CHANNEL_MESSAGE_ACTION_NAMES } from "../channels/plugins/message-action-names.js";
|
||||
export {
|
||||
BLUEBUBBLES_ACTIONS,
|
||||
BLUEBUBBLES_ACTION_NAMES,
|
||||
BLUEBUBBLES_GROUP_ACTIONS,
|
||||
} from "../channels/plugins/bluebubbles-actions.js";
|
||||
export type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelAccountState,
|
||||
|
||||
@@ -106,6 +106,7 @@ import { loginWeb } from "../../web/login.js";
|
||||
import { startWebLoginWithQr, waitForWebLogin } from "../../web/login-qr.js";
|
||||
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";
|
||||
import { registerMemoryCli } from "../../cli/memory-cli.js";
|
||||
import { formatNativeDependencyHint } from "./native-deps.js";
|
||||
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
@@ -134,6 +135,7 @@ export function createPluginRuntime(): PluginRuntime {
|
||||
system: {
|
||||
enqueueSystemEvent,
|
||||
runCommandWithTimeout,
|
||||
formatNativeDependencyHint,
|
||||
},
|
||||
media: {
|
||||
loadWebMedia,
|
||||
|
||||
28
src/plugins/runtime/native-deps.ts
Normal file
28
src/plugins/runtime/native-deps.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export type NativeDependencyHintParams = {
|
||||
packageName: string;
|
||||
manager?: "pnpm" | "npm" | "yarn";
|
||||
rebuildCommand?: string;
|
||||
approveBuildsCommand?: string;
|
||||
downloadCommand?: string;
|
||||
};
|
||||
|
||||
export function formatNativeDependencyHint(params: NativeDependencyHintParams): string {
|
||||
const manager = params.manager ?? "pnpm";
|
||||
const rebuildCommand =
|
||||
params.rebuildCommand ??
|
||||
(manager === "npm"
|
||||
? `npm rebuild ${params.packageName}`
|
||||
: manager === "yarn"
|
||||
? `yarn rebuild ${params.packageName}`
|
||||
: `pnpm rebuild ${params.packageName}`);
|
||||
const approveBuildsCommand =
|
||||
params.approveBuildsCommand ??
|
||||
(manager === "pnpm" ? `pnpm approve-builds (select ${params.packageName})` : undefined);
|
||||
const steps = [approveBuildsCommand, rebuildCommand, params.downloadCommand].filter(
|
||||
(step): step is string => Boolean(step),
|
||||
);
|
||||
if (steps.length === 0) {
|
||||
return `Install ${params.packageName} and rebuild its native module.`;
|
||||
}
|
||||
return `Install ${params.packageName} and rebuild its native module (${steps.join("; ")}).`;
|
||||
}
|
||||
@@ -59,6 +59,7 @@ type RecordChannelActivity = typeof import("../../infra/channel-activity.js").re
|
||||
type GetChannelActivity = typeof import("../../infra/channel-activity.js").getChannelActivity;
|
||||
type EnqueueSystemEvent = typeof import("../../infra/system-events.js").enqueueSystemEvent;
|
||||
type RunCommandWithTimeout = typeof import("../../process/exec.js").runCommandWithTimeout;
|
||||
type FormatNativeDependencyHint = typeof import("./native-deps.js").formatNativeDependencyHint;
|
||||
type LoadWebMedia = typeof import("../../web/media.js").loadWebMedia;
|
||||
type DetectMime = typeof import("../../media/mime.js").detectMime;
|
||||
type MediaKindFromMime = typeof import("../../media/constants.js").mediaKindFromMime;
|
||||
@@ -146,6 +147,7 @@ export type PluginRuntime = {
|
||||
system: {
|
||||
enqueueSystemEvent: EnqueueSystemEvent;
|
||||
runCommandWithTimeout: RunCommandWithTimeout;
|
||||
formatNativeDependencyHint: FormatNativeDependencyHint;
|
||||
};
|
||||
media: {
|
||||
loadWebMedia: LoadWebMedia;
|
||||
|
||||
Reference in New Issue
Block a user