mirror of https://github.com/openclaw/openclaw.git
refactor(channels): move bootstrap channel logic behind extension seams
This commit is contained in:
parent
fff7e610df
commit
bc457fd1b8
|
|
@ -1,4 +1,5 @@
|
|||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import { collectBlueBubblesStatusIssues } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
||||
|
|
@ -9,7 +10,6 @@ import {
|
|||
import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
|
||||
import {
|
||||
buildProbeChannelStatusSummary,
|
||||
collectBlueBubblesStatusIssues,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "openclaw/plugin-sdk/channel-status";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
|
|
|
|||
|
|
@ -8,3 +8,10 @@ export {
|
|||
collectUnsupportedSecretRefConfigCandidates,
|
||||
} from "./src/security-contract.js";
|
||||
export { deriveLegacySessionChatType } from "./src/session-contract.js";
|
||||
|
||||
export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
return (
|
||||
typeof params.env?.DISCORD_BOT_TOKEN === "string" &&
|
||||
params.env.DISCORD_BOT_TOKEN.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
return (
|
||||
typeof params.env?.IRC_HOST === "string" &&
|
||||
params.env.IRC_HOST.trim().length > 0 &&
|
||||
typeof params.env?.IRC_NICK === "string" &&
|
||||
params.env.IRC_NICK.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
|
@ -3,3 +3,9 @@ export {
|
|||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
|
||||
export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
return ["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some(
|
||||
(key) => typeof params.env?.[key] === "string" && params.env[key]?.trim().length > 0,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,18 @@ export {
|
|||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
export {
|
||||
TELEGRAM_COMMAND_NAME_PATTERN,
|
||||
normalizeTelegramCommandDescription,
|
||||
normalizeTelegramCommandName,
|
||||
resolveTelegramCustomCommands,
|
||||
} from "./src/command-config.js";
|
||||
export { parseTelegramTopicConversation } from "./src/topic-conversation.js";
|
||||
export { singleAccountKeysToMove } from "./src/setup-contract.js";
|
||||
export { buildTelegramModelsProviderChannelData } from "./src/command-ui.js";
|
||||
export {
|
||||
buildCommandsPaginationKeyboard,
|
||||
buildTelegramModelsProviderChannelData,
|
||||
} from "./src/command-ui.js";
|
||||
export type {
|
||||
TelegramInteractiveHandlerContext,
|
||||
TelegramInteractiveHandlerRegistration,
|
||||
|
|
|
|||
|
|
@ -4,3 +4,10 @@ export {
|
|||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
export { singleAccountKeysToMove } from "./src/setup-contract.js";
|
||||
|
||||
export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
return (
|
||||
typeof params.env?.TELEGRAM_BOT_TOKEN === "string" &&
|
||||
params.env.TELEGRAM_BOT_TOKEN.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ type UnsupportedSecretRefConfigCandidate = {
|
|||
value: unknown;
|
||||
};
|
||||
|
||||
import { hasAnyWhatsAppAuth } from "./src/accounts.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
|
@ -14,6 +16,12 @@ export const unsupportedSecretRefSurfacePatterns = [
|
|||
|
||||
export { resolveLegacyGroupSessionKey } from "./src/group-session-contract.js";
|
||||
|
||||
export function hasPersistedAuthState(params: {
|
||||
cfg: import("openclaw/plugin-sdk/config-runtime").OpenClawConfig;
|
||||
}): boolean {
|
||||
return hasAnyWhatsAppAuth(params.cfg);
|
||||
}
|
||||
|
||||
export function collectUnsupportedSecretRefConfigCandidates(
|
||||
raw: unknown,
|
||||
): UnsupportedSecretRefConfigCandidate[] {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function buildChatChannelMetaById(): Record<ChatChannelId, ChatChannelMeta> {
|
|||
if (!rawId || !CHAT_CHANNEL_ID_SET.has(rawId)) {
|
||||
continue;
|
||||
}
|
||||
const id = rawId as ChatChannelId;
|
||||
const id = rawId;
|
||||
entries.set(
|
||||
id,
|
||||
toChatChannelMeta({
|
||||
|
|
@ -89,11 +89,6 @@ function buildChatChannelMetaById(): Record<ChatChannelId, ChatChannelMeta> {
|
|||
);
|
||||
}
|
||||
|
||||
const missingIds = CHAT_CHANNEL_ORDER.filter((id) => !entries.has(id));
|
||||
if (missingIds.length > 0) {
|
||||
throw new Error(`Missing bundled chat channel metadata for: ${missingIds.join(", ")}`);
|
||||
}
|
||||
|
||||
return Object.freeze(Object.fromEntries(entries)) as Record<ChatChannelId, ChatChannelMeta>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +1,66 @@
|
|||
// Keep built-in channel IDs in a leaf module so shared config/sandbox code can
|
||||
// reference them without importing channel registry helpers that may pull in
|
||||
// plugin runtime state.
|
||||
export const CHAT_CHANNEL_ORDER = [
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
"discord",
|
||||
"irc",
|
||||
"googlechat",
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
"line",
|
||||
] as const;
|
||||
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||
|
||||
export type ChatChannelId = (typeof CHAT_CHANNEL_ORDER)[number];
|
||||
export type ChatChannelId = string;
|
||||
|
||||
export const CHANNEL_IDS = [...CHAT_CHANNEL_ORDER] as const;
|
||||
|
||||
const BUILT_IN_CHAT_CHANNEL_ALIAS_ENTRIES = [
|
||||
["gchat", "googlechat"],
|
||||
["google-chat", "googlechat"],
|
||||
["imsg", "imessage"],
|
||||
["internet-relay-chat", "irc"],
|
||||
] as const satisfies ReadonlyArray<readonly [string, ChatChannelId]>;
|
||||
|
||||
export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = Object.freeze(
|
||||
Object.fromEntries(BUILT_IN_CHAT_CHANNEL_ALIAS_ENTRIES),
|
||||
) as Record<string, ChatChannelId>;
|
||||
type BundledChatChannelEntry = {
|
||||
id: ChatChannelId;
|
||||
aliases: readonly string[];
|
||||
order: number;
|
||||
};
|
||||
|
||||
function normalizeChannelKey(raw?: string | null): string | undefined {
|
||||
const normalized = raw?.trim().toLowerCase();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function listBundledChatChannelEntries(): BundledChatChannelEntry[] {
|
||||
return listBundledPluginMetadata({
|
||||
includeChannelConfigs: false,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
})
|
||||
.flatMap((entry) => {
|
||||
const channel =
|
||||
entry.packageManifest && "channel" in entry.packageManifest
|
||||
? entry.packageManifest.channel
|
||||
: undefined;
|
||||
const id = normalizeChannelKey(channel?.id);
|
||||
if (!channel || !id) {
|
||||
return [];
|
||||
}
|
||||
const aliases = (channel.aliases ?? [])
|
||||
.map((alias) => normalizeChannelKey(alias))
|
||||
.filter((alias): alias is string => Boolean(alias));
|
||||
return [
|
||||
{
|
||||
id,
|
||||
aliases,
|
||||
order: typeof channel.order === "number" ? channel.order : Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
];
|
||||
})
|
||||
.toSorted(
|
||||
(left, right) =>
|
||||
left.order - right.order || left.id.localeCompare(right.id, "en", { sensitivity: "base" }),
|
||||
);
|
||||
}
|
||||
|
||||
const BUNDLED_CHAT_CHANNEL_ENTRIES = Object.freeze(listBundledChatChannelEntries());
|
||||
const CHAT_CHANNEL_ID_SET = new Set(BUNDLED_CHAT_CHANNEL_ENTRIES.map((entry) => entry.id));
|
||||
|
||||
export const CHAT_CHANNEL_ORDER = Object.freeze(
|
||||
BUNDLED_CHAT_CHANNEL_ENTRIES.map((entry) => entry.id),
|
||||
);
|
||||
|
||||
export const CHANNEL_IDS = CHAT_CHANNEL_ORDER;
|
||||
|
||||
export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = Object.freeze(
|
||||
Object.fromEntries(
|
||||
BUNDLED_CHAT_CHANNEL_ENTRIES.flatMap((entry) =>
|
||||
entry.aliases.map((alias) => [alias, entry.id] as const),
|
||||
),
|
||||
),
|
||||
) as Record<string, ChatChannelId>;
|
||||
|
||||
export function listChatChannelAliases(): string[] {
|
||||
return Object.keys(CHAT_CHANNEL_ALIASES);
|
||||
}
|
||||
|
|
@ -43,5 +71,5 @@ export function normalizeChatChannelId(raw?: string | null): ChatChannelId | nul
|
|||
return null;
|
||||
}
|
||||
const resolved = CHAT_CHANNEL_ALIASES[normalized] ?? normalized;
|
||||
return CHAT_CHANNEL_ORDER.includes(resolved) ? resolved : null;
|
||||
return CHAT_CHANNEL_ID_SET.has(resolved) ? resolved : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { parseFeishuConversationId } from "../plugin-sdk/feishu-conversation.js";
|
||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
import {
|
||||
buildChannelKeyCandidates,
|
||||
|
|
@ -59,10 +58,6 @@ function buildChannelCandidates(
|
|||
normalizeMessageChannel(params.channel ?? "") ?? params.channel?.trim().toLowerCase();
|
||||
const groupId = params.groupId?.trim();
|
||||
const sessionConversation = resolveSessionConversationRef(params.parentSessionKey);
|
||||
const bundledParentOverrideFallbacks = resolveBundledParentOverrideFallbacks({
|
||||
channel: normalizedChannel,
|
||||
parentConversationId: sessionConversation?.rawId,
|
||||
});
|
||||
const parentOverrideFallbacks =
|
||||
(normalizedChannel
|
||||
? getChannelPlugin(
|
||||
|
|
@ -70,7 +65,7 @@ function buildChannelCandidates(
|
|||
)?.conversationBindings?.buildModelOverrideParentCandidates?.({
|
||||
parentConversationId: sessionConversation?.rawId,
|
||||
})
|
||||
: null) ?? bundledParentOverrideFallbacks;
|
||||
: null) ?? [];
|
||||
const groupConversationKind =
|
||||
normalizeChatType(params.groupChatType ?? undefined) === "channel"
|
||||
? "channel"
|
||||
|
|
@ -108,34 +103,6 @@ function buildChannelCandidates(
|
|||
};
|
||||
}
|
||||
|
||||
function resolveBundledParentOverrideFallbacks(params: {
|
||||
channel?: string | null;
|
||||
parentConversationId?: string | null;
|
||||
}): string[] {
|
||||
if (params.channel !== "feishu") {
|
||||
return [];
|
||||
}
|
||||
const parsed = parseFeishuConversationId({
|
||||
conversationId: params.parentConversationId ?? "",
|
||||
});
|
||||
if (!parsed) {
|
||||
return [];
|
||||
}
|
||||
switch (parsed.scope) {
|
||||
case "group_topic_sender":
|
||||
return buildChannelKeyCandidates(
|
||||
parsed.topicId ? `${parsed.chatId}:topic:${parsed.topicId}` : undefined,
|
||||
parsed.chatId,
|
||||
);
|
||||
case "group_topic":
|
||||
case "group_sender":
|
||||
return buildChannelKeyCandidates(parsed.chatId);
|
||||
case "group":
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveChannelModelOverride(
|
||||
params: ChannelModelOverrideParams,
|
||||
): ChannelModelOverride | null {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import { discoverOpenClawPlugins } from "../../plugins/discovery.js";
|
||||
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
buildPluginLoaderJitiOptions,
|
||||
resolveLoaderPackageRoot,
|
||||
shouldPreferNativeJiti,
|
||||
} from "../../plugins/sdk-alias.js";
|
||||
|
||||
|
|
@ -15,12 +17,25 @@ const CONTRACT_SURFACE_BASENAMES = [
|
|||
"contract-api.ts",
|
||||
"contract-api.js",
|
||||
] as const;
|
||||
const PUBLIC_SURFACE_SOURCE_EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"] as const;
|
||||
const OPENCLAW_PACKAGE_ROOT =
|
||||
resolveLoaderPackageRoot({
|
||||
modulePath: fileURLToPath(import.meta.url),
|
||||
moduleUrl: import.meta.url,
|
||||
}) ?? fileURLToPath(new URL("../../..", import.meta.url));
|
||||
const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url);
|
||||
const RUNNING_FROM_BUILT_ARTIFACT =
|
||||
CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) ||
|
||||
CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`);
|
||||
|
||||
type ContractSurfaceBasename = (typeof CONTRACT_SURFACE_BASENAMES)[number];
|
||||
|
||||
let cachedSurfaces: unknown[] | null = null;
|
||||
let cachedSurfaceEntries: Array<{
|
||||
pluginId: string;
|
||||
surface: unknown;
|
||||
}> | null = null;
|
||||
const cachedPreferredSurfaceModules = new Map<string, unknown>();
|
||||
|
||||
function createModuleLoader() {
|
||||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
|
|
@ -46,28 +61,87 @@ function createModuleLoader() {
|
|||
|
||||
const loadModule = createModuleLoader();
|
||||
|
||||
function resolveContractSurfaceModulePaths(rootDir: string | undefined): string[] {
|
||||
function matchesPreferredBasename(
|
||||
basename: ContractSurfaceBasename,
|
||||
preferredBasename: ContractSurfaceBasename | undefined,
|
||||
): boolean {
|
||||
if (!preferredBasename) {
|
||||
return true;
|
||||
}
|
||||
return basename.replace(/\.[^.]+$/u, "") === preferredBasename.replace(/\.[^.]+$/u, "");
|
||||
}
|
||||
|
||||
function resolveDistPreferredModulePath(modulePath: string): string {
|
||||
const compiledDistModulePath = modulePath.replace(
|
||||
`${path.sep}dist-runtime${path.sep}`,
|
||||
`${path.sep}dist${path.sep}`,
|
||||
);
|
||||
return compiledDistModulePath !== modulePath && fs.existsSync(compiledDistModulePath)
|
||||
? compiledDistModulePath
|
||||
: modulePath;
|
||||
}
|
||||
|
||||
function resolveContractSurfaceModulePaths(
|
||||
rootDir: string | undefined,
|
||||
preferredBasename?: ContractSurfaceBasename,
|
||||
): string[] {
|
||||
if (typeof rootDir !== "string" || rootDir.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const modulePaths: string[] = [];
|
||||
for (const basename of CONTRACT_SURFACE_BASENAMES) {
|
||||
if (!matchesPreferredBasename(basename, preferredBasename)) {
|
||||
continue;
|
||||
}
|
||||
const modulePath = path.join(rootDir, basename);
|
||||
if (!fs.existsSync(modulePath)) {
|
||||
continue;
|
||||
}
|
||||
const compiledDistModulePath = modulePath.replace(
|
||||
`${path.sep}dist-runtime${path.sep}`,
|
||||
`${path.sep}dist${path.sep}`,
|
||||
);
|
||||
// Prefer the compiled dist module over the dist-runtime shim so Jiti sees
|
||||
// the full named export surface instead of only local wrapper exports.
|
||||
if (compiledDistModulePath !== modulePath && fs.existsSync(compiledDistModulePath)) {
|
||||
modulePaths.push(compiledDistModulePath);
|
||||
modulePaths.push(resolveDistPreferredModulePath(modulePath));
|
||||
}
|
||||
return modulePaths;
|
||||
}
|
||||
|
||||
function resolveSourceFirstContractSurfaceModulePaths(params: {
|
||||
rootDir: string | undefined;
|
||||
preferredBasename?: ContractSurfaceBasename;
|
||||
}): string[] {
|
||||
if (typeof params.rootDir !== "string" || params.rootDir.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (RUNNING_FROM_BUILT_ARTIFACT) {
|
||||
return resolveContractSurfaceModulePaths(params.rootDir, params.preferredBasename);
|
||||
}
|
||||
|
||||
const dirName = path.basename(path.resolve(params.rootDir));
|
||||
const sourceRoot = path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", dirName);
|
||||
const modulePaths: string[] = [];
|
||||
|
||||
for (const basename of CONTRACT_SURFACE_BASENAMES) {
|
||||
if (!matchesPreferredBasename(basename, params.preferredBasename)) {
|
||||
continue;
|
||||
}
|
||||
modulePaths.push(modulePath);
|
||||
|
||||
const sourceBaseName = basename.replace(/\.[^.]+$/u, "");
|
||||
let sourceCandidatePath: string | null = null;
|
||||
for (const ext of PUBLIC_SURFACE_SOURCE_EXTENSIONS) {
|
||||
const candidate = path.join(sourceRoot, `${sourceBaseName}${ext}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
sourceCandidatePath = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sourceCandidatePath) {
|
||||
modulePaths.push(sourceCandidatePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const builtCandidates = resolveContractSurfaceModulePaths(params.rootDir, basename);
|
||||
if (builtCandidates[0]) {
|
||||
modulePaths.push(builtCandidates[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return modulePaths;
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +165,9 @@ function loadBundledChannelContractSurfaceEntries(): Array<{
|
|||
if (manifest.origin !== "bundled" || manifest.channels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const modulePaths = resolveContractSurfaceModulePaths(manifest.rootDir);
|
||||
const modulePaths = resolveSourceFirstContractSurfaceModulePaths({
|
||||
rootDir: manifest.rootDir,
|
||||
});
|
||||
if (modulePaths.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -123,3 +199,44 @@ export function getBundledChannelContractSurfaceEntries(): Array<{
|
|||
cachedSurfaceEntries ??= loadBundledChannelContractSurfaceEntries();
|
||||
return cachedSurfaceEntries;
|
||||
}
|
||||
|
||||
export function getBundledChannelContractSurfaceModule<T = unknown>(params: {
|
||||
pluginId: string;
|
||||
preferredBasename?: ContractSurfaceBasename;
|
||||
}): T | null {
|
||||
const cacheKey = `${params.pluginId}:${params.preferredBasename ?? "*"}`;
|
||||
if (cachedPreferredSurfaceModules.has(cacheKey)) {
|
||||
return (cachedPreferredSurfaceModules.get(cacheKey) ?? null) as T | null;
|
||||
}
|
||||
const discovery = discoverOpenClawPlugins({ cache: false });
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
cache: false,
|
||||
config: {},
|
||||
candidates: discovery.candidates,
|
||||
diagnostics: discovery.diagnostics,
|
||||
});
|
||||
const manifest = manifestRegistry.plugins.find(
|
||||
(entry) =>
|
||||
entry.origin === "bundled" && entry.channels.length > 0 && entry.id === params.pluginId,
|
||||
);
|
||||
if (!manifest) {
|
||||
cachedPreferredSurfaceModules.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
const modulePath = resolveSourceFirstContractSurfaceModulePaths({
|
||||
rootDir: manifest.rootDir,
|
||||
preferredBasename: params.preferredBasename,
|
||||
})[0];
|
||||
if (!modulePath) {
|
||||
cachedPreferredSurfaceModules.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const module = loadModule(modulePath)(modulePath) as T;
|
||||
cachedPreferredSurfaceModules.set(cacheKey, module);
|
||||
return module;
|
||||
} catch {
|
||||
cachedPreferredSurfaceModules.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -474,6 +474,26 @@ function expectNoCrossPluginSdkFacadeImports(file: string, imports: string[]): v
|
|||
}
|
||||
}
|
||||
|
||||
function expectCoreSourceStaysOffPluginSpecificSdkFacades(file: string, imports: string[]): void {
|
||||
for (const specifier of imports) {
|
||||
if (!specifier.includes("/plugin-sdk/")) {
|
||||
continue;
|
||||
}
|
||||
const targetSubpath = specifier.split("/plugin-sdk/")[1]?.replace(/\.[cm]?[jt]sx?$/u, "") ?? "";
|
||||
const targetExtensionId =
|
||||
BUNDLED_EXTENSION_IDS.find(
|
||||
(extensionId) =>
|
||||
targetSubpath === extensionId || targetSubpath.startsWith(`${extensionId}-`),
|
||||
) ?? null;
|
||||
if (!targetExtensionId) {
|
||||
continue;
|
||||
}
|
||||
expect.fail(
|
||||
`${file} should not import plugin-specific SDK facades (${specifier}) from core production code. Use a neutral contract surface or plugin hook instead.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe("channel import guardrails", () => {
|
||||
it("keeps channel helper modules off their own SDK barrels", () => {
|
||||
for (const source of SAME_CHANNEL_SDK_GUARDS) {
|
||||
|
|
@ -553,6 +573,15 @@ describe("channel import guardrails", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("keeps core production files off plugin-specific sdk facades", () => {
|
||||
for (const file of collectCoreSourceFiles()) {
|
||||
expectCoreSourceStaysOffPluginSpecificSdkFacades(
|
||||
file,
|
||||
getSourceAnalysis(file).importSpecifiers,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps extension-to-extension imports limited to approved public surfaces", () => {
|
||||
for (const file of collectExtensionSourceFiles()) {
|
||||
expectOnlyApprovedExtensionSeams(file, getSourceAnalysis(file).extensionImports);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { isChannelConfigured } from "./channel-configured.js";
|
||||
|
||||
describe("isChannelConfigured", () => {
|
||||
it("detects Telegram env configuration through the channel plugin seam", () => {
|
||||
expect(isChannelConfigured({}, "telegram", { TELEGRAM_BOT_TOKEN: "token" })).toBe(true);
|
||||
});
|
||||
|
||||
it("detects Discord env configuration through the channel plugin seam", () => {
|
||||
expect(isChannelConfigured({}, "discord", { DISCORD_BOT_TOKEN: "token" })).toBe(true);
|
||||
});
|
||||
|
||||
it("detects Slack env configuration through the channel plugin seam", () => {
|
||||
expect(isChannelConfigured({}, "slack", { SLACK_BOT_TOKEN: "xoxb-test" })).toBe(true);
|
||||
});
|
||||
|
||||
it("requires both IRC host and nick env vars through the channel plugin seam", () => {
|
||||
expect(isChannelConfigured({}, "irc", { IRC_HOST: "irc.example.com" })).toBe(false);
|
||||
expect(
|
||||
isChannelConfigured({}, "irc", {
|
||||
IRC_HOST: "irc.example.com",
|
||||
IRC_NICK: "openclaw",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still falls back to generic config presence for channels without a custom hook", () => {
|
||||
expect(
|
||||
isChannelConfigured(
|
||||
{
|
||||
channels: {
|
||||
signal: {
|
||||
httpPort: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
"signal",
|
||||
{},
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,28 +1,12 @@
|
|||
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
|
||||
function hasNonEmptyString(value: unknown): boolean {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function accountsHaveKeys(value: unknown, keys: readonly string[]): boolean {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
for (const account of Object.values(value)) {
|
||||
if (!isRecord(account)) {
|
||||
continue;
|
||||
}
|
||||
for (const key of keys) {
|
||||
if (hasNonEmptyString(account[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
type ChannelConfiguredSurface = {
|
||||
hasConfiguredState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean;
|
||||
hasPersistedAuthState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean;
|
||||
};
|
||||
|
||||
function resolveChannelConfig(
|
||||
cfg: OpenClawConfig,
|
||||
|
|
@ -33,120 +17,31 @@ function resolveChannelConfig(
|
|||
return isRecord(entry) ? entry : null;
|
||||
}
|
||||
|
||||
type StructuredChannelConfigSpec = {
|
||||
envAny?: readonly string[];
|
||||
envAll?: readonly string[];
|
||||
stringKeys?: readonly string[];
|
||||
numberKeys?: readonly string[];
|
||||
accountStringKeys?: readonly string[];
|
||||
};
|
||||
|
||||
const STRUCTURED_CHANNEL_CONFIG_SPECS: Record<string, StructuredChannelConfigSpec> = {
|
||||
telegram: {
|
||||
envAny: ["TELEGRAM_BOT_TOKEN"],
|
||||
stringKeys: ["botToken", "tokenFile"],
|
||||
accountStringKeys: ["botToken", "tokenFile"],
|
||||
},
|
||||
discord: {
|
||||
envAny: ["DISCORD_BOT_TOKEN"],
|
||||
stringKeys: ["token"],
|
||||
accountStringKeys: ["token"],
|
||||
},
|
||||
irc: {
|
||||
envAll: ["IRC_HOST", "IRC_NICK"],
|
||||
stringKeys: ["host", "nick"],
|
||||
accountStringKeys: ["host", "nick"],
|
||||
},
|
||||
slack: {
|
||||
envAny: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"],
|
||||
stringKeys: ["botToken", "appToken", "userToken"],
|
||||
accountStringKeys: ["botToken", "appToken", "userToken"],
|
||||
},
|
||||
signal: {
|
||||
stringKeys: ["account", "httpUrl", "httpHost", "cliPath"],
|
||||
numberKeys: ["httpPort"],
|
||||
accountStringKeys: ["account", "httpUrl", "httpHost", "cliPath"],
|
||||
},
|
||||
imessage: {
|
||||
stringKeys: ["cliPath"],
|
||||
},
|
||||
};
|
||||
|
||||
function envHasAnyKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
|
||||
for (const key of keys) {
|
||||
if (hasNonEmptyString(env[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function envHasAllKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
|
||||
for (const key of keys) {
|
||||
if (!hasNonEmptyString(env[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return keys.length > 0;
|
||||
}
|
||||
|
||||
function hasAnyNumberKeys(entry: Record<string, unknown>, keys: readonly string[]): boolean {
|
||||
for (const key of keys) {
|
||||
if (typeof entry[key] === "number") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isStructuredChannelConfigured(
|
||||
cfg: OpenClawConfig,
|
||||
channelId: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
spec: StructuredChannelConfigSpec,
|
||||
): boolean {
|
||||
if (spec.envAny && envHasAnyKeys(env, spec.envAny)) {
|
||||
return true;
|
||||
}
|
||||
if (spec.envAll && envHasAllKeys(env, spec.envAll)) {
|
||||
return true;
|
||||
}
|
||||
const entry = resolveChannelConfig(cfg, channelId);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
if (spec.stringKeys && spec.stringKeys.some((key) => hasNonEmptyString(entry[key]))) {
|
||||
return true;
|
||||
}
|
||||
if (spec.numberKeys && hasAnyNumberKeys(entry, spec.numberKeys)) {
|
||||
return true;
|
||||
}
|
||||
if (spec.accountStringKeys && accountsHaveKeys(entry.accounts, spec.accountStringKeys)) {
|
||||
return true;
|
||||
}
|
||||
return hasMeaningfulChannelConfig(entry);
|
||||
}
|
||||
|
||||
function isGenericChannelConfigured(cfg: OpenClawConfig, channelId: string): boolean {
|
||||
const entry = resolveChannelConfig(cfg, channelId);
|
||||
return hasMeaningfulChannelConfig(entry);
|
||||
}
|
||||
|
||||
function getChannelConfiguredSurface(channelId: string): ChannelConfiguredSurface | null {
|
||||
return getBundledChannelContractSurfaceModule<ChannelConfiguredSurface>({
|
||||
pluginId: channelId,
|
||||
preferredBasename: "contract-surfaces.ts",
|
||||
});
|
||||
}
|
||||
|
||||
export function isChannelConfigured(
|
||||
cfg: OpenClawConfig,
|
||||
channelId: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
const pluginConfigured = getChannelPlugin(channelId)?.config.hasPersistedAuthState?.({
|
||||
cfg,
|
||||
env,
|
||||
});
|
||||
const surface = getChannelConfiguredSurface(channelId);
|
||||
const pluginConfigured = surface?.hasConfiguredState?.({ cfg, env });
|
||||
if (pluginConfigured) {
|
||||
return true;
|
||||
}
|
||||
const spec = STRUCTURED_CHANNEL_CONFIG_SPECS[channelId];
|
||||
if (spec) {
|
||||
return isStructuredChannelConfigured(cfg, channelId, env, spec);
|
||||
const pluginPersistedAuthState = surface?.hasPersistedAuthState?.({ cfg, env });
|
||||
if (pluginPersistedAuthState) {
|
||||
return true;
|
||||
}
|
||||
return isGenericChannelConfigured(cfg, channelId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
|
||||
|
||||
export type TelegramCustomCommandInput = {
|
||||
command?: string | null;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
export type TelegramCustomCommandIssue = {
|
||||
index: number;
|
||||
field: "command" | "description";
|
||||
message: string;
|
||||
};
|
||||
|
||||
type TelegramCommandConfigContract = {
|
||||
TELEGRAM_COMMAND_NAME_PATTERN: RegExp;
|
||||
normalizeTelegramCommandName: (value: string) => string;
|
||||
normalizeTelegramCommandDescription: (value: string) => string;
|
||||
resolveTelegramCustomCommands: (params: {
|
||||
commands?: TelegramCustomCommandInput[] | null;
|
||||
reservedCommands?: Set<string>;
|
||||
checkReserved?: boolean;
|
||||
checkDuplicates?: boolean;
|
||||
}) => {
|
||||
commands: Array<{ command: string; description: string }>;
|
||||
issues: TelegramCustomCommandIssue[];
|
||||
};
|
||||
};
|
||||
|
||||
function loadTelegramCommandConfigContract(): TelegramCommandConfigContract {
|
||||
const contract = getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
|
||||
pluginId: "telegram",
|
||||
preferredBasename: "contract-api.ts",
|
||||
});
|
||||
if (!contract) {
|
||||
throw new Error("telegram command config contract surface is unavailable");
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
export const TELEGRAM_COMMAND_NAME_PATTERN =
|
||||
loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN;
|
||||
|
||||
export function normalizeTelegramCommandName(value: string): string {
|
||||
return loadTelegramCommandConfigContract().normalizeTelegramCommandName(value);
|
||||
}
|
||||
|
||||
export function normalizeTelegramCommandDescription(value: string): string {
|
||||
return loadTelegramCommandConfigContract().normalizeTelegramCommandDescription(value);
|
||||
}
|
||||
|
||||
export function resolveTelegramCustomCommands(params: {
|
||||
commands?: TelegramCustomCommandInput[] | null;
|
||||
reservedCommands?: Set<string>;
|
||||
checkReserved?: boolean;
|
||||
checkDuplicates?: boolean;
|
||||
}): {
|
||||
commands: Array<{ command: string; description: string }>;
|
||||
issues: TelegramCustomCommandIssue[];
|
||||
} {
|
||||
return loadTelegramCommandConfigContract().resolveTelegramCustomCommands(params);
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
normalizeTelegramCommandDescription,
|
||||
normalizeTelegramCommandName,
|
||||
resolveTelegramCustomCommands,
|
||||
} from "../plugin-sdk/telegram-command-config.js";
|
||||
} from "./telegram-command-config.js";
|
||||
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
|
||||
import {
|
||||
ChannelHealthMonitorSchema,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,8 @@
|
|||
import { z } from "zod";
|
||||
import { getBundledChannelRuntimeMap } from "./bundled-channel-config-runtime.js";
|
||||
import type { ChannelsConfig } from "./types.channels.js";
|
||||
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
|
||||
import { ContextVisibilityModeSchema, GroupPolicySchema } from "./zod-schema.core.js";
|
||||
import {
|
||||
BlueBubblesConfigSchema,
|
||||
DiscordConfigSchema,
|
||||
GoogleChatConfigSchema,
|
||||
IMessageConfigSchema,
|
||||
IrcConfigSchema,
|
||||
MSTeamsConfigSchema,
|
||||
SignalConfigSchema,
|
||||
SlackConfigSchema,
|
||||
TelegramConfigSchema,
|
||||
} from "./zod-schema.providers-core.js";
|
||||
import { WhatsAppConfigSchema } from "./zod-schema.providers-whatsapp.js";
|
||||
|
||||
export * from "./zod-schema.providers-core.js";
|
||||
export * from "./zod-schema.providers-whatsapp.js";
|
||||
|
|
@ -23,21 +12,7 @@ const ChannelModelByChannelSchema = z
|
|||
.record(z.string(), z.record(z.string(), z.string()))
|
||||
.optional();
|
||||
|
||||
const directChannelRuntimeSchemas = new Map<
|
||||
string,
|
||||
{ safeParse: (value: unknown) => ReturnType<z.ZodTypeAny["safeParse"]> }
|
||||
>([
|
||||
["bluebubbles", { safeParse: (value) => BlueBubblesConfigSchema.safeParse(value) }],
|
||||
["discord", { safeParse: (value) => DiscordConfigSchema.safeParse(value) }],
|
||||
["googlechat", { safeParse: (value) => GoogleChatConfigSchema.safeParse(value) }],
|
||||
["imessage", { safeParse: (value) => IMessageConfigSchema.safeParse(value) }],
|
||||
["irc", { safeParse: (value) => IrcConfigSchema.safeParse(value) }],
|
||||
["msteams", { safeParse: (value) => MSTeamsConfigSchema.safeParse(value) }],
|
||||
["signal", { safeParse: (value) => SignalConfigSchema.safeParse(value) }],
|
||||
["slack", { safeParse: (value) => SlackConfigSchema.safeParse(value) }],
|
||||
["telegram", { safeParse: (value) => TelegramConfigSchema.safeParse(value) }],
|
||||
["whatsapp", { safeParse: (value) => WhatsAppConfigSchema.safeParse(value) }],
|
||||
]);
|
||||
const directChannelRuntimeSchemas = getBundledChannelRuntimeMap();
|
||||
|
||||
function addLegacyChannelAcpBindingIssues(
|
||||
value: unknown,
|
||||
|
|
@ -86,7 +61,7 @@ function normalizeBundledChannelConfigs(
|
|||
}
|
||||
const parsed = runtimeSchema.safeParse(value[channelId]);
|
||||
if (!parsed.success) {
|
||||
for (const issue of parsed.error.issues) {
|
||||
for (const issue of parsed.issues) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: issue.message ?? `Invalid channels.${channelId} config.`,
|
||||
|
|
|
|||
|
|
@ -311,12 +311,11 @@ export async function hydrateAttachmentParamsForAction(params: {
|
|||
dryRun?: boolean;
|
||||
mediaPolicy: AttachmentMediaPolicy;
|
||||
}): Promise<void> {
|
||||
const shouldHydrateBlueBubblesUploadFile =
|
||||
params.action === "upload-file" && params.channel === "bluebubbles";
|
||||
const shouldHydrateUploadFile = params.action === "upload-file";
|
||||
if (
|
||||
params.action !== "sendAttachment" &&
|
||||
params.action !== "setGroupIcon" &&
|
||||
!shouldHydrateBlueBubblesUploadFile
|
||||
!shouldHydrateUploadFile
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -327,8 +326,7 @@ export async function hydrateAttachmentParamsForAction(params: {
|
|||
args: params.args,
|
||||
dryRun: params.dryRun,
|
||||
mediaPolicy: params.mediaPolicy,
|
||||
allowMessageCaptionFallback:
|
||||
params.action === "sendAttachment" || shouldHydrateBlueBubblesUploadFile,
|
||||
allowMessageCaptionFallback: params.action === "sendAttachment" || shouldHydrateUploadFile,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { ChannelAccountSnapshot } from "../channels/plugins/types.core.js";
|
||||
import type { ChannelStatusIssue } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
parseChatTargetPrefixesOrThrow,
|
||||
|
|
@ -5,6 +7,7 @@ import {
|
|||
type ParsedChatTarget,
|
||||
} from "./channel-targets.js";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { asString, collectIssuesForEnabledAccounts, isRecord } from "./status-helpers.js";
|
||||
|
||||
// Narrow plugin-sdk surface for the bundled BlueBubbles plugin.
|
||||
// Keep this list additive and scoped to the conversation-binding seam only.
|
||||
|
|
@ -263,6 +266,101 @@ export function resolveBlueBubblesConversationIdFromTarget(target: string): stri
|
|||
return normalizeBlueBubblesAcpConversationId(target)?.conversationId;
|
||||
}
|
||||
|
||||
type BlueBubblesAccountStatus = {
|
||||
accountId?: unknown;
|
||||
enabled?: unknown;
|
||||
configured?: unknown;
|
||||
running?: unknown;
|
||||
baseUrl?: unknown;
|
||||
lastError?: unknown;
|
||||
probe?: unknown;
|
||||
};
|
||||
|
||||
type BlueBubblesProbeResult = {
|
||||
ok?: boolean;
|
||||
status?: number | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
function readBlueBubblesAccountStatus(
|
||||
value: ChannelAccountSnapshot,
|
||||
): BlueBubblesAccountStatus | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
configured: value.configured,
|
||||
running: value.running,
|
||||
baseUrl: value.baseUrl,
|
||||
lastError: value.lastError,
|
||||
probe: value.probe,
|
||||
};
|
||||
}
|
||||
|
||||
function readBlueBubblesProbeResult(value: unknown): BlueBubblesProbeResult | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: typeof value.ok === "boolean" ? value.ok : undefined,
|
||||
status: typeof value.status === "number" ? value.status : null,
|
||||
error: asString(value.error) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function collectBlueBubblesStatusIssues(
|
||||
accounts: ChannelAccountSnapshot[],
|
||||
): ChannelStatusIssue[] {
|
||||
return collectIssuesForEnabledAccounts({
|
||||
accounts,
|
||||
readAccount: readBlueBubblesAccountStatus,
|
||||
collectIssues: ({ account, accountId, issues }) => {
|
||||
const configured = account.configured === true;
|
||||
const running = account.running === true;
|
||||
const lastError = asString(account.lastError);
|
||||
const probe = readBlueBubblesProbeResult(account.probe);
|
||||
|
||||
if (!configured) {
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Not configured (missing serverUrl or password).",
|
||||
fix: "Run: openclaw channels add bluebubbles --http-url <server-url> --password <password>",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (probe && probe.ok === false) {
|
||||
const errorDetail = probe.error
|
||||
? `: ${probe.error}`
|
||||
: probe.status
|
||||
? ` (HTTP ${probe.status})`
|
||||
: "";
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `BlueBubbles server unreachable${errorDetail}`,
|
||||
fix: "Check that the BlueBubbles server is running and accessible. Verify serverUrl and password in your config.",
|
||||
});
|
||||
}
|
||||
|
||||
if (running && lastError) {
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `Channel error: ${lastError}`,
|
||||
fix: "Check gateway logs for details. If the webhook is failing, verify the webhook URL is configured in BlueBubbles server settings.",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { resolveAckReaction } from "../agents/identity.js";
|
||||
export {
|
||||
createActionGate,
|
||||
|
|
@ -305,7 +403,6 @@ export {
|
|||
patchScopedAccountConfig,
|
||||
} from "../channels/plugins/setup-helpers.js";
|
||||
export { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
|
||||
export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js";
|
||||
export type {
|
||||
BaseProbeResult,
|
||||
ChannelAccountSnapshot,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
|
||||
export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js";
|
||||
export {
|
||||
projectCredentialSnapshotFields,
|
||||
resolveConfiguredFromCredentialStatuses,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveDmGroupAccessWithLists } from "../security/dm-policy-shared.js";
|
||||
export { buildCommandsPaginationKeyboard } from "../../extensions/telegram/api.js";
|
||||
export {
|
||||
createPreCryptoDirectDmAuthorizer,
|
||||
resolveInboundDirectDmAccessWithRuntime,
|
||||
|
|
@ -86,6 +86,37 @@ export {
|
|||
buildHelpMessage,
|
||||
} from "../auto-reply/status.js";
|
||||
|
||||
type TelegramCommandUiContract = {
|
||||
buildCommandsPaginationKeyboard: (
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
agentId?: string,
|
||||
) => Array<Array<{ text: string; callback_data: string }>>;
|
||||
};
|
||||
|
||||
function loadTelegramCommandUiContract(): TelegramCommandUiContract {
|
||||
const contract = getBundledChannelContractSurfaceModule<TelegramCommandUiContract>({
|
||||
pluginId: "telegram",
|
||||
preferredBasename: "contract-api.ts",
|
||||
});
|
||||
if (!contract) {
|
||||
throw new Error("telegram command ui contract surface is unavailable");
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
export function buildCommandsPaginationKeyboard(
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
agentId?: string,
|
||||
): Array<Array<{ text: string; callback_data: string }>> {
|
||||
return loadTelegramCommandUiContract().buildCommandsPaginationKeyboard(
|
||||
currentPage,
|
||||
totalPages,
|
||||
agentId,
|
||||
);
|
||||
}
|
||||
|
||||
export type ResolveSenderCommandAuthorizationParams = {
|
||||
cfg: OpenClawConfig;
|
||||
rawBody: string;
|
||||
|
|
|
|||
|
|
@ -49,4 +49,4 @@ export {
|
|||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
} from "./bluebubbles-policy.js";
|
||||
export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js";
|
||||
export { collectBlueBubblesStatusIssues } from "./bluebubbles.js";
|
||||
|
|
|
|||
|
|
@ -1,13 +1,83 @@
|
|||
import type { OpenClawConfig } from "./config-runtime.js";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
type MatrixLegacyLog = {
|
||||
info?: (message: string) => void;
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
type MatrixLegacyCryptoPlan = {
|
||||
accountId: string;
|
||||
rootDir: string;
|
||||
recoveryKeyPath: string;
|
||||
statePath: string;
|
||||
legacyCryptoPath: string;
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
deviceId: string | null;
|
||||
};
|
||||
|
||||
type MatrixLegacyCryptoDetection = {
|
||||
plans: MatrixLegacyCryptoPlan[];
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
type MatrixLegacyMigrationResult = {
|
||||
migrated: boolean;
|
||||
changes: string[];
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
type MatrixLegacyStatePlan = {
|
||||
accountId: string;
|
||||
legacyStoragePath: string;
|
||||
legacyCryptoPath: string;
|
||||
targetRootDir: string;
|
||||
targetStoragePath: string;
|
||||
targetCryptoPath: string;
|
||||
selectionNote?: string;
|
||||
};
|
||||
|
||||
type MatrixLegacyStateDetection = MatrixLegacyStatePlan | { warning: string } | null;
|
||||
|
||||
type MatrixMigrationSnapshotResult = {
|
||||
created: boolean;
|
||||
archivePath: string;
|
||||
markerPath: string;
|
||||
};
|
||||
|
||||
type MatrixRuntimeHeavyModule = {
|
||||
autoPrepareLegacyMatrixCrypto: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["autoPrepareLegacyMatrixCrypto"];
|
||||
detectLegacyMatrixCrypto: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["detectLegacyMatrixCrypto"];
|
||||
autoMigrateLegacyMatrixState: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["autoMigrateLegacyMatrixState"];
|
||||
detectLegacyMatrixState: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["detectLegacyMatrixState"];
|
||||
hasActionableMatrixMigration: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["hasActionableMatrixMigration"];
|
||||
hasPendingMatrixMigration: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["hasPendingMatrixMigration"];
|
||||
maybeCreateMatrixMigrationSnapshot: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["maybeCreateMatrixMigrationSnapshot"];
|
||||
autoPrepareLegacyMatrixCrypto: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
log?: MatrixLegacyLog;
|
||||
deps?: Partial<Record<string, unknown>>;
|
||||
}) => Promise<MatrixLegacyMigrationResult>;
|
||||
detectLegacyMatrixCrypto: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => MatrixLegacyCryptoDetection;
|
||||
autoMigrateLegacyMatrixState: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
log?: MatrixLegacyLog;
|
||||
}) => Promise<MatrixLegacyMigrationResult>;
|
||||
detectLegacyMatrixState: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => MatrixLegacyStateDetection;
|
||||
hasActionableMatrixMigration: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => boolean;
|
||||
hasPendingMatrixMigration: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean;
|
||||
maybeCreateMatrixMigrationSnapshot: (params: {
|
||||
trigger: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
outputDir?: string;
|
||||
log?: MatrixLegacyLog;
|
||||
}) => Promise<MatrixMigrationSnapshotResult>;
|
||||
};
|
||||
|
||||
function loadFacadeModule(): MatrixRuntimeHeavyModule {
|
||||
|
|
|
|||
|
|
@ -3,4 +3,6 @@ export {
|
|||
normalizeTelegramCommandDescription,
|
||||
normalizeTelegramCommandName,
|
||||
resolveTelegramCustomCommands,
|
||||
} from "../../extensions/telegram/src/command-config.js";
|
||||
type TelegramCustomCommandInput,
|
||||
type TelegramCustomCommandIssue,
|
||||
} from "../config/telegram-command-config.js";
|
||||
|
|
|
|||
|
|
@ -607,6 +607,8 @@ describe("plugin-sdk subpath exports", () => {
|
|||
"shouldComputeCommandAuthorized",
|
||||
"shouldHandleTextCommands",
|
||||
]);
|
||||
expectSourceOmitsSnippet("command-auth", "../../extensions/");
|
||||
expectSourceOmitsSnippet("matrix-runtime-heavy", "../../extensions/");
|
||||
expectSourceMentions("channel-send-result", [
|
||||
"attachChannelToResult",
|
||||
"buildChannelSendResult",
|
||||
|
|
|
|||
Loading…
Reference in New Issue