feat: implement zalouser channel plugin with configuration and status monitoring
This commit is contained in:
13
extensions/zalouser/CHANGELOG.md
Normal file
13
extensions/zalouser/CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
- Initial version with full channel plugin support
|
||||
- QR code login via zca-cli
|
||||
- Multi-account support
|
||||
- Agent tool for sending messages
|
||||
- Group and DM policy support
|
||||
- ChannelDock for lightweight shared metadata
|
||||
- Zod-based config schema validation
|
||||
- Setup adapter for programmatic configuration
|
||||
- Dedicated probe and status issues modules
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||
|
||||
import { zalouserPlugin } from "./src/channel.js";
|
||||
import { zalouserDock, zalouserPlugin } from "./src/channel.js";
|
||||
import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
|
||||
import { setZalouserRuntime } from "./src/runtime.js";
|
||||
|
||||
@@ -13,7 +13,7 @@ const plugin = {
|
||||
register(api: ClawdbotPluginApi) {
|
||||
setZalouserRuntime(api.runtime);
|
||||
// Register channel plugin (for onboarding & gateway)
|
||||
api.registerChannel(zalouserPlugin);
|
||||
api.registerChannel({ plugin: zalouserPlugin, dock: zalouserDock });
|
||||
|
||||
// Register agent tool
|
||||
api.registerTool({
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelDirectoryEntry,
|
||||
ChannelDock,
|
||||
ChannelPlugin,
|
||||
ClawdbotConfig,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
buildChannelConfigSchema,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
formatPairingApproveHint,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
@@ -23,6 +27,9 @@ import { zalouserOnboardingAdapter } from "./onboarding.js";
|
||||
import { sendMessageZalouser } from "./send.js";
|
||||
import { checkZcaInstalled, parseJsonOutput, runZca, runZcaInteractive } from "./zca.js";
|
||||
import type { ZcaFriend, ZcaGroup, ZcaUserInfo } from "./types.js";
|
||||
import { ZalouserConfigSchema } from "./config-schema.js";
|
||||
import { collectZalouserStatusIssues } from "./status-issues.js";
|
||||
import { probeZalouser } from "./probe.js";
|
||||
|
||||
const meta = {
|
||||
id: "zalouser",
|
||||
@@ -72,6 +79,34 @@ function mapGroup(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export const zalouserDock: ChannelDock = {
|
||||
id: "zalouser",
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
blockStreaming: true,
|
||||
},
|
||||
outbound: { textChunkLimit: 2000 },
|
||||
config: {
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
(resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId }).config.allowFrom ?? []).map(
|
||||
(entry) => String(entry),
|
||||
),
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => entry.replace(/^(zalouser|zlu):/i, ""))
|
||||
.map((entry) => entry.toLowerCase()),
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: () => true,
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: () => "off",
|
||||
},
|
||||
};
|
||||
|
||||
export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
id: "zalouser",
|
||||
meta,
|
||||
@@ -86,6 +121,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
blockStreaming: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.zalouser"] },
|
||||
configSchema: buildChannelConfigSchema(ZalouserConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listZalouserAccountIds(cfg as ClawdbotConfig),
|
||||
resolveAccount: (cfg, accountId) =>
|
||||
@@ -156,6 +192,63 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
threading: {
|
||||
resolveReplyToMode: () => "off",
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
channelKey: "zalouser",
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: () => null,
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
channelKey: "zalouser",
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: "zalouser",
|
||||
})
|
||||
: namedConfig;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
zalouser: {
|
||||
...next.channels?.zalouser,
|
||||
enabled: true,
|
||||
...(input.profile ? { profile: input.profile } : {}),
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
zalouser: {
|
||||
...next.channels?.zalouser,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...(next.channels?.zalouser?.accounts ?? {}),
|
||||
[accountId]: {
|
||||
...(next.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
||||
enabled: true,
|
||||
...(input.profile ? { profile: input.profile } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
},
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: (raw) => {
|
||||
const trimmed = raw?.trim();
|
||||
@@ -424,6 +517,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
collectStatusIssues: collectZalouserStatusIssues,
|
||||
buildChannelSummary: ({ snapshot }) => ({
|
||||
configured: snapshot.configured ?? false,
|
||||
running: snapshot.running ?? false,
|
||||
@@ -433,20 +527,8 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
probe: snapshot.probe,
|
||||
lastProbeAt: snapshot.lastProbeAt ?? null,
|
||||
}),
|
||||
probeAccount: async ({ account, timeoutMs }) => {
|
||||
const result = await runZca(["me", "info", "-j"], {
|
||||
profile: account.profile,
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
if (!result.ok) {
|
||||
return { ok: false, error: result.stderr };
|
||||
}
|
||||
try {
|
||||
return { ok: true, user: JSON.parse(result.stdout) };
|
||||
} catch {
|
||||
return { ok: false, error: "Failed to parse user info" };
|
||||
}
|
||||
},
|
||||
probeAccount: async ({ account, timeoutMs }) =>
|
||||
probeZalouser(account.profile, timeoutMs),
|
||||
buildAccountSnapshot: async ({ account, runtime }) => {
|
||||
const configured = await checkZcaAuthenticated(account.profile);
|
||||
return {
|
||||
|
||||
23
extensions/zalouser/src/config-schema.ts
Normal file
23
extensions/zalouser/src/config-schema.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
|
||||
const groupConfigSchema = z.object({
|
||||
allow: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const zalouserAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
profile: z.string().optional(),
|
||||
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
|
||||
allowFrom: z.array(allowFromEntry).optional(),
|
||||
groupPolicy: z.enum(["disabled", "allowlist", "open"]).optional(),
|
||||
groups: z.object({}).catchall(groupConfigSchema).optional(),
|
||||
messagePrefix: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZalouserConfigSchema = zalouserAccountSchema.extend({
|
||||
accounts: z.object({}).catchall(zalouserAccountSchema).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
});
|
||||
29
extensions/zalouser/src/probe.ts
Normal file
29
extensions/zalouser/src/probe.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { runZca, parseJsonOutput } from "./zca.js";
|
||||
import type { ZcaUserInfo } from "./types.js";
|
||||
|
||||
export interface ZalouserProbeResult {
|
||||
ok: boolean;
|
||||
user?: ZcaUserInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function probeZalouser(
|
||||
profile: string,
|
||||
timeoutMs?: number,
|
||||
): Promise<ZalouserProbeResult> {
|
||||
const result = await runZca(["me", "info", "-j"], {
|
||||
profile,
|
||||
timeout: timeoutMs ?? 10000,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return { ok: false, error: result.stderr || "Failed to probe" };
|
||||
}
|
||||
|
||||
try {
|
||||
const user = parseJsonOutput<ZcaUserInfo>(result.stdout);
|
||||
return { ok: true, user: user ?? undefined };
|
||||
} catch {
|
||||
return { ok: false, error: "Failed to parse user info" };
|
||||
}
|
||||
}
|
||||
53
extensions/zalouser/src/status-issues.ts
Normal file
53
extensions/zalouser/src/status-issues.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import { checkZcaInstalled } from "./zca.js";
|
||||
import { resolveZalouserAccountSync } from "./accounts.js";
|
||||
|
||||
export interface ZalouserStatusIssue {
|
||||
level: "error" | "warn" | "info";
|
||||
code: string;
|
||||
message: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export async function collectZalouserStatusIssues(params: {
|
||||
cfg: unknown;
|
||||
accountId?: string;
|
||||
}): Promise<ZalouserStatusIssue[]> {
|
||||
const issues: ZalouserStatusIssue[] = [];
|
||||
|
||||
// Check zca binary
|
||||
const zcaInstalled = await checkZcaInstalled();
|
||||
if (!zcaInstalled) {
|
||||
issues.push({
|
||||
level: "error",
|
||||
code: "ZCA_NOT_FOUND",
|
||||
message: "zca CLI not found in PATH",
|
||||
hint: "Install zca from https://zca-cli.dev or ensure it's in your PATH",
|
||||
});
|
||||
return issues;
|
||||
}
|
||||
|
||||
// Check account configuration
|
||||
try {
|
||||
const account = resolveZalouserAccountSync({
|
||||
cfg: params.cfg as ClawdbotConfig,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
|
||||
if (!account.enabled) {
|
||||
issues.push({
|
||||
level: "warn",
|
||||
code: "ACCOUNT_DISABLED",
|
||||
message: `Account ${account.accountId} is disabled`,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
issues.push({
|
||||
level: "error",
|
||||
code: "ACCOUNT_RESOLVE_FAILED",
|
||||
message: `Failed to resolve account: ${String(err)}`,
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
Reference in New Issue
Block a user