feat: migrate zalouser plugin to sdk

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Peter Steinberger
2026-01-18 04:23:25 +00:00
parent b105745299
commit 89c5185f1c
9 changed files with 152 additions and 397 deletions

View File

@@ -15,6 +15,19 @@ Docs: https://docs.clawd.bot
## 2026.1.18-2 ## 2026.1.18-2
## 2026.1.17-6
### Changes
- Plugins: add exclusive plugin slots with a dedicated memory slot selector.
- Memory: ship core memory tools + CLI as the bundled `memory-core` plugin.
- 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.
- Plugins: migrate the Zalo Personal plugin to the shared plugin SDK runtime.
## 2026.1.17-5
### Changes ### Changes
- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback. - Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback.
- Memory: add SQLite embedding cache to speed up reindexing and frequent updates. - Memory: add SQLite embedding cache to speed up reindexing and frequent updates.

View File

@@ -2,12 +2,14 @@ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
import { zalouserPlugin } from "./src/channel.js"; import { zalouserPlugin } from "./src/channel.js";
import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js"; import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
import { setZalouserRuntime } from "./src/runtime.js";
const plugin = { const plugin = {
id: "zalouser", id: "zalouser",
name: "Zalo Personal", name: "Zalo Personal",
description: "Zalo personal account messaging via zca-cli", description: "Zalo personal account messaging via zca-cli",
register(api: ClawdbotPluginApi) { register(api: ClawdbotPluginApi) {
setZalouserRuntime(api.runtime);
// Register channel plugin (for onboarding & gateway) // Register channel plugin (for onboarding & gateway)
api.registerChannel(zalouserPlugin); api.registerChannel(zalouserPlugin);

View File

@@ -1,25 +1,22 @@
import { runZca, parseJsonOutput } from "./zca.js"; import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
import { import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
DEFAULT_ACCOUNT_ID,
type CoreConfig,
type ResolvedZalouserAccount,
type ZalouserAccountConfig,
type ZalouserConfig,
} from "./types.js";
function listConfiguredAccountIds(cfg: CoreConfig): string[] { import { runZca, parseJsonOutput } from "./zca.js";
import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js";
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts; const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
if (!accounts || typeof accounts !== "object") return []; if (!accounts || typeof accounts !== "object") return [];
return Object.keys(accounts).filter(Boolean); return Object.keys(accounts).filter(Boolean);
} }
export function listZalouserAccountIds(cfg: CoreConfig): string[] { export function listZalouserAccountIds(cfg: ClawdbotConfig): string[] {
const ids = listConfiguredAccountIds(cfg); const ids = listConfiguredAccountIds(cfg);
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
return ids.sort((a, b) => a.localeCompare(b)); return ids.sort((a, b) => a.localeCompare(b));
} }
export function resolveDefaultZalouserAccountId(cfg: CoreConfig): string { export function resolveDefaultZalouserAccountId(cfg: ClawdbotConfig): string {
const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined; const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined;
if (zalouserConfig?.defaultAccount?.trim()) return zalouserConfig.defaultAccount.trim(); if (zalouserConfig?.defaultAccount?.trim()) return zalouserConfig.defaultAccount.trim();
const ids = listZalouserAccountIds(cfg); const ids = listZalouserAccountIds(cfg);
@@ -27,14 +24,8 @@ export function resolveDefaultZalouserAccountId(cfg: CoreConfig): string {
return ids[0] ?? DEFAULT_ACCOUNT_ID; return ids[0] ?? DEFAULT_ACCOUNT_ID;
} }
export function normalizeAccountId(accountId?: string | null): string {
const trimmed = accountId?.trim();
if (!trimmed) return DEFAULT_ACCOUNT_ID;
return trimmed.toLowerCase();
}
function resolveAccountConfig( function resolveAccountConfig(
cfg: CoreConfig, cfg: ClawdbotConfig,
accountId: string, accountId: string,
): ZalouserAccountConfig | undefined { ): ZalouserAccountConfig | undefined {
const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts; const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
@@ -42,7 +33,10 @@ function resolveAccountConfig(
return accounts[accountId] as ZalouserAccountConfig | undefined; return accounts[accountId] as ZalouserAccountConfig | undefined;
} }
function mergeZalouserAccountConfig(cfg: CoreConfig, accountId: string): ZalouserAccountConfig { function mergeZalouserAccountConfig(
cfg: ClawdbotConfig,
accountId: string,
): ZalouserAccountConfig {
const raw = (cfg.channels?.zalouser ?? {}) as ZalouserConfig; const raw = (cfg.channels?.zalouser ?? {}) as ZalouserConfig;
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw; const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
const account = resolveAccountConfig(cfg, accountId) ?? {}; const account = resolveAccountConfig(cfg, accountId) ?? {};
@@ -62,7 +56,7 @@ export async function checkZcaAuthenticated(profile: string): Promise<boolean> {
} }
export async function resolveZalouserAccount(params: { export async function resolveZalouserAccount(params: {
cfg: CoreConfig; cfg: ClawdbotConfig;
accountId?: string | null; accountId?: string | null;
}): Promise<ResolvedZalouserAccount> { }): Promise<ResolvedZalouserAccount> {
const accountId = normalizeAccountId(params.accountId); const accountId = normalizeAccountId(params.accountId);
@@ -84,7 +78,7 @@ export async function resolveZalouserAccount(params: {
} }
export function resolveZalouserAccountSync(params: { export function resolveZalouserAccountSync(params: {
cfg: CoreConfig; cfg: ClawdbotConfig;
accountId?: string | null; accountId?: string | null;
}): ResolvedZalouserAccount { }): ResolvedZalouserAccount {
const accountId = normalizeAccountId(params.accountId); const accountId = normalizeAccountId(params.accountId);
@@ -104,7 +98,9 @@ export function resolveZalouserAccountSync(params: {
}; };
} }
export async function listEnabledZalouserAccounts(cfg: CoreConfig): Promise<ResolvedZalouserAccount[]> { export async function listEnabledZalouserAccounts(
cfg: ClawdbotConfig,
): Promise<ResolvedZalouserAccount[]> {
const ids = listZalouserAccountIds(cfg); const ids = listZalouserAccountIds(cfg);
const accounts = await Promise.all( const accounts = await Promise.all(
ids.map((accountId) => resolveZalouserAccount({ cfg, accountId })) ids.map((accountId) => resolveZalouserAccount({ cfg, accountId }))

View File

@@ -1,6 +1,16 @@
import type { ChannelAccountSnapshot, ChannelDirectoryEntry, ChannelPlugin } from "clawdbot/plugin-sdk"; import type {
ChannelAccountSnapshot,
import { formatPairingApproveHint } from "clawdbot/plugin-sdk"; ChannelDirectoryEntry,
ChannelPlugin,
ClawdbotConfig,
} from "clawdbot/plugin-sdk";
import {
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
formatPairingApproveHint,
normalizeAccountId,
setAccountEnabledInConfigSection,
} from "clawdbot/plugin-sdk";
import { import {
listZalouserAccountIds, listZalouserAccountIds,
resolveDefaultZalouserAccountId, resolveDefaultZalouserAccountId,
@@ -12,14 +22,7 @@ import {
import { zalouserOnboardingAdapter } from "./onboarding.js"; import { zalouserOnboardingAdapter } from "./onboarding.js";
import { sendMessageZalouser } from "./send.js"; import { sendMessageZalouser } from "./send.js";
import { checkZcaInstalled, parseJsonOutput, runZca, runZcaInteractive } from "./zca.js"; import { checkZcaInstalled, parseJsonOutput, runZca, runZcaInteractive } from "./zca.js";
import { import type { ZcaFriend, ZcaGroup, ZcaUserInfo } from "./types.js";
DEFAULT_ACCOUNT_ID,
type CoreConfig,
type ZalouserConfig,
type ZcaFriend,
type ZcaGroup,
type ZcaUserInfo,
} from "./types.js";
const meta = { const meta = {
id: "zalouser", id: "zalouser",
@@ -34,7 +37,7 @@ const meta = {
}; };
function resolveZalouserQrProfile(accountId?: string | null): string { function resolveZalouserQrProfile(accountId?: string | null): string {
const normalized = String(accountId ?? "").trim(); const normalized = normalizeAccountId(accountId);
if (!normalized || normalized === DEFAULT_ACCOUNT_ID) { if (!normalized || normalized === DEFAULT_ACCOUNT_ID) {
return process.env.ZCA_PROFILE?.trim() || "default"; return process.env.ZCA_PROFILE?.trim() || "default";
} }
@@ -69,65 +72,6 @@ function mapGroup(params: {
}; };
} }
function deleteAccountFromConfigSection(params: {
cfg: CoreConfig;
accountId: string;
}): CoreConfig {
const { cfg, accountId } = params;
if (accountId === DEFAULT_ACCOUNT_ID) {
const { zalouser: _removed, ...restChannels } = cfg.channels ?? {};
return { ...cfg, channels: restChannels };
}
const accounts = { ...(cfg.channels?.zalouser?.accounts ?? {}) };
delete accounts[accountId];
return {
...cfg,
channels: {
...cfg.channels,
zalouser: {
...cfg.channels?.zalouser,
accounts,
},
},
};
}
function setAccountEnabledInConfigSection(params: {
cfg: CoreConfig;
accountId: string;
enabled: boolean;
}): CoreConfig {
const { cfg, accountId, enabled } = params;
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...cfg,
channels: {
...cfg.channels,
zalouser: {
...cfg.channels?.zalouser,
enabled,
},
},
};
}
return {
...cfg,
channels: {
...cfg.channels,
zalouser: {
...cfg.channels?.zalouser,
accounts: {
...(cfg.channels?.zalouser?.accounts ?? {}),
[accountId]: {
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
enabled,
},
},
},
},
};
}
export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = { export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
id: "zalouser", id: "zalouser",
meta, meta,
@@ -143,20 +87,24 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
}, },
reload: { configPrefixes: ["channels.zalouser"] }, reload: { configPrefixes: ["channels.zalouser"] },
config: { config: {
listAccountIds: (cfg) => listZalouserAccountIds(cfg as CoreConfig), listAccountIds: (cfg) => listZalouserAccountIds(cfg as ClawdbotConfig),
resolveAccount: (cfg, accountId) => resolveAccount: (cfg, accountId) =>
resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }), resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId }),
defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg as CoreConfig), defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg as ClawdbotConfig),
setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({ setAccountEnabledInConfigSection({
cfg: cfg as CoreConfig, cfg: cfg as ClawdbotConfig,
sectionKey: "zalouser",
accountId, accountId,
enabled, enabled,
allowTopLevel: true,
}), }),
deleteAccount: ({ cfg, accountId }) => deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({ deleteAccountFromConfigSection({
cfg: cfg as CoreConfig, cfg: cfg as ClawdbotConfig,
sectionKey: "zalouser",
accountId, accountId,
clearBaseFields: ["profile", "name", "dmPolicy", "allowFrom", "groupPolicy", "groups", "messagePrefix"],
}), }),
isConfigured: async (account) => { isConfigured: async (account) => {
// Check if zca auth status is OK for this profile // Check if zca auth status is OK for this profile
@@ -173,7 +121,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
configured: undefined, configured: undefined,
}), }),
resolveAllowFrom: ({ cfg, accountId }) => resolveAllowFrom: ({ cfg, accountId }) =>
(resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map( (resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId }).config.allowFrom ?? []).map(
(entry) => String(entry), (entry) => String(entry),
), ),
formatAllowFrom: ({ allowFrom }) => formatAllowFrom: ({ allowFrom }) =>
@@ -187,7 +135,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean( const useAccountPath = Boolean(
(cfg as CoreConfig).channels?.zalouser?.accounts?.[resolvedAccountId], (cfg as ClawdbotConfig).channels?.zalouser?.accounts?.[resolvedAccountId],
); );
const basePath = useAccountPath const basePath = useAccountPath
? `channels.zalouser.accounts.${resolvedAccountId}.` ? `channels.zalouser.accounts.${resolvedAccountId}.`
@@ -227,7 +175,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
self: async ({ cfg, accountId, runtime }) => { self: async ({ cfg, accountId, runtime }) => {
const ok = await checkZcaInstalled(); const ok = await checkZcaInstalled();
if (!ok) throw new Error("Missing dependency: `zca` not found in PATH"); if (!ok) throw new Error("Missing dependency: `zca` not found in PATH");
const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }); const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
const result = await runZca(["me", "info", "-j"], { profile: account.profile, timeout: 10000 }); const result = await runZca(["me", "info", "-j"], { profile: account.profile, timeout: 10000 });
if (!result.ok) { if (!result.ok) {
runtime.error(result.stderr || "Failed to fetch profile"); runtime.error(result.stderr || "Failed to fetch profile");
@@ -245,7 +193,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
listPeers: async ({ cfg, accountId, query, limit }) => { listPeers: async ({ cfg, accountId, query, limit }) => {
const ok = await checkZcaInstalled(); const ok = await checkZcaInstalled();
if (!ok) throw new Error("Missing dependency: `zca` not found in PATH"); if (!ok) throw new Error("Missing dependency: `zca` not found in PATH");
const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }); const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
const args = query?.trim() const args = query?.trim()
? ["friend", "find", query.trim()] ? ["friend", "find", query.trim()]
: ["friend", "list", "-j"]; : ["friend", "list", "-j"];
@@ -269,7 +217,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
listGroups: async ({ cfg, accountId, query, limit }) => { listGroups: async ({ cfg, accountId, query, limit }) => {
const ok = await checkZcaInstalled(); const ok = await checkZcaInstalled();
if (!ok) throw new Error("Missing dependency: `zca` not found in PATH"); if (!ok) throw new Error("Missing dependency: `zca` not found in PATH");
const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }); const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
const result = await runZca(["group", "list", "-j"], { profile: account.profile, timeout: 15000 }); const result = await runZca(["group", "list", "-j"], { profile: account.profile, timeout: 15000 });
if (!result.ok) { if (!result.ok) {
throw new Error(result.stderr || "Failed to list groups"); throw new Error(result.stderr || "Failed to list groups");
@@ -293,7 +241,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
listGroupMembers: async ({ cfg, accountId, groupId, limit }) => { listGroupMembers: async ({ cfg, accountId, groupId, limit }) => {
const ok = await checkZcaInstalled(); const ok = await checkZcaInstalled();
if (!ok) throw new Error("Missing dependency: `zca` not found in PATH"); if (!ok) throw new Error("Missing dependency: `zca` not found in PATH");
const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }); const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
const result = await runZca(["group", "members", groupId, "-j"], { const result = await runZca(["group", "members", groupId, "-j"], {
profile: account.profile, profile: account.profile,
timeout: 20000, timeout: 20000,
@@ -335,7 +283,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
} }
try { try {
const account = resolveZalouserAccountSync({ const account = resolveZalouserAccountSync({
cfg: cfg as CoreConfig, cfg: cfg as ClawdbotConfig,
accountId: accountId ?? DEFAULT_ACCOUNT_ID, accountId: accountId ?? DEFAULT_ACCOUNT_ID,
}); });
const args = const args =
@@ -391,7 +339,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
idLabel: "zalouserUserId", idLabel: "zalouserUserId",
normalizeAllowEntry: (entry) => entry.replace(/^(zalouser|zlu):/i, ""), normalizeAllowEntry: (entry) => entry.replace(/^(zalouser|zlu):/i, ""),
notifyApproval: async ({ cfg, id }) => { notifyApproval: async ({ cfg, id }) => {
const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig }); const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig });
const authenticated = await checkZcaAuthenticated(account.profile); const authenticated = await checkZcaAuthenticated(account.profile);
if (!authenticated) throw new Error("Zalouser not authenticated"); if (!authenticated) throw new Error("Zalouser not authenticated");
await sendMessageZalouser(id, "Your pairing request has been approved.", { await sendMessageZalouser(id, "Your pairing request has been approved.", {
@@ -402,7 +350,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
auth: { auth: {
login: async ({ cfg, accountId, runtime }) => { login: async ({ cfg, accountId, runtime }) => {
const account = resolveZalouserAccountSync({ const account = resolveZalouserAccountSync({
cfg: cfg as CoreConfig, cfg: cfg as ClawdbotConfig,
accountId: accountId ?? DEFAULT_ACCOUNT_ID, accountId: accountId ?? DEFAULT_ACCOUNT_ID,
}); });
const ok = await checkZcaInstalled(); const ok = await checkZcaInstalled();
@@ -445,7 +393,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
}, },
textChunkLimit: 2000, textChunkLimit: 2000,
sendText: async ({ to, text, accountId, cfg }) => { sendText: async ({ to, text, accountId, cfg }) => {
const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }); const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
const result = await sendMessageZalouser(to, text, { profile: account.profile }); const result = await sendMessageZalouser(to, text, { profile: account.profile });
return { return {
channel: "zalouser", channel: "zalouser",
@@ -455,7 +403,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
}; };
}, },
sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => { sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }); const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
const result = await sendMessageZalouser(to, text, { const result = await sendMessageZalouser(to, text, {
profile: account.profile, profile: account.profile,
mediaUrl, mediaUrl,
@@ -534,7 +482,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
const { monitorZalouserProvider } = await import("./monitor.js"); const { monitorZalouserProvider } = await import("./monitor.js");
return monitorZalouserProvider({ return monitorZalouserProvider({
account, account,
config: ctx.cfg as CoreConfig, config: ctx.cfg as ClawdbotConfig,
runtime: ctx.runtime, runtime: ctx.runtime,
abortSignal: ctx.abortSignal, abortSignal: ctx.abortSignal,
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }), statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),

View File

@@ -1,171 +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<void>;
onError?: (err: unknown, info: { kind: string }) => void;
};
}) => Promise<void>;
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<string[]>;
upsertChannelPairingRequest: (params: {
channel: string;
id: string;
meta?: { name?: string };
}) => 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<CoreChannelDeps> | 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<string>();
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<T>(relativePath: string): Promise<T> {
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<CoreChannelDeps> {
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;
}

View File

@@ -1,8 +1,9 @@
import type { ChildProcess } from "node:child_process"; import type { ChildProcess } from "node:child_process";
import type { RuntimeEnv } from "clawdbot/plugin-sdk"; import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk";
import { import {
finalizeInboundContext, finalizeInboundContext,
formatAgentEnvelope,
isControlCommandMessage, isControlCommandMessage,
mergeAllowlist, mergeAllowlist,
recordSessionMetaFromInbound, recordSessionMetaFromInbound,
@@ -11,20 +12,19 @@ import {
shouldComputeCommandAuthorized, shouldComputeCommandAuthorized,
summarizeMapping, summarizeMapping,
} from "clawdbot/plugin-sdk"; } from "clawdbot/plugin-sdk";
import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js";
import { sendMessageZalouser } from "./send.js"; import { sendMessageZalouser } from "./send.js";
import type { import type {
CoreConfig,
ResolvedZalouserAccount, ResolvedZalouserAccount,
ZcaFriend, ZcaFriend,
ZcaGroup, ZcaGroup,
ZcaMessage, ZcaMessage,
} from "./types.js"; } from "./types.js";
import { getZalouserRuntime } from "./runtime.js";
import { parseJsonOutput, runZca, runZcaStreaming } from "./zca.js"; import { parseJsonOutput, runZca, runZcaStreaming } from "./zca.js";
export type ZalouserMonitorOptions = { export type ZalouserMonitorOptions = {
account: ResolvedZalouserAccount; account: ResolvedZalouserAccount;
config: CoreConfig; config: ClawdbotConfig;
runtime: RuntimeEnv; runtime: RuntimeEnv;
abortSignal: AbortSignal; abortSignal: AbortSignal;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
@@ -55,8 +55,10 @@ function buildNameIndex<T>(
return index; return index;
} }
function logVerbose(deps: CoreChannelDeps, runtime: RuntimeEnv, message: string): void { type ZalouserCoreRuntime = ReturnType<typeof getZalouserRuntime>;
if (deps.shouldLogVerbose()) {
function logVerbose(core: ZalouserCoreRuntime, runtime: RuntimeEnv, message: string): void {
if (core.logging.shouldLogVerbose()) {
runtime.log(`[zalouser] ${message}`); runtime.log(`[zalouser] ${message}`);
} }
} }
@@ -157,8 +159,8 @@ function startZcaListener(
async function processMessage( async function processMessage(
message: ZcaMessage, message: ZcaMessage,
account: ResolvedZalouserAccount, account: ResolvedZalouserAccount,
config: CoreConfig, config: ClawdbotConfig,
deps: CoreChannelDeps, core: ZalouserCoreRuntime,
runtime: RuntimeEnv, runtime: RuntimeEnv,
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void, statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
): Promise<void> { ): Promise<void> {
@@ -176,13 +178,13 @@ async function processMessage(
const groups = account.config.groups ?? {}; const groups = account.config.groups ?? {};
if (isGroup) { if (isGroup) {
if (groupPolicy === "disabled") { if (groupPolicy === "disabled") {
logVerbose(deps, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`); logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
return; return;
} }
if (groupPolicy === "allowlist") { if (groupPolicy === "allowlist") {
const allowed = isGroupAllowed({ groupId: chatId, groupName, groups }); const allowed = isGroupAllowed({ groupId: chatId, groupName, groups });
if (!allowed) { if (!allowed) {
logVerbose(deps, runtime, `zalouser: drop group ${chatId} (not allowlisted)`); logVerbose(core, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
return; return;
} }
} }
@@ -194,7 +196,7 @@ async function processMessage(
const shouldComputeAuth = shouldComputeCommandAuthorized(rawBody, config); const shouldComputeAuth = shouldComputeCommandAuthorized(rawBody, config);
const storeAllowFrom = const storeAllowFrom =
!isGroup && (dmPolicy !== "open" || shouldComputeAuth) !isGroup && (dmPolicy !== "open" || shouldComputeAuth)
? await deps.readChannelAllowFromStore("zalouser").catch(() => []) ? await core.channel.pairing.readAllowFromStore("zalouser").catch(() => [])
: []; : [];
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
const useAccessGroups = config.commands?.useAccessGroups !== false; const useAccessGroups = config.commands?.useAccessGroups !== false;
@@ -208,7 +210,7 @@ async function processMessage(
if (!isGroup) { if (!isGroup) {
if (dmPolicy === "disabled") { if (dmPolicy === "disabled") {
logVerbose(deps, runtime, `Blocked zalouser DM from ${senderId} (dmPolicy=disabled)`); logVerbose(core, runtime, `Blocked zalouser DM from ${senderId} (dmPolicy=disabled)`);
return; return;
} }
@@ -217,18 +219,18 @@ async function processMessage(
if (!allowed) { if (!allowed) {
if (dmPolicy === "pairing") { if (dmPolicy === "pairing") {
const { code, created } = await deps.upsertChannelPairingRequest({ const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "zalouser", channel: "zalouser",
id: senderId, id: senderId,
meta: { name: senderName || undefined }, meta: { name: senderName || undefined },
}); });
if (created) { if (created) {
logVerbose(deps, runtime, `zalouser pairing request sender=${senderId}`); logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
try { try {
await sendMessageZalouser( await sendMessageZalouser(
chatId, chatId,
deps.buildPairingReply({ core.channel.pairing.buildPairingReply({
channel: "zalouser", channel: "zalouser",
idLine: `Your Zalo user id: ${senderId}`, idLine: `Your Zalo user id: ${senderId}`,
code, code,
@@ -238,7 +240,7 @@ async function processMessage(
statusSink?.({ lastOutboundAt: Date.now() }); statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) { } catch (err) {
logVerbose( logVerbose(
deps, core,
runtime, runtime,
`zalouser pairing reply failed for ${senderId}: ${String(err)}`, `zalouser pairing reply failed for ${senderId}: ${String(err)}`,
); );
@@ -246,7 +248,7 @@ async function processMessage(
} }
} else { } else {
logVerbose( logVerbose(
deps, core,
runtime, runtime,
`Blocked unauthorized zalouser sender ${senderId} (dmPolicy=${dmPolicy})`, `Blocked unauthorized zalouser sender ${senderId} (dmPolicy=${dmPolicy})`,
); );
@@ -257,13 +259,13 @@ async function processMessage(
} }
if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) { if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
logVerbose(deps, runtime, `zalouser: drop control command from unauthorized sender ${senderId}`); logVerbose(core, runtime, `zalouser: drop control command from unauthorized sender ${senderId}`);
return; return;
} }
const peer = isGroup ? { kind: "group" as const, id: chatId } : { kind: "group" as const, id: senderId }; const peer = isGroup ? { kind: "group" as const, id: chatId } : { kind: "group" as const, id: senderId };
const route = deps.resolveAgentRoute({ const route = core.channel.routing.resolveAgentRoute({
cfg: config, cfg: config,
channel: "zalouser", channel: "zalouser",
accountId: account.accountId, accountId: account.accountId,
@@ -275,7 +277,7 @@ async function processMessage(
}); });
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`; const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
const body = deps.formatAgentEnvelope({ const body = formatAgentEnvelope({
channel: "Zalo Personal", channel: "Zalo Personal",
from: fromLabel, from: fromLabel,
timestamp: timestamp ? timestamp * 1000 : undefined, timestamp: timestamp ? timestamp * 1000 : undefined,
@@ -313,7 +315,7 @@ async function processMessage(
runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`); runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
}); });
await deps.dispatchReplyWithBufferedBlockDispatcher({ await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload, ctx: ctxPayload,
cfg: config, cfg: config,
dispatcherOptions: { dispatcherOptions: {
@@ -324,7 +326,7 @@ async function processMessage(
chatId, chatId,
isGroup, isGroup,
runtime, runtime,
deps, core,
statusSink, statusSink,
}); });
}, },
@@ -343,10 +345,10 @@ async function deliverZalouserReply(params: {
chatId: string; chatId: string;
isGroup: boolean; isGroup: boolean;
runtime: RuntimeEnv; runtime: RuntimeEnv;
deps: CoreChannelDeps; core: ZalouserCoreRuntime;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
}): Promise<void> { }): Promise<void> {
const { payload, profile, chatId, isGroup, runtime, deps, statusSink } = params; const { payload, profile, chatId, isGroup, runtime, core, statusSink } = params;
const mediaList = payload.mediaUrls?.length const mediaList = payload.mediaUrls?.length
? payload.mediaUrls ? payload.mediaUrls
@@ -360,7 +362,7 @@ async function deliverZalouserReply(params: {
const caption = first ? payload.text : undefined; const caption = first ? payload.text : undefined;
first = false; first = false;
try { try {
logVerbose(deps, runtime, `Sending media to ${chatId}`); logVerbose(core, runtime, `Sending media to ${chatId}`);
await sendMessageZalouser(chatId, caption ?? "", { await sendMessageZalouser(chatId, caption ?? "", {
profile, profile,
mediaUrl, mediaUrl,
@@ -375,8 +377,8 @@ async function deliverZalouserReply(params: {
} }
if (payload.text) { if (payload.text) {
const chunks = deps.chunkMarkdownText(payload.text, ZALOUSER_TEXT_LIMIT); const chunks = core.channel.text.chunkMarkdownText(payload.text, ZALOUSER_TEXT_LIMIT);
logVerbose(deps, runtime, `Sending ${chunks.length} text chunk(s) to ${chatId}`); logVerbose(core, runtime, `Sending ${chunks.length} text chunk(s) to ${chatId}`);
for (const chunk of chunks) { for (const chunk of chunks) {
try { try {
await sendMessageZalouser(chatId, chunk, { profile, isGroup }); await sendMessageZalouser(chatId, chunk, { profile, isGroup });
@@ -394,7 +396,7 @@ export async function monitorZalouserProvider(
let { account, config } = options; let { account, config } = options;
const { abortSignal, statusSink, runtime } = options; const { abortSignal, statusSink, runtime } = options;
const deps = await loadCoreChannelDeps(); const core = getZalouserRuntime();
let stopped = false; let stopped = false;
let proc: ChildProcess | null = null; let proc: ChildProcess | null = null;
let restartTimer: ReturnType<typeof setTimeout> | null = null; let restartTimer: ReturnType<typeof setTimeout> | null = null;
@@ -506,7 +508,7 @@ export async function monitorZalouserProvider(
} }
logVerbose( logVerbose(
deps, core,
runtime, runtime,
`[${account.accountId}] starting zca listener (profile=${account.profile})`, `[${account.accountId}] starting zca listener (profile=${account.profile})`,
); );
@@ -515,16 +517,16 @@ export async function monitorZalouserProvider(
runtime, runtime,
account.profile, account.profile,
(msg) => { (msg) => {
logVerbose(deps, runtime, `[${account.accountId}] inbound message`); logVerbose(core, runtime, `[${account.accountId}] inbound message`);
statusSink?.({ lastInboundAt: Date.now() }); statusSink?.({ lastInboundAt: Date.now() });
processMessage(msg, account, config, deps, runtime, statusSink).catch((err) => { processMessage(msg, account, config, core, runtime, statusSink).catch((err) => {
runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`); runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
}); });
}, },
(err) => { (err) => {
runtime.error(`[${account.accountId}] zca listener error: ${String(err)}`); runtime.error(`[${account.accountId}] zca listener error: ${String(err)}`);
if (!stopped && !abortSignal.aborted) { if (!stopped && !abortSignal.aborted) {
logVerbose(deps, runtime, `[${account.accountId}] restarting listener in 5s...`); logVerbose(core, runtime, `[${account.accountId}] restarting listener in 5s...`);
restartTimer = setTimeout(startListener, 5000); restartTimer = setTimeout(startListener, 5000);
} else { } else {
resolveRunning?.(); resolveRunning?.();

View File

@@ -1,31 +1,35 @@
import type { import type {
ChannelOnboardingAdapter, ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy, ChannelOnboardingDmPolicy,
ClawdbotConfig,
WizardPrompter, WizardPrompter,
} from "clawdbot/plugin-sdk"; } from "clawdbot/plugin-sdk";
import { promptChannelAccessConfig } from "clawdbot/plugin-sdk"; import {
addWildcardAllowFrom,
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
promptAccountId,
promptChannelAccessConfig,
} from "clawdbot/plugin-sdk";
import { import {
listZalouserAccountIds, listZalouserAccountIds,
resolveDefaultZalouserAccountId, resolveDefaultZalouserAccountId,
resolveZalouserAccountSync, resolveZalouserAccountSync,
normalizeAccountId,
checkZcaAuthenticated, checkZcaAuthenticated,
} from "./accounts.js"; } from "./accounts.js";
import { runZca, runZcaInteractive, checkZcaInstalled, parseJsonOutput } from "./zca.js"; import { runZca, runZcaInteractive, checkZcaInstalled, parseJsonOutput } from "./zca.js";
import { DEFAULT_ACCOUNT_ID, type CoreConfig, type ZcaGroup } from "./types.js"; import type { ZcaGroup } from "./types.js";
const channel = "zalouser" as const; const channel = "zalouser" as const;
function setZalouserDmPolicy( function setZalouserDmPolicy(
cfg: CoreConfig, cfg: ClawdbotConfig,
dmPolicy: "pairing" | "allowlist" | "open" | "disabled", dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
): CoreConfig { ): ClawdbotConfig {
const allowFrom = const allowFrom =
dmPolicy === "open" dmPolicy === "open"
? [...(cfg.channels?.zalouser?.allowFrom ?? []), "*"].filter( ? addWildcardAllowFrom(cfg.channels?.zalouser?.allowFrom)
(v, i, a) => a.indexOf(v) === i,
)
: undefined; : undefined;
return { return {
...cfg, ...cfg,
@@ -37,7 +41,7 @@ function setZalouserDmPolicy(
...(allowFrom ? { allowFrom } : {}), ...(allowFrom ? { allowFrom } : {}),
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} }
async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> { async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
@@ -56,10 +60,10 @@ async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
} }
async function promptZalouserAllowFrom(params: { async function promptZalouserAllowFrom(params: {
cfg: CoreConfig; cfg: ClawdbotConfig;
prompter: WizardPrompter; prompter: WizardPrompter;
accountId: string; accountId: string;
}): Promise<CoreConfig> { }): Promise<ClawdbotConfig> {
const { cfg, prompter, accountId } = params; const { cfg, prompter, accountId } = params;
const resolved = resolveZalouserAccountSync({ cfg, accountId }); const resolved = resolveZalouserAccountSync({ cfg, accountId });
const existingAllowFrom = resolved.config.allowFrom ?? []; const existingAllowFrom = resolved.config.allowFrom ?? [];
@@ -93,7 +97,7 @@ async function promptZalouserAllowFrom(params: {
allowFrom: unique, allowFrom: unique,
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} }
return { return {
@@ -114,14 +118,14 @@ async function promptZalouserAllowFrom(params: {
}, },
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} }
function setZalouserGroupPolicy( function setZalouserGroupPolicy(
cfg: CoreConfig, cfg: ClawdbotConfig,
accountId: string, accountId: string,
groupPolicy: "open" | "allowlist" | "disabled", groupPolicy: "open" | "allowlist" | "disabled",
): CoreConfig { ): ClawdbotConfig {
if (accountId === DEFAULT_ACCOUNT_ID) { if (accountId === DEFAULT_ACCOUNT_ID) {
return { return {
...cfg, ...cfg,
@@ -133,7 +137,7 @@ function setZalouserGroupPolicy(
groupPolicy, groupPolicy,
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} }
return { return {
...cfg, ...cfg,
@@ -152,14 +156,14 @@ function setZalouserGroupPolicy(
}, },
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} }
function setZalouserGroupAllowlist( function setZalouserGroupAllowlist(
cfg: CoreConfig, cfg: ClawdbotConfig,
accountId: string, accountId: string,
groupKeys: string[], groupKeys: string[],
): CoreConfig { ): ClawdbotConfig {
const groups = Object.fromEntries(groupKeys.map((key) => [key, { allow: true }])); const groups = Object.fromEntries(groupKeys.map((key) => [key, { allow: true }]));
if (accountId === DEFAULT_ACCOUNT_ID) { if (accountId === DEFAULT_ACCOUNT_ID) {
return { return {
@@ -172,7 +176,7 @@ function setZalouserGroupAllowlist(
groups, groups,
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} }
return { return {
...cfg, ...cfg,
@@ -191,11 +195,11 @@ function setZalouserGroupAllowlist(
}, },
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} }
async function resolveZalouserGroups(params: { async function resolveZalouserGroups(params: {
cfg: CoreConfig; cfg: ClawdbotConfig;
accountId: string; accountId: string;
entries: string[]; entries: string[];
}): Promise<Array<{ input: string; resolved: boolean; id?: string }>> { }): Promise<Array<{ input: string; resolved: boolean; id?: string }>> {
@@ -226,65 +230,23 @@ async function resolveZalouserGroups(params: {
}); });
} }
async function promptAccountId(params: {
cfg: CoreConfig;
prompter: WizardPrompter;
label: string;
currentId: string;
listAccountIds: (cfg: CoreConfig) => string[];
defaultAccountId: string;
}): Promise<string> {
const { cfg, prompter, label, currentId, listAccountIds, defaultAccountId } = params;
const existingIds = listAccountIds(cfg);
const options = [
...existingIds.map((id) => ({
value: id,
label: id === defaultAccountId ? `${id} (default)` : id,
})),
{ value: "__new__", label: "Create new account" },
];
const selected = await prompter.select({
message: `${label} account`,
options,
initialValue: currentId,
});
if (selected === "__new__") {
const newId = await prompter.text({
message: "New account ID",
placeholder: "work",
validate: (value) => {
const raw = String(value ?? "").trim().toLowerCase();
if (!raw) return "Required";
if (!/^[a-z0-9_-]+$/.test(raw)) return "Use lowercase alphanumeric, dash, or underscore";
if (existingIds.includes(raw)) return "Account already exists";
return undefined;
},
});
return String(newId).trim().toLowerCase();
}
return selected as string;
}
const dmPolicy: ChannelOnboardingDmPolicy = { const dmPolicy: ChannelOnboardingDmPolicy = {
label: "Zalo Personal", label: "Zalo Personal",
channel, channel,
policyKey: "channels.zalouser.dmPolicy", policyKey: "channels.zalouser.dmPolicy",
allowFromKey: "channels.zalouser.allowFrom", allowFromKey: "channels.zalouser.allowFrom",
getCurrent: (cfg) => ((cfg as CoreConfig).channels?.zalouser?.dmPolicy ?? "pairing") as "pairing", getCurrent: (cfg) => ((cfg as ClawdbotConfig).channels?.zalouser?.dmPolicy ?? "pairing") as "pairing",
setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as CoreConfig, policy), setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as ClawdbotConfig, policy),
}; };
export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = { export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
channel, channel,
dmPolicy, dmPolicy,
getStatus: async ({ cfg }) => { getStatus: async ({ cfg }) => {
const ids = listZalouserAccountIds(cfg as CoreConfig); const ids = listZalouserAccountIds(cfg as ClawdbotConfig);
let configured = false; let configured = false;
for (const accountId of ids) { for (const accountId of ids) {
const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }); const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
const isAuth = await checkZcaAuthenticated(account.profile); const isAuth = await checkZcaAuthenticated(account.profile);
if (isAuth) { if (isAuth) {
configured = true; configured = true;
@@ -316,14 +278,14 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
} }
const zalouserOverride = accountOverrides.zalouser?.trim(); const zalouserOverride = accountOverrides.zalouser?.trim();
const defaultAccountId = resolveDefaultZalouserAccountId(cfg as CoreConfig); const defaultAccountId = resolveDefaultZalouserAccountId(cfg as ClawdbotConfig);
let accountId = zalouserOverride let accountId = zalouserOverride
? normalizeAccountId(zalouserOverride) ? normalizeAccountId(zalouserOverride)
: defaultAccountId; : defaultAccountId;
if (shouldPromptAccountIds && !zalouserOverride) { if (shouldPromptAccountIds && !zalouserOverride) {
accountId = await promptAccountId({ accountId = await promptAccountId({
cfg: cfg as CoreConfig, cfg: cfg as ClawdbotConfig,
prompter, prompter,
label: "Zalo Personal", label: "Zalo Personal",
currentId: accountId, currentId: accountId,
@@ -332,7 +294,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
}); });
} }
let next = cfg as CoreConfig; let next = cfg as ClawdbotConfig;
const account = resolveZalouserAccountSync({ cfg: next, accountId }); const account = resolveZalouserAccountSync({ cfg: next, accountId });
const alreadyAuthenticated = await checkZcaAuthenticated(account.profile); const alreadyAuthenticated = await checkZcaAuthenticated(account.profile);
@@ -390,7 +352,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
profile: account.profile !== "default" ? account.profile : undefined, profile: account.profile !== "default" ? account.profile : undefined,
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} else { } else {
next = { next = {
...next, ...next,
@@ -409,7 +371,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
}, },
}, },
}, },
} as CoreConfig; } as ClawdbotConfig;
} }
if (forceAllowFrom) { if (forceAllowFrom) {

View File

@@ -0,0 +1,14 @@
import type { PluginRuntime } from "clawdbot/plugin-sdk";
let runtime: PluginRuntime | null = null;
export function setZalouserRuntime(next: PluginRuntime): void {
runtime = next;
}
export function getZalouserRuntime(): PluginRuntime {
if (!runtime) {
throw new Error("Zalouser runtime not initialized");
}
return runtime;
}

View File

@@ -68,9 +68,6 @@ export type ListenOptions = CommonOptions & {
prefix?: string; prefix?: string;
}; };
// Channel plugin config types
export const DEFAULT_ACCOUNT_ID = "default";
export type ZalouserAccountConfig = { export type ZalouserAccountConfig = {
enabled?: boolean; enabled?: boolean;
name?: string; name?: string;
@@ -95,14 +92,6 @@ export type ZalouserConfig = {
accounts?: Record<string, ZalouserAccountConfig>; accounts?: Record<string, ZalouserAccountConfig>;
}; };
export type CoreConfig = {
channels?: {
zalouser?: ZalouserConfig;
[key: string]: unknown;
};
[key: string]: unknown;
};
export type ResolvedZalouserAccount = { export type ResolvedZalouserAccount = {
accountId: string; accountId: string;
name?: string; name?: string;