diff --git a/CHANGELOG.md b/CHANGELOG.md index c00f18f94..295d4bd7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.clawd.bot - Docs: document plugin slots and memory plugin behavior. - Plugins: add the bundled BlueBubbles channel plugin (disabled by default). - Plugins: migrate bundled messaging extensions to the plugin SDK; resolve plugin-sdk imports in loader. +- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime. ## 2026.1.17-5 diff --git a/extensions/zalo/index.ts b/extensions/zalo/index.ts index 38b408f93..5e5a44512 100644 --- a/extensions/zalo/index.ts +++ b/extensions/zalo/index.ts @@ -2,12 +2,14 @@ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; import { zaloDock, zaloPlugin } from "./src/channel.js"; import { handleZaloWebhookRequest } from "./src/monitor.js"; +import { setZaloRuntime } from "./src/runtime.js"; const plugin = { id: "zalo", name: "Zalo", description: "Zalo channel plugin (Bot API)", register(api: ClawdbotPluginApi) { + setZaloRuntime(api.runtime); api.registerChannel({ plugin: zaloPlugin, dock: zaloDock }); api.registerHttpHandler(handleZaloWebhookRequest); }, diff --git a/extensions/zalo/src/accounts.ts b/extensions/zalo/src/accounts.ts index c9dc3c069..168525473 100644 --- a/extensions/zalo/src/accounts.ts +++ b/extensions/zalo/src/accounts.ts @@ -1,25 +1,22 @@ -import type { - CoreConfig, - ResolvedZaloAccount, - ZaloAccountConfig, - ZaloConfig, -} from "./types.js"; -import { resolveZaloToken } from "./token.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./shared/account-ids.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; -function listConfiguredAccountIds(cfg: CoreConfig): string[] { +import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js"; +import { resolveZaloToken } from "./token.js"; + +function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] { const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts; if (!accounts || typeof accounts !== "object") return []; return Object.keys(accounts).filter(Boolean); } -export function listZaloAccountIds(cfg: CoreConfig): string[] { +export function listZaloAccountIds(cfg: ClawdbotConfig): string[] { const ids = listConfiguredAccountIds(cfg); if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; return ids.sort((a, b) => a.localeCompare(b)); } -export function resolveDefaultZaloAccountId(cfg: CoreConfig): string { +export function resolveDefaultZaloAccountId(cfg: ClawdbotConfig): string { const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined; if (zaloConfig?.defaultAccount?.trim()) return zaloConfig.defaultAccount.trim(); const ids = listZaloAccountIds(cfg); @@ -28,7 +25,7 @@ export function resolveDefaultZaloAccountId(cfg: CoreConfig): string { } function resolveAccountConfig( - cfg: CoreConfig, + cfg: ClawdbotConfig, accountId: string, ): ZaloAccountConfig | undefined { const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts; @@ -36,7 +33,7 @@ function resolveAccountConfig( return accounts[accountId] as ZaloAccountConfig | undefined; } -function mergeZaloAccountConfig(cfg: CoreConfig, accountId: string): ZaloAccountConfig { +function mergeZaloAccountConfig(cfg: ClawdbotConfig, accountId: string): ZaloAccountConfig { const raw = (cfg.channels?.zalo ?? {}) as ZaloConfig; const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw; const account = resolveAccountConfig(cfg, accountId) ?? {}; @@ -44,7 +41,7 @@ function mergeZaloAccountConfig(cfg: CoreConfig, accountId: string): ZaloAccount } export function resolveZaloAccount(params: { - cfg: CoreConfig; + cfg: ClawdbotConfig; accountId?: string | null; }): ResolvedZaloAccount { const accountId = normalizeAccountId(params.accountId); @@ -67,7 +64,7 @@ export function resolveZaloAccount(params: { }; } -export function listEnabledZaloAccounts(cfg: CoreConfig): ResolvedZaloAccount[] { +export function listEnabledZaloAccounts(cfg: ClawdbotConfig): ResolvedZaloAccount[] { return listZaloAccountIds(cfg) .map((accountId) => resolveZaloAccount({ cfg, accountId })) .filter((account) => account.enabled); diff --git a/extensions/zalo/src/actions.ts b/extensions/zalo/src/actions.ts index 9bf33ee52..ba3df8315 100644 --- a/extensions/zalo/src/actions.ts +++ b/extensions/zalo/src/actions.ts @@ -1,16 +1,16 @@ import type { ChannelMessageActionAdapter, ChannelMessageActionName, + ClawdbotConfig, } from "clawdbot/plugin-sdk"; +import { jsonResult, readStringParam } from "clawdbot/plugin-sdk"; -import type { CoreConfig } from "./types.js"; import { listEnabledZaloAccounts } from "./accounts.js"; import { sendMessageZalo } from "./send.js"; -import { jsonResult, readStringParam } from "./tool-helpers.js"; const providerId = "zalo"; -function listEnabledAccounts(cfg: CoreConfig) { +function listEnabledAccounts(cfg: ClawdbotConfig) { return listEnabledZaloAccounts(cfg).filter( (account) => account.enabled && account.tokenSource !== "none", ); @@ -18,7 +18,7 @@ function listEnabledAccounts(cfg: CoreConfig) { export const zaloMessageActions: ChannelMessageActionAdapter = { listActions: ({ cfg }) => { - const accounts = listEnabledAccounts(cfg as CoreConfig); + const accounts = listEnabledAccounts(cfg as ClawdbotConfig); if (accounts.length === 0) return []; const actions = new Set(["send"]); return Array.from(actions); @@ -44,7 +44,7 @@ export const zaloMessageActions: ChannelMessageActionAdapter = { const result = await sendMessageZalo(to ?? "", content ?? "", { accountId: accountId ?? undefined, mediaUrl: mediaUrl ?? undefined, - cfg: cfg as CoreConfig, + cfg: cfg as ClawdbotConfig, }); if (!result.ok) { diff --git a/extensions/zalo/src/channel.directory.test.ts b/extensions/zalo/src/channel.directory.test.ts index 0ce14ca9f..d3c59ed1e 100644 --- a/extensions/zalo/src/channel.directory.test.ts +++ b/extensions/zalo/src/channel.directory.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { CoreConfig } from "./types.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; import { zaloPlugin } from "./channel.js"; @@ -12,7 +12,7 @@ describe("zalo directory", () => { allowFrom: ["zalo:123", "zl:234", "345"], }, }, - } as unknown as CoreConfig; + } as unknown as ClawdbotConfig; expect(zaloPlugin.directory).toBeTruthy(); expect(zaloPlugin.directory?.listPeers).toBeTruthy(); diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 6bdf5fd92..b9eca12c6 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -1,25 +1,29 @@ -import type { ChannelAccountSnapshot, ChannelDock, ChannelPlugin } from "clawdbot/plugin-sdk"; -import { buildChannelConfigSchema } from "clawdbot/plugin-sdk"; +import type { + ChannelAccountSnapshot, + ChannelDock, + ChannelPlugin, + ClawdbotConfig, +} from "clawdbot/plugin-sdk"; +import { + applyAccountNameToChannelSection, + buildChannelConfigSchema, + DEFAULT_ACCOUNT_ID, + deleteAccountFromConfigSection, + formatPairingApproveHint, + migrateBaseNameToDefaultAccount, + normalizeAccountId, + PAIRING_APPROVED_MESSAGE, + setAccountEnabledInConfigSection, +} from "clawdbot/plugin-sdk"; import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount, type ResolvedZaloAccount } from "./accounts.js"; import { zaloMessageActions } from "./actions.js"; import { ZaloConfigSchema } from "./config-schema.js"; -import { - deleteAccountFromConfigSection, - setAccountEnabledInConfigSection, -} from "./shared/channel-config.js"; import { zaloOnboardingAdapter } from "./onboarding.js"; -import { formatPairingApproveHint, PAIRING_APPROVED_MESSAGE } from "./shared/pairing.js"; import { resolveZaloProxyFetch } from "./proxy.js"; import { probeZalo } from "./probe.js"; import { sendMessageZalo } from "./send.js"; -import { - applyAccountNameToChannelSection, - migrateBaseNameToDefaultAccount, -} from "./shared/channel-setup.js"; import { collectZaloStatusIssues } from "./status-issues.js"; -import type { CoreConfig } from "./types.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./shared/account-ids.js"; const meta = { id: "zalo", @@ -33,7 +37,6 @@ const meta = { quickstartAllowFrom: true, }; - function normalizeZaloMessagingTarget(raw: string): string | undefined { const trimmed = raw?.trim(); if (!trimmed) return undefined; @@ -50,7 +53,7 @@ export const zaloDock: ChannelDock = { outbound: { textChunkLimit: 2000 }, config: { resolveAllowFrom: ({ cfg, accountId }) => - (resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map( + (resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }).config.allowFrom ?? []).map( (entry) => String(entry), ), formatAllowFrom: ({ allowFrom }) => @@ -84,12 +87,12 @@ export const zaloPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.zalo"] }, configSchema: buildChannelConfigSchema(ZaloConfigSchema), config: { - listAccountIds: (cfg) => listZaloAccountIds(cfg as CoreConfig), - resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }), - defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg as CoreConfig), + listAccountIds: (cfg) => listZaloAccountIds(cfg as ClawdbotConfig), + resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }), + defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg as ClawdbotConfig), setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({ - cfg: cfg as CoreConfig, + cfg: cfg as ClawdbotConfig, sectionKey: "zalo", accountId, enabled, @@ -97,7 +100,7 @@ export const zaloPlugin: ChannelPlugin = { }), deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({ - cfg: cfg as CoreConfig, + cfg: cfg as ClawdbotConfig, sectionKey: "zalo", accountId, clearBaseFields: ["botToken", "tokenFile", "name"], @@ -111,7 +114,7 @@ export const zaloPlugin: ChannelPlugin = { tokenSource: account.tokenSource, }), resolveAllowFrom: ({ cfg, accountId }) => - (resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map( + (resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }).config.allowFrom ?? []).map( (entry) => String(entry), ), formatAllowFrom: ({ allowFrom }) => @@ -125,7 +128,7 @@ export const zaloPlugin: ChannelPlugin = { resolveDmPolicy: ({ cfg, accountId, account }) => { const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; const useAccountPath = Boolean( - (cfg as CoreConfig).channels?.zalo?.accounts?.[resolvedAccountId], + (cfg as ClawdbotConfig).channels?.zalo?.accounts?.[resolvedAccountId], ); const basePath = useAccountPath ? `channels.zalo.accounts.${resolvedAccountId}.` @@ -161,7 +164,7 @@ export const zaloPlugin: ChannelPlugin = { directory: { self: async () => null, listPeers: async ({ cfg, accountId, query, limit }) => { - const account = resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }); + const account = resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }); const q = query?.trim().toLowerCase() || ""; const peers = Array.from( new Set( @@ -182,7 +185,7 @@ export const zaloPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as CoreConfig, + cfg: cfg as ClawdbotConfig, channelKey: "zalo", accountId, name, @@ -198,7 +201,7 @@ export const zaloPlugin: ChannelPlugin = { }, applyAccountConfig: ({ cfg, accountId, input }) => { const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as CoreConfig, + cfg: cfg as ClawdbotConfig, channelKey: "zalo", accountId, name: input.name, @@ -227,7 +230,7 @@ export const zaloPlugin: ChannelPlugin = { : {}), }, }, - } as CoreConfig; + } as ClawdbotConfig; } return { ...next, @@ -250,14 +253,14 @@ export const zaloPlugin: ChannelPlugin = { }, }, }, - } as CoreConfig; + } as ClawdbotConfig; }, }, pairing: { idLabel: "zaloUserId", normalizeAllowEntry: (entry) => entry.replace(/^(zalo|zl):/i, ""), notifyApproval: async ({ cfg, id }) => { - const account = resolveZaloAccount({ cfg: cfg as CoreConfig }); + const account = resolveZaloAccount({ cfg: cfg as ClawdbotConfig }); if (!account.token) throw new Error("Zalo token not configured"); await sendMessageZalo(id, PAIRING_APPROVED_MESSAGE, { token: account.token }); }, @@ -289,7 +292,7 @@ export const zaloPlugin: ChannelPlugin = { sendText: async ({ to, text, accountId, cfg }) => { const result = await sendMessageZalo(to, text, { accountId: accountId ?? undefined, - cfg: cfg as CoreConfig, + cfg: cfg as ClawdbotConfig, }); return { channel: "zalo", @@ -302,7 +305,7 @@ export const zaloPlugin: ChannelPlugin = { const result = await sendMessageZalo(to, text, { accountId: accountId ?? undefined, mediaUrl, - cfg: cfg as CoreConfig, + cfg: cfg as ClawdbotConfig, }); return { channel: "zalo", @@ -375,7 +378,7 @@ export const zaloPlugin: ChannelPlugin = { return monitorZaloProvider({ token, account, - config: ctx.cfg as CoreConfig, + config: ctx.cfg as ClawdbotConfig, runtime: ctx.runtime, abortSignal: ctx.abortSignal, useWebhook: Boolean(account.config.webhookUrl), diff --git a/extensions/zalo/src/core-bridge.ts b/extensions/zalo/src/core-bridge.ts deleted file mode 100644 index 77cb72271..000000000 --- a/extensions/zalo/src/core-bridge.ts +++ /dev/null @@ -1,176 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -export type CoreChannelDeps = { - chunkMarkdownText: (text: string, limit: number) => string[]; - formatAgentEnvelope: (params: { - channel: string; - from: string; - timestamp?: number; - body: string; - }) => string; - dispatchReplyWithBufferedBlockDispatcher: (params: { - ctx: unknown; - cfg: unknown; - dispatcherOptions: { - deliver: (payload: unknown) => Promise; - onError?: (err: unknown, info: { kind: string }) => void; - }; - }) => Promise; - resolveAgentRoute: (params: { - cfg: unknown; - channel: string; - accountId: string; - peer: { kind: "dm" | "group" | "channel"; id: string }; - }) => { sessionKey: string; accountId: string }; - buildPairingReply: (params: { channel: string; idLine: string; code: string }) => string; - readChannelAllowFromStore: (channel: string) => Promise; - upsertChannelPairingRequest: (params: { - channel: string; - id: string; - meta?: { name?: string }; - pairingAdapter?: { - idLabel: string; - normalizeAllowEntry?: (entry: string) => string; - notifyApproval?: (params: { cfg: unknown; id: string; runtime?: unknown }) => Promise; - }; - }) => Promise<{ code: string; created: boolean }>; - fetchRemoteMedia: (params: { url: string }) => Promise<{ buffer: Buffer; contentType?: string }>; - saveMediaBuffer: ( - buffer: Buffer, - contentType: string | undefined, - type: "inbound" | "outbound", - maxBytes: number, - ) => Promise<{ path: string; contentType: string }>; - shouldLogVerbose: () => boolean; -}; - -let coreRootCache: string | null = null; -let coreDepsPromise: Promise | null = null; - -function findPackageRoot(startDir: string, name: string): string | null { - let dir = startDir; - for (;;) { - const pkgPath = path.join(dir, "package.json"); - try { - if (fs.existsSync(pkgPath)) { - const raw = fs.readFileSync(pkgPath, "utf8"); - const pkg = JSON.parse(raw) as { name?: string }; - if (pkg.name === name) return dir; - } - } catch { - // ignore parse errors - } - const parent = path.dirname(dir); - if (parent === dir) return null; - dir = parent; - } -} - -function resolveClawdbotRoot(): string { - if (coreRootCache) return coreRootCache; - const override = process.env.CLAWDBOT_ROOT?.trim(); - if (override) { - coreRootCache = override; - return override; - } - - const candidates = new Set(); - if (process.argv[1]) { - candidates.add(path.dirname(process.argv[1])); - } - candidates.add(process.cwd()); - try { - const urlPath = fileURLToPath(import.meta.url); - candidates.add(path.dirname(urlPath)); - } catch { - // ignore - } - - for (const start of candidates) { - const found = findPackageRoot(start, "clawdbot"); - if (found) { - coreRootCache = found; - return found; - } - } - - throw new Error( - "Unable to resolve Clawdbot root. Set CLAWDBOT_ROOT to the package root.", - ); -} - -async function importCoreModule(relativePath: string): Promise { - const root = resolveClawdbotRoot(); - const distPath = path.join(root, "dist", relativePath); - if (!fs.existsSync(distPath)) { - throw new Error( - `Missing core module at ${distPath}. Run \`pnpm build\` or install the official package.`, - ); - } - return (await import(pathToFileURL(distPath).href)) as T; -} - -export async function loadCoreChannelDeps(): Promise { - if (coreDepsPromise) return coreDepsPromise; - - coreDepsPromise = (async () => { - const [ - chunk, - envelope, - dispatcher, - routing, - pairingMessages, - pairingStore, - mediaFetch, - mediaStore, - globals, - ] = await Promise.all([ - importCoreModule<{ chunkMarkdownText: CoreChannelDeps["chunkMarkdownText"] }>( - "auto-reply/chunk.js", - ), - importCoreModule<{ formatAgentEnvelope: CoreChannelDeps["formatAgentEnvelope"] }>( - "auto-reply/envelope.js", - ), - importCoreModule<{ - dispatchReplyWithBufferedBlockDispatcher: CoreChannelDeps["dispatchReplyWithBufferedBlockDispatcher"]; - }>("auto-reply/reply/provider-dispatcher.js"), - importCoreModule<{ resolveAgentRoute: CoreChannelDeps["resolveAgentRoute"] }>( - "routing/resolve-route.js", - ), - importCoreModule<{ buildPairingReply: CoreChannelDeps["buildPairingReply"] }>( - "pairing/pairing-messages.js", - ), - importCoreModule<{ - readChannelAllowFromStore: CoreChannelDeps["readChannelAllowFromStore"]; - upsertChannelPairingRequest: CoreChannelDeps["upsertChannelPairingRequest"]; - }>("pairing/pairing-store.js"), - importCoreModule<{ fetchRemoteMedia: CoreChannelDeps["fetchRemoteMedia"] }>( - "media/fetch.js", - ), - importCoreModule<{ saveMediaBuffer: CoreChannelDeps["saveMediaBuffer"] }>( - "media/store.js", - ), - importCoreModule<{ shouldLogVerbose: CoreChannelDeps["shouldLogVerbose"] }>( - "globals.js", - ), - ]); - - return { - chunkMarkdownText: chunk.chunkMarkdownText, - formatAgentEnvelope: envelope.formatAgentEnvelope, - dispatchReplyWithBufferedBlockDispatcher: - dispatcher.dispatchReplyWithBufferedBlockDispatcher, - resolveAgentRoute: routing.resolveAgentRoute, - buildPairingReply: pairingMessages.buildPairingReply, - readChannelAllowFromStore: pairingStore.readChannelAllowFromStore, - upsertChannelPairingRequest: pairingStore.upsertChannelPairingRequest, - fetchRemoteMedia: mediaFetch.fetchRemoteMedia, - saveMediaBuffer: mediaStore.saveMediaBuffer, - shouldLogVerbose: globals.shouldLogVerbose, - }; - })(); - - return coreDepsPromise; -} diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index 0aeabd1cf..cad7d0694 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -1,14 +1,17 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { ResolvedZaloAccount } from "./accounts.js"; import { finalizeInboundContext, + formatAgentEnvelope, isControlCommandMessage, recordSessionMetaFromInbound, resolveCommandAuthorizedFromAuthorizers, resolveStorePath, shouldComputeCommandAuthorized, + type ClawdbotConfig, } from "clawdbot/plugin-sdk"; + +import type { ResolvedZaloAccount } from "./accounts.js"; import { ZaloApiError, deleteWebhook, @@ -20,10 +23,8 @@ import { type ZaloMessage, type ZaloUpdate, } from "./api.js"; -import { zaloPlugin } from "./channel.js"; -import { loadCoreChannelDeps } from "./core-bridge.js"; import { resolveZaloProxyFetch } from "./proxy.js"; -import type { CoreConfig } from "./types.js"; +import { getZaloRuntime } from "./runtime.js"; export type ZaloRuntimeEnv = { log?: (message: string) => void; @@ -33,7 +34,7 @@ export type ZaloRuntimeEnv = { export type ZaloMonitorOptions = { token: string; account: ResolvedZaloAccount; - config: CoreConfig; + config: ClawdbotConfig; runtime: ZaloRuntimeEnv; abortSignal: AbortSignal; useWebhook?: boolean; @@ -51,9 +52,11 @@ export type ZaloMonitorResult = { const ZALO_TEXT_LIMIT = 2000; const DEFAULT_MEDIA_MAX_MB = 5; -function logVerbose(deps: Awaited>, message: string): void { - if (deps.shouldLogVerbose()) { - console.log(`[zalo] ${message}`); +type ZaloCoreRuntime = ReturnType; + +function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: string): void { + if (core.logging.shouldLogVerbose()) { + runtime.log?.(`[zalo] ${message}`); } } @@ -100,9 +103,9 @@ async function readJsonBody(req: IncomingMessage, maxBytes: number) { type WebhookTarget = { token: string; account: ResolvedZaloAccount; - config: CoreConfig; + config: ClawdbotConfig; runtime: ZaloRuntimeEnv; - deps: Awaited>; + core: ZaloCoreRuntime; secret: string; path: string; mediaMaxMb: number; @@ -207,7 +210,7 @@ export async function handleZaloWebhookRequest( target.account, target.config, target.runtime, - target.deps, + target.core, target.mediaMaxMb, target.statusSink, target.fetcher, @@ -223,9 +226,9 @@ export async function handleZaloWebhookRequest( function startPollingLoop(params: { token: string; account: ResolvedZaloAccount; - config: CoreConfig; + config: ClawdbotConfig; runtime: ZaloRuntimeEnv; - deps: Awaited>; + core: ZaloCoreRuntime; abortSignal: AbortSignal; isStopped: () => boolean; mediaMaxMb: number; @@ -237,7 +240,7 @@ function startPollingLoop(params: { account, config, runtime, - deps, + core, abortSignal, isStopped, mediaMaxMb, @@ -259,7 +262,7 @@ function startPollingLoop(params: { account, config, runtime, - deps, + core, mediaMaxMb, statusSink, fetcher, @@ -286,9 +289,9 @@ async function processUpdate( update: ZaloUpdate, token: string, account: ResolvedZaloAccount, - config: CoreConfig, + config: ClawdbotConfig, runtime: ZaloRuntimeEnv, - deps: Awaited>, + core: ZaloCoreRuntime, mediaMaxMb: number, statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void, fetcher?: ZaloFetch, @@ -304,7 +307,7 @@ async function processUpdate( account, config, runtime, - deps, + core, statusSink, fetcher, ); @@ -316,7 +319,7 @@ async function processUpdate( account, config, runtime, - deps, + core, mediaMaxMb, statusSink, fetcher, @@ -337,9 +340,9 @@ async function handleTextMessage( message: ZaloMessage, token: string, account: ResolvedZaloAccount, - config: CoreConfig, + config: ClawdbotConfig, runtime: ZaloRuntimeEnv, - deps: Awaited>, + core: ZaloCoreRuntime, statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void, fetcher?: ZaloFetch, ): Promise { @@ -352,7 +355,7 @@ async function handleTextMessage( account, config, runtime, - deps, + core, text, mediaPath: undefined, mediaType: undefined, @@ -365,9 +368,9 @@ async function handleImageMessage( message: ZaloMessage, token: string, account: ResolvedZaloAccount, - config: CoreConfig, + config: ClawdbotConfig, runtime: ZaloRuntimeEnv, - deps: Awaited>, + core: ZaloCoreRuntime, mediaMaxMb: number, statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void, fetcher?: ZaloFetch, @@ -380,8 +383,8 @@ async function handleImageMessage( if (photo) { try { const maxBytes = mediaMaxMb * 1024 * 1024; - const fetched = await deps.fetchRemoteMedia({ url: photo }); - const saved = await deps.saveMediaBuffer( + const fetched = await core.channel.media.fetchRemoteMedia({ url: photo }); + const saved = await core.channel.media.saveMediaBuffer( fetched.buffer, fetched.contentType, "inbound", @@ -400,7 +403,7 @@ async function handleImageMessage( account, config, runtime, - deps, + core, text: caption, mediaPath, mediaType, @@ -413,9 +416,9 @@ async function processMessageWithPipeline(params: { message: ZaloMessage; token: string; account: ResolvedZaloAccount; - config: CoreConfig; + config: ClawdbotConfig; runtime: ZaloRuntimeEnv; - deps: Awaited>; + core: ZaloCoreRuntime; text?: string; mediaPath?: string; mediaType?: string; @@ -428,7 +431,7 @@ async function processMessageWithPipeline(params: { account, config, runtime, - deps, + core, text, mediaPath, mediaType, @@ -448,7 +451,7 @@ async function processMessageWithPipeline(params: { const shouldComputeAuth = shouldComputeCommandAuthorized(rawBody, config); const storeAllowFrom = !isGroup && (dmPolicy !== "open" || shouldComputeAuth) - ? await deps.readChannelAllowFromStore("zalo").catch(() => []) + ? await core.channel.pairing.readAllowFromStore("zalo").catch(() => []) : []; const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; const useAccessGroups = config.commands?.useAccessGroups !== false; @@ -462,7 +465,7 @@ async function processMessageWithPipeline(params: { if (!isGroup) { if (dmPolicy === "disabled") { - logVerbose(deps, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`); + logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`); return; } @@ -471,21 +474,20 @@ async function processMessageWithPipeline(params: { if (!allowed) { if (dmPolicy === "pairing") { - const { code, created } = await deps.upsertChannelPairingRequest({ + const { code, created } = await core.channel.pairing.upsertPairingRequest({ channel: "zalo", id: senderId, meta: { name: senderName ?? undefined }, - pairingAdapter: zaloPlugin.pairing, }); if (created) { - logVerbose(deps, `zalo pairing request sender=${senderId}`); + logVerbose(core, runtime, `zalo pairing request sender=${senderId}`); try { await sendMessage( token, { chat_id: chatId, - text: deps.buildPairingReply({ + text: core.channel.pairing.buildPairingReply({ channel: "zalo", idLine: `Your Zalo user id: ${senderId}`, code, @@ -495,18 +497,26 @@ async function processMessageWithPipeline(params: { ); statusSink?.({ lastOutboundAt: Date.now() }); } catch (err) { - logVerbose(deps, `zalo pairing reply failed for ${senderId}: ${String(err)}`); + logVerbose( + core, + runtime, + `zalo pairing reply failed for ${senderId}: ${String(err)}`, + ); } } } else { - logVerbose(deps, `Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`); + logVerbose( + core, + runtime, + `Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`, + ); } return; } } } - const route = deps.resolveAgentRoute({ + const route = core.channel.routing.resolveAgentRoute({ cfg: config, channel: "zalo", accountId: account.accountId, @@ -517,16 +527,14 @@ async function processMessageWithPipeline(params: { }); if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) { - logVerbose(deps, `zalo: drop control command from unauthorized sender ${senderId}`); + logVerbose(core, runtime, `zalo: drop control command from unauthorized sender ${senderId}`); return; } - const fromLabel = isGroup - ? `group:${chatId}` - : senderName || `user:${senderId}`; - const body = deps.formatAgentEnvelope({ - channel: "Zalo", - from: fromLabel, + const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`; + const body = formatAgentEnvelope({ + channel: "Zalo", + from: fromLabel, timestamp: date ? date * 1000 : undefined, body: rawBody, }); @@ -565,7 +573,7 @@ async function processMessageWithPipeline(params: { runtime.error?.(`zalo: failed updating session meta: ${String(err)}`); }); - await deps.dispatchReplyWithBufferedBlockDispatcher({ + await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, cfg: config, dispatcherOptions: { @@ -575,7 +583,7 @@ async function processMessageWithPipeline(params: { token, chatId, runtime, - deps, + core, statusSink, fetcher, }); @@ -592,11 +600,11 @@ async function deliverZaloReply(params: { token: string; chatId: string; runtime: ZaloRuntimeEnv; - deps: Awaited>; + core: ZaloCoreRuntime; statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; fetcher?: ZaloFetch; }): Promise { - const { payload, token, chatId, runtime, deps, statusSink, fetcher } = params; + const { payload, token, chatId, runtime, core, statusSink, fetcher } = params; const mediaList = payload.mediaUrls?.length ? payload.mediaUrls @@ -620,7 +628,7 @@ async function deliverZaloReply(params: { } if (payload.text) { - const chunks = deps.chunkMarkdownText(payload.text, ZALO_TEXT_LIMIT); + const chunks = core.channel.text.chunkMarkdownText(payload.text, ZALO_TEXT_LIMIT); for (const chunk of chunks) { try { await sendMessage(token, { chat_id: chatId, text: chunk }, fetcher); @@ -649,7 +657,7 @@ export async function monitorZaloProvider( fetcher: fetcherOverride, } = options; - const deps = await loadCoreChannelDeps(); + const core = getZaloRuntime(); const effectiveMediaMaxMb = account.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB; const fetcher = fetcherOverride ?? resolveZaloProxyFetch(account.config.proxy); @@ -686,7 +694,7 @@ export async function monitorZaloProvider( account, config, runtime, - deps, + core, path, secret: webhookSecret, statusSink: (patch) => statusSink?.(patch), @@ -715,7 +723,7 @@ export async function monitorZaloProvider( account, config, runtime, - deps, + core, abortSignal, isStopped: () => stopped, mediaMaxMb: effectiveMediaMaxMb, diff --git a/extensions/zalo/src/monitor.webhook.test.ts b/extensions/zalo/src/monitor.webhook.test.ts index fed1c7b7a..1d7001342 100644 --- a/extensions/zalo/src/monitor.webhook.test.ts +++ b/extensions/zalo/src/monitor.webhook.test.ts @@ -3,8 +3,8 @@ import type { AddressInfo } from "node:net"; import { describe, expect, it } from "vitest"; -import type { CoreConfig, ResolvedZaloAccount } from "./types.js"; -import type { loadCoreChannelDeps } from "./core-bridge.js"; +import type { ClawdbotConfig, PluginRuntime } from "clawdbot/plugin-sdk"; +import type { ResolvedZaloAccount } from "./types.js"; import { handleZaloWebhookRequest, registerZaloWebhookTarget } from "./monitor.js"; async function withServer( @@ -26,7 +26,7 @@ async function withServer( describe("handleZaloWebhookRequest", () => { it("returns 400 for non-object payloads", async () => { - const deps = {} as Awaited>; + const core = {} as PluginRuntime; const account: ResolvedZaloAccount = { accountId: "default", enabled: true, @@ -37,9 +37,9 @@ describe("handleZaloWebhookRequest", () => { const unregister = registerZaloWebhookTarget({ token: "tok", account, - config: {} as CoreConfig, + config: {} as ClawdbotConfig, runtime: {}, - deps, + core, secret: "secret", path: "/hook", mediaMaxMb: 5, diff --git a/extensions/zalo/src/onboarding.ts b/extensions/zalo/src/onboarding.ts index e9cd6359e..82b427551 100644 --- a/extensions/zalo/src/onboarding.ts +++ b/extensions/zalo/src/onboarding.ts @@ -1,23 +1,30 @@ import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy, + ClawdbotConfig, WizardPrompter, } from "clawdbot/plugin-sdk"; +import { + addWildcardAllowFrom, + DEFAULT_ACCOUNT_ID, + normalizeAccountId, + promptAccountId, +} from "clawdbot/plugin-sdk"; -import { addWildcardAllowFrom, promptAccountId } from "./shared/onboarding.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./shared/account-ids.js"; import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount, } from "./accounts.js"; -import type { CoreConfig } from "./types.js"; const channel = "zalo" as const; type UpdateMode = "polling" | "webhook"; -function setZaloDmPolicy(cfg: CoreConfig, dmPolicy: "pairing" | "allowlist" | "open" | "disabled") { +function setZaloDmPolicy( + cfg: ClawdbotConfig, + dmPolicy: "pairing" | "allowlist" | "open" | "disabled", +) { const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.zalo?.allowFrom) : undefined; return { ...cfg, @@ -29,17 +36,17 @@ function setZaloDmPolicy(cfg: CoreConfig, dmPolicy: "pairing" | "allowlist" | "o ...(allowFrom ? { allowFrom } : {}), }, }, - } as CoreConfig; + } as ClawdbotConfig; } function setZaloUpdateMode( - cfg: CoreConfig, + cfg: ClawdbotConfig, accountId: string, mode: UpdateMode, webhookUrl?: string, webhookSecret?: string, webhookPath?: string, -): CoreConfig { +): ClawdbotConfig { const isDefault = accountId === DEFAULT_ACCOUNT_ID; if (mode === "polling") { if (isDefault) { @@ -55,7 +62,7 @@ function setZaloUpdateMode( ...cfg.channels, zalo: rest, }, - } as CoreConfig; + } as ClawdbotConfig; } const accounts = { ...(cfg.channels?.zalo?.accounts ?? {}) } as Record< string, @@ -78,7 +85,7 @@ function setZaloUpdateMode( accounts, }, }, - } as CoreConfig; + } as ClawdbotConfig; } if (isDefault) { @@ -93,7 +100,7 @@ function setZaloUpdateMode( webhookPath, }, }, - } as CoreConfig; + } as ClawdbotConfig; } const accounts = { ...(cfg.channels?.zalo?.accounts ?? {}) } as Record< @@ -115,7 +122,7 @@ function setZaloUpdateMode( accounts, }, }, - } as CoreConfig; + } as ClawdbotConfig; } async function noteZaloTokenHelp(prompter: WizardPrompter): Promise { @@ -132,10 +139,10 @@ async function noteZaloTokenHelp(prompter: WizardPrompter): Promise { } async function promptZaloAllowFrom(params: { - cfg: CoreConfig; + cfg: ClawdbotConfig; prompter: WizardPrompter; accountId: string; -}): Promise { +}): Promise { const { cfg, prompter, accountId } = params; const resolved = resolveZaloAccount({ cfg, accountId }); const existingAllowFrom = resolved.config.allowFrom ?? []; @@ -169,7 +176,7 @@ async function promptZaloAllowFrom(params: { allowFrom: unique, }, }, - } as CoreConfig; + } as ClawdbotConfig; } return { @@ -190,7 +197,7 @@ async function promptZaloAllowFrom(params: { }, }, }, - } as CoreConfig; + } as ClawdbotConfig; } const dmPolicy: ChannelOnboardingDmPolicy = { @@ -199,15 +206,15 @@ const dmPolicy: ChannelOnboardingDmPolicy = { policyKey: "channels.zalo.dmPolicy", allowFromKey: "channels.zalo.allowFrom", getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing", - setPolicy: (cfg, policy) => setZaloDmPolicy(cfg as CoreConfig, policy), + setPolicy: (cfg, policy) => setZaloDmPolicy(cfg as ClawdbotConfig, policy), }; export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { channel, dmPolicy, getStatus: async ({ cfg }) => { - const configured = listZaloAccountIds(cfg as CoreConfig).some((accountId) => - Boolean(resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }).token), + const configured = listZaloAccountIds(cfg as ClawdbotConfig).some((accountId) => + Boolean(resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }).token), ); return { channel, @@ -219,13 +226,13 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { }, configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds, forceAllowFrom }) => { const zaloOverride = accountOverrides.zalo?.trim(); - const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg as CoreConfig); + const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg as ClawdbotConfig); let zaloAccountId = zaloOverride ? normalizeAccountId(zaloOverride) : defaultZaloAccountId; if (shouldPromptAccountIds && !zaloOverride) { zaloAccountId = await promptAccountId({ - cfg: cfg as CoreConfig, + cfg: cfg as ClawdbotConfig, prompter, label: "Zalo", currentId: zaloAccountId, @@ -234,7 +241,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { }); } - let next = cfg as CoreConfig; + let next = cfg as ClawdbotConfig; const resolvedAccount = resolveZaloAccount({ cfg: next, accountId: zaloAccountId }); const accountConfigured = Boolean(resolvedAccount.token); const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID; @@ -262,7 +269,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { enabled: true, }, }, - } as CoreConfig; + } as ClawdbotConfig; } else { token = String( await prompter.text({ @@ -305,7 +312,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { botToken: token, }, }, - } as CoreConfig; + } as ClawdbotConfig; } else { next = { ...next, @@ -324,7 +331,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { }, }, }, - } as CoreConfig; + } as ClawdbotConfig; } } diff --git a/extensions/zalo/src/runtime.ts b/extensions/zalo/src/runtime.ts new file mode 100644 index 000000000..ab67f8dbf --- /dev/null +++ b/extensions/zalo/src/runtime.ts @@ -0,0 +1,14 @@ +import type { PluginRuntime } from "clawdbot/plugin-sdk"; + +let runtime: PluginRuntime | null = null; + +export function setZaloRuntime(next: PluginRuntime): void { + runtime = next; +} + +export function getZaloRuntime(): PluginRuntime { + if (!runtime) { + throw new Error("Zalo runtime not initialized"); + } + return runtime; +} diff --git a/extensions/zalo/src/send.ts b/extensions/zalo/src/send.ts index 18fba3301..e14d2bf36 100644 --- a/extensions/zalo/src/send.ts +++ b/extensions/zalo/src/send.ts @@ -1,4 +1,5 @@ -import type { CoreConfig } from "./types.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; + import type { ZaloFetch } from "./api.js"; import { sendMessage, sendPhoto } from "./api.js"; import { resolveZaloAccount } from "./accounts.js"; @@ -8,7 +9,7 @@ import { resolveZaloToken } from "./token.js"; export type ZaloSendOptions = { token?: string; accountId?: string; - cfg?: CoreConfig; + cfg?: ClawdbotConfig; mediaUrl?: string; caption?: string; verbose?: boolean; diff --git a/extensions/zalo/src/shared/account-ids.ts b/extensions/zalo/src/shared/account-ids.ts deleted file mode 100644 index 5edcd8376..000000000 --- a/extensions/zalo/src/shared/account-ids.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const DEFAULT_ACCOUNT_ID = "default"; - -export function normalizeAccountId(value: string | undefined | null): string { - const trimmed = (value ?? "").trim(); - if (!trimmed) return DEFAULT_ACCOUNT_ID; - if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed; - return ( - trimmed - .toLowerCase() - .replace(/[^a-z0-9_-]+/g, "-") - .replace(/^-+/, "") - .replace(/-+$/, "") - .slice(0, 64) || DEFAULT_ACCOUNT_ID - ); -} diff --git a/extensions/zalo/src/shared/channel-config.ts b/extensions/zalo/src/shared/channel-config.ts deleted file mode 100644 index 184b5cf12..000000000 --- a/extensions/zalo/src/shared/channel-config.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { DEFAULT_ACCOUNT_ID } from "./account-ids.js"; - -type ChannelSection = { - accounts?: Record>; - enabled?: boolean; -}; - -type ConfigWithChannels = { - channels?: Record; -}; - -export function setAccountEnabledInConfigSection(params: { - cfg: T; - sectionKey: string; - accountId: string; - enabled: boolean; - allowTopLevel?: boolean; -}): T { - const accountKey = params.accountId || DEFAULT_ACCOUNT_ID; - const channels = params.cfg.channels; - const base = (channels?.[params.sectionKey] as ChannelSection | undefined) ?? undefined; - const hasAccounts = Boolean(base?.accounts); - if (params.allowTopLevel && accountKey === DEFAULT_ACCOUNT_ID && !hasAccounts) { - return { - ...params.cfg, - channels: { - ...channels, - [params.sectionKey]: { - ...base, - enabled: params.enabled, - }, - }, - } as T; - } - - const baseAccounts = (base?.accounts ?? {}) as Record>; - const existing = baseAccounts[accountKey] ?? {}; - return { - ...params.cfg, - channels: { - ...channels, - [params.sectionKey]: { - ...base, - accounts: { - ...baseAccounts, - [accountKey]: { - ...existing, - enabled: params.enabled, - }, - }, - }, - }, - } as T; -} - -export function deleteAccountFromConfigSection(params: { - cfg: T; - sectionKey: string; - accountId: string; - clearBaseFields?: string[]; -}): T { - const accountKey = params.accountId || DEFAULT_ACCOUNT_ID; - const channels = params.cfg.channels as Record | undefined; - const base = (channels?.[params.sectionKey] as ChannelSection | undefined) ?? undefined; - if (!base) return params.cfg; - - const baseAccounts = - base.accounts && typeof base.accounts === "object" ? { ...base.accounts } : undefined; - - if (accountKey !== DEFAULT_ACCOUNT_ID) { - const accounts = baseAccounts ? { ...baseAccounts } : {}; - delete accounts[accountKey]; - return { - ...params.cfg, - channels: { - ...channels, - [params.sectionKey]: { - ...base, - accounts: Object.keys(accounts).length ? accounts : undefined, - }, - }, - } as T; - } - - if (baseAccounts && Object.keys(baseAccounts).length > 0) { - delete baseAccounts[accountKey]; - const baseRecord = { ...(base as Record) }; - for (const field of params.clearBaseFields ?? []) { - if (field in baseRecord) baseRecord[field] = undefined; - } - return { - ...params.cfg, - channels: { - ...channels, - [params.sectionKey]: { - ...baseRecord, - accounts: Object.keys(baseAccounts).length ? baseAccounts : undefined, - }, - }, - } as T; - } - - const nextChannels = { ...channels } as Record; - delete nextChannels[params.sectionKey]; - const nextCfg = { ...params.cfg } as T; - if (Object.keys(nextChannels).length > 0) { - nextCfg.channels = nextChannels as T["channels"]; - } else { - delete nextCfg.channels; - } - return nextCfg; -} diff --git a/extensions/zalo/src/shared/channel-setup.ts b/extensions/zalo/src/shared/channel-setup.ts deleted file mode 100644 index b164ed18f..000000000 --- a/extensions/zalo/src/shared/channel-setup.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./account-ids.js"; - -type ConfigWithChannels = { - channels?: Record; -}; - -type ChannelSectionBase = { - name?: string; - accounts?: Record>; -}; - -function channelHasAccounts(cfg: ConfigWithChannels, channelKey: string): boolean { - const channels = cfg.channels as Record | undefined; - const base = channels?.[channelKey] as ChannelSectionBase | undefined; - return Boolean(base?.accounts && Object.keys(base.accounts).length > 0); -} - -function shouldStoreNameInAccounts(params: { - cfg: ConfigWithChannels; - channelKey: string; - accountId: string; - alwaysUseAccounts?: boolean; -}): boolean { - if (params.alwaysUseAccounts) return true; - if (params.accountId !== DEFAULT_ACCOUNT_ID) return true; - return channelHasAccounts(params.cfg, params.channelKey); -} - -export function applyAccountNameToChannelSection(params: { - cfg: T; - channelKey: string; - accountId: string; - name?: string; - alwaysUseAccounts?: boolean; -}): T { - const trimmed = params.name?.trim(); - if (!trimmed) return params.cfg; - const accountId = normalizeAccountId(params.accountId); - const channels = params.cfg.channels as Record | undefined; - const baseConfig = channels?.[params.channelKey]; - const base = - typeof baseConfig === "object" && baseConfig ? (baseConfig as ChannelSectionBase) : undefined; - const useAccounts = shouldStoreNameInAccounts({ - cfg: params.cfg, - channelKey: params.channelKey, - accountId, - alwaysUseAccounts: params.alwaysUseAccounts, - }); - if (!useAccounts && accountId === DEFAULT_ACCOUNT_ID) { - const safeBase = base ?? {}; - return { - ...params.cfg, - channels: { - ...channels, - [params.channelKey]: { - ...safeBase, - name: trimmed, - }, - }, - } as T; - } - const baseAccounts: Record> = base?.accounts ?? {}; - const existingAccount = baseAccounts[accountId] ?? {}; - const baseWithoutName = - accountId === DEFAULT_ACCOUNT_ID - ? (({ name: _ignored, ...rest }) => rest)(base ?? {}) - : (base ?? {}); - return { - ...params.cfg, - channels: { - ...channels, - [params.channelKey]: { - ...baseWithoutName, - accounts: { - ...baseAccounts, - [accountId]: { - ...existingAccount, - name: trimmed, - }, - }, - }, - }, - } as T; -} - -export function migrateBaseNameToDefaultAccount(params: { - cfg: T; - channelKey: string; - alwaysUseAccounts?: boolean; -}): T { - if (params.alwaysUseAccounts) return params.cfg; - const channels = params.cfg.channels as Record | undefined; - const base = channels?.[params.channelKey] as ChannelSectionBase | undefined; - const baseName = base?.name?.trim(); - if (!baseName) return params.cfg; - const accounts: Record> = { - ...base?.accounts, - }; - const defaultAccount = accounts[DEFAULT_ACCOUNT_ID] ?? {}; - if (!defaultAccount.name) { - accounts[DEFAULT_ACCOUNT_ID] = { ...defaultAccount, name: baseName }; - } - const { name: _ignored, ...rest } = base ?? {}; - return { - ...params.cfg, - channels: { - ...channels, - [params.channelKey]: { - ...rest, - accounts, - }, - }, - } as T; -} diff --git a/extensions/zalo/src/shared/onboarding.ts b/extensions/zalo/src/shared/onboarding.ts deleted file mode 100644 index d9b633f18..000000000 --- a/extensions/zalo/src/shared/onboarding.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { WizardPrompter } from "clawdbot/plugin-sdk"; - -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./account-ids.js"; - -export type PromptAccountIdParams = { - cfg: TConfig; - prompter: WizardPrompter; - label: string; - currentId?: string; - listAccountIds: (cfg: TConfig) => string[]; - defaultAccountId: string; -}; - -export async function promptAccountId( - params: PromptAccountIdParams, -): Promise { - const existingIds = params.listAccountIds(params.cfg); - const initial = params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID; - const choice = (await params.prompter.select({ - message: `${params.label} account`, - options: [ - ...existingIds.map((id) => ({ - value: id, - label: id === DEFAULT_ACCOUNT_ID ? "default (primary)" : id, - })), - { value: "__new__", label: "Add a new account" }, - ], - initialValue: initial, - })) as string; - - if (choice !== "__new__") return normalizeAccountId(choice); - - const entered = await params.prompter.text({ - message: `New ${params.label} account id`, - validate: (value) => (value?.trim() ? undefined : "Required"), - }); - const normalized = normalizeAccountId(String(entered)); - if (String(entered).trim() !== normalized) { - await params.prompter.note( - `Normalized account id to "${normalized}".`, - `${params.label} account`, - ); - } - return normalized; -} - -export function addWildcardAllowFrom( - allowFrom?: Array | null, -): Array { - const next = (allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean); - if (!next.includes("*")) next.push("*"); - return next; -} diff --git a/extensions/zalo/src/shared/pairing.ts b/extensions/zalo/src/shared/pairing.ts deleted file mode 100644 index 91e75fbab..000000000 --- a/extensions/zalo/src/shared/pairing.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const PAIRING_APPROVED_MESSAGE = - "\u2705 Clawdbot access approved. Send a message to start chatting."; - -export function formatPairingApproveHint(channelId: string): string { - return `Approve via: clawdbot pairing list ${channelId} / clawdbot pairing approve ${channelId} `; -} diff --git a/extensions/zalo/src/token.ts b/extensions/zalo/src/token.ts index be3ee5dd9..41c372666 100644 --- a/extensions/zalo/src/token.ts +++ b/extensions/zalo/src/token.ts @@ -1,7 +1,8 @@ import { readFileSync } from "node:fs"; +import { DEFAULT_ACCOUNT_ID } from "clawdbot/plugin-sdk"; + import type { ZaloConfig } from "./types.js"; -import { DEFAULT_ACCOUNT_ID } from "./shared/account-ids.js"; export type ZaloTokenResolution = { token: string; diff --git a/extensions/zalo/src/tool-helpers.ts b/extensions/zalo/src/tool-helpers.ts deleted file mode 100644 index 358be47ac..000000000 --- a/extensions/zalo/src/tool-helpers.ts +++ /dev/null @@ -1,30 +0,0 @@ -export function readStringParam( - params: Record, - key: string, - opts?: { required?: boolean; allowEmpty?: boolean; trim?: boolean }, -): string | undefined { - const raw = params[key]; - if (raw === undefined || raw === null) { - if (opts?.required) throw new Error(`${key} is required`); - return undefined; - } - const value = String(raw); - const trimmed = opts?.trim === false ? value : value.trim(); - if (!opts?.allowEmpty && !trimmed) { - if (opts?.required) throw new Error(`${key} is required`); - return undefined; - } - return trimmed; -} - -export function jsonResult(payload: unknown) { - return { - content: [ - { - type: "text", - text: JSON.stringify(payload, null, 2), - }, - ], - details: payload, - }; -} diff --git a/extensions/zalo/src/types.ts b/extensions/zalo/src/types.ts index 8309654c7..6b17da99f 100644 --- a/extensions/zalo/src/types.ts +++ b/extensions/zalo/src/types.ts @@ -40,10 +40,3 @@ export type ResolvedZaloAccount = { tokenSource: ZaloTokenSource; config: ZaloAccountConfig; }; - -export type CoreConfig = { - channels?: { - zalo?: ZaloConfig; - }; - [key: string]: unknown; -}; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index fc6b67dc6..2d64a65f2 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -138,7 +138,7 @@ export type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy, } from "../channels/plugins/onboarding-types.js"; -export { addWildcardAllowFrom } from "../channels/plugins/onboarding/helpers.js"; +export { addWildcardAllowFrom, promptAccountId } from "../channels/plugins/onboarding/helpers.js"; export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js"; export { diff --git a/src/plugins/runtime/types.ts b/src/plugins/runtime/types.ts index 9b3ac02e2..54494f222 100644 --- a/src/plugins/runtime/types.ts +++ b/src/plugins/runtime/types.ts @@ -41,7 +41,14 @@ export type PluginRuntime = { channel: string; accountId: string; peer: { kind: "dm" | "group" | "channel"; id: string }; - }) => { sessionKey: string; accountId: string }; + }) => { + agentId: string; + channel: string; + accountId: string; + sessionKey: string; + mainSessionKey: string; + matchedBy: string; + }; }; pairing: { buildPairingReply: (params: { channel: string; idLine: string; code: string }) => string;