import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk"; import type { ResolvedPopoAccount, PopoConfig } from "./types.js"; import { resolvePopoAccount, resolvePopoCredentials } from "./accounts.js"; import { popoOutbound } from "./outbound.js"; import { probePopo } from "./probe.js"; import { resolvePopoGroupToolPolicy } from "./policy.js"; import { normalizePopoTarget, looksLikePopoId, formatPopoTarget } from "./targets.js"; import { sendMessagePopo } from "./send.js"; const meta = { id: "popo", label: "POPO", selectionLabel: "POPO (网易)", docsPath: "/channels/popo", docsLabel: "popo", blurb: "POPO enterprise messaging.", aliases: [], order: 80, } as const; export const popoPlugin: ChannelPlugin = { id: "popo", meta: { ...meta, }, pairing: { idLabel: "popoEmail", normalizeAllowEntry: (entry) => entry.replace(/^(popo|user):/i, ""), notifyApproval: async ({ cfg, id }) => { await sendMessagePopo({ cfg, to: id, text: PAIRING_APPROVED_MESSAGE, }); }, }, capabilities: { chatTypes: ["direct", "channel"], polls: false, threads: false, media: true, reactions: false, edit: false, reply: true, }, agentPrompt: { messageToolHints: () => [ "- POPO targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:email@example.com` or `group:groupId`.", ], }, groups: { resolveToolPolicy: resolvePopoGroupToolPolicy, }, reload: { configPrefixes: ["channels.popo"] }, configSchema: { schema: { type: "object", additionalProperties: false, properties: { enabled: { type: "boolean" }, appKey: { type: "string" }, appSecret: { type: "string" }, token: { type: "string" }, aesKey: { type: "string" }, server: { type: "string" }, webhookPath: { type: "string" }, webhookPort: { type: "integer", minimum: 1 }, dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] }, allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } }, groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] }, groupAllowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } }, requireMention: { type: "boolean" }, historyLimit: { type: "integer", minimum: 0 }, dmHistoryLimit: { type: "integer", minimum: 0 }, textChunkLimit: { type: "integer", minimum: 1 }, chunkMode: { type: "string", enum: ["length", "newline"] }, mediaMaxMb: { type: "number", minimum: 0 }, renderMode: { type: "string", enum: ["raw", "rich_text"] }, }, }, }, config: { listAccountIds: () => [DEFAULT_ACCOUNT_ID], resolveAccount: (cfg) => resolvePopoAccount({ cfg }), defaultAccountId: () => DEFAULT_ACCOUNT_ID, setAccountEnabled: ({ cfg, enabled }) => ({ ...cfg, channels: { ...cfg.channels, popo: { ...cfg.channels?.popo, enabled, }, }, }), deleteAccount: ({ cfg }) => { const next = { ...cfg } as ClawdbotConfig; const nextChannels = { ...cfg.channels }; delete (nextChannels as Record).popo; if (Object.keys(nextChannels).length > 0) { next.channels = nextChannels; } else { delete next.channels; } return next; }, isConfigured: (_account, cfg) => Boolean(resolvePopoCredentials(cfg.channels?.popo as PopoConfig | undefined)), describeAccount: (account) => ({ accountId: account.accountId, enabled: account.enabled, configured: account.configured, }), resolveAllowFrom: ({ cfg }) => (cfg.channels?.popo as PopoConfig | undefined)?.allowFrom ?? [], formatAllowFrom: ({ allowFrom }) => allowFrom .map((entry) => String(entry).trim()) .filter(Boolean) .map((entry) => entry.toLowerCase()), }, security: { collectWarnings: ({ cfg }) => { const popoCfg = cfg.channels?.popo as PopoConfig | undefined; const defaultGroupPolicy = (cfg.channels as Record | undefined)?.defaults?.groupPolicy; const groupPolicy = popoCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; if (groupPolicy !== "open") return []; return [ `- POPO groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.popo.groupPolicy="allowlist" + channels.popo.groupAllowFrom to restrict senders.`, ]; }, }, setup: { resolveAccountId: () => DEFAULT_ACCOUNT_ID, applyAccountConfig: ({ cfg }) => ({ ...cfg, channels: { ...cfg.channels, popo: { ...cfg.channels?.popo, enabled: true, }, }, }), }, messaging: { normalizeTarget: normalizePopoTarget, targetResolver: { looksLikeId: looksLikePopoId, hint: "", }, }, outbound: popoOutbound, status: { defaultRuntime: { accountId: DEFAULT_ACCOUNT_ID, running: false, lastStartAt: null, lastStopAt: null, lastError: null, port: null, }, buildChannelSummary: ({ snapshot }) => ({ configured: snapshot.configured ?? false, running: snapshot.running ?? false, lastStartAt: snapshot.lastStartAt ?? null, lastStopAt: snapshot.lastStopAt ?? null, lastError: snapshot.lastError ?? null, port: snapshot.port ?? null, probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null, }), probeAccount: async ({ cfg }) => await probePopo(cfg.channels?.popo as PopoConfig | undefined), buildAccountSnapshot: ({ account, runtime, probe }) => ({ accountId: account.accountId, enabled: account.enabled, configured: account.configured, running: runtime?.running ?? false, lastStartAt: runtime?.lastStartAt ?? null, lastStopAt: runtime?.lastStopAt ?? null, lastError: runtime?.lastError ?? null, port: runtime?.port ?? null, probe, }), }, gateway: { startAccount: async (ctx) => { const { monitorPopoProvider } = await import("./monitor.js"); const popoCfg = ctx.cfg.channels?.popo as PopoConfig | undefined; const port = popoCfg?.webhookPort ?? 3001; ctx.setStatus({ accountId: ctx.accountId, port }); ctx.log?.info(`starting popo provider (webhook mode, port: ${port})`); return monitorPopoProvider({ config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, accountId: ctx.accountId, }); }, }, };