refactor: generalize channel type handling across various components

This commit is contained in:
Lauren ten Hoor
2026-02-10 09:56:17 +08:00
parent cb0f628772
commit 38ad3fe27f
8 changed files with 36 additions and 37 deletions

View File

@@ -38,10 +38,6 @@ const plugin = {
description: "Plugin-level project execution: parallel (each project independent) or sequential (only one project active at a time)", description: "Plugin-level project execution: parallel (each project independent) or sequential (only one project active at a time)",
default: "parallel", default: "parallel",
}, },
orchestratorDm: {
type: "string",
description: "Telegram/WhatsApp chat ID for orchestrator DM notifications (heartbeat summaries)",
},
notifications: { notifications: {
type: "object", type: "object",
description: "Notification settings for worker lifecycle events", description: "Notification settings for worker lifecycle events",

View File

@@ -7,7 +7,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
export type ChannelType = "telegram" | "whatsapp"; export type ChannelType = string;
export interface BindingAnalysis { export interface BindingAnalysis {
channelEnabled: boolean; channelEnabled: boolean;

View File

@@ -12,10 +12,10 @@ import path from "node:path";
export type InteractionContext = export type InteractionContext =
| { type: "via-agent"; agentId: string; agentName?: string } | { type: "via-agent"; agentId: string; agentName?: string }
| { type: "direct"; channel?: "telegram" | "whatsapp" | "cli" } | { type: "direct"; channel?: string; chatId?: string }
| { | {
type: "group"; type: "group";
channel: "telegram" | "whatsapp"; channel: string;
groupId: string; groupId: string;
projectName?: string; projectName?: string;
}; };
@@ -64,7 +64,7 @@ export async function detectContext(
return { return {
type: "group", type: "group",
channel: messageChannel as "telegram" | "whatsapp", channel: messageChannel,
groupId: actualGroupId, groupId: actualGroupId,
projectName, projectName,
}; };
@@ -72,11 +72,12 @@ export async function detectContext(
} }
// --- Direct (DM or CLI) --- // --- Direct (DM or CLI) ---
// Extract chat ID from sessionKey (e.g. "agent:devclaw:telegram:direct:657120585")
const chatId = sessionKey ? sessionKey.split(":").pop() : undefined;
return { return {
type: "direct", type: "direct",
channel: messageChannel channel: messageChannel ?? "cli",
? (messageChannel as "telegram" | "whatsapp") chatId,
: "cli",
}; };
} }

View File

@@ -121,35 +121,31 @@ function buildMessage(event: NotifyEvent): string {
} }
/** /**
* Send a notification message to a Telegram/WhatsApp target. * Send a notification message via the native OpenClaw messaging CLI.
* *
* Uses the OpenClaw CLI to invoke the message tool. * Uses `openclaw message send` which handles target resolution, chunking,
* retries, and error reporting for all supported channels.
* Fails silently (logs error but doesn't throw) to avoid breaking the main flow. * Fails silently (logs error but doesn't throw) to avoid breaking the main flow.
*/ */
async function sendMessage( async function sendMessage(
target: string, target: string,
message: string, message: string,
channel: "telegram" | "whatsapp", channel: string,
workspaceDir: string, workspaceDir: string,
): Promise<boolean> { ): Promise<boolean> {
try { try {
// Use openclaw agent to send via message tool
// The message tool requires action=send, target, message
await execFileAsync( await execFileAsync(
"openclaw", "openclaw",
[ [
"gateway", "message",
"call", "send",
"tools.invoke", "--channel",
"--params", channel,
JSON.stringify({ "--target",
tool: "message", target,
params: { "--message",
action: "send", message,
target, "--json",
message,
},
}),
], ],
{ timeout: 30_000 }, { timeout: 30_000 },
); );
@@ -178,8 +174,8 @@ export async function notify(
config?: NotificationConfig; config?: NotificationConfig;
/** Target for project-scoped notifications (groupId) */ /** Target for project-scoped notifications (groupId) */
groupId?: string; groupId?: string;
/** Channel type for routing */ /** Channel type for routing (e.g. "telegram", "whatsapp", "discord", "slack") */
channel?: "telegram" | "whatsapp"; channel?: string;
/** Target for DM notifications (orchestrator) */ /** Target for DM notifications (orchestrator) */
orchestratorDm?: string; orchestratorDm?: string;
}, },

View File

@@ -23,6 +23,8 @@ export type Project = {
baseBranch: string; baseBranch: string;
deployBranch: string; deployBranch: string;
autoChain: boolean; autoChain: boolean;
/** Messaging channel for this project's group (e.g. "telegram", "whatsapp", "discord", "slack"). Stored at registration time. */
channel?: string;
/** Project-level role execution: parallel (DEV+QA can run simultaneously) or sequential (only one role at a time). Default: parallel */ /** Project-level role execution: parallel (DEV+QA can run simultaneously) or sequential (only one role at a time). Default: parallel */
roleExecution?: "parallel" | "sequential"; roleExecution?: "parallel" | "sequential";
maxDevWorkers?: number; maxDevWorkers?: number;
@@ -133,6 +135,9 @@ export async function readProjects(workspaceDir: string): Promise<ProjectsData>
if (project.autoChain === undefined) { if (project.autoChain === undefined) {
project.autoChain = false; project.autoChain = false;
} }
if (!project.channel) {
project.channel = "telegram";
}
} }
return data; return data;

View File

@@ -519,10 +519,11 @@ export function createHeartbeatTickTool(api: OpenClawPluginApi) {
skipped: result.skipped.length, skipped: result.skipped.length,
}); });
// Send heartbeat notification to orchestrator DM // Send heartbeat notification back to whoever triggered it
// Both channel and target are derived from context (DM sessionKey)
const notifyConfig = getNotificationConfig(pluginConfig); const notifyConfig = getNotificationConfig(pluginConfig);
const orchestratorDm = pluginConfig?.orchestratorDm as string | undefined; const orchestratorDm = context.type === "direct" ? context.chatId : undefined;
await notify( await notify(
{ {
type: "heartbeat", type: "heartbeat",
@@ -541,7 +542,7 @@ export function createHeartbeatTickTool(api: OpenClawPluginApi) {
workspaceDir, workspaceDir,
config: notifyConfig, config: notifyConfig,
orchestratorDm, orchestratorDm,
channel: "telegram", channel: "channel" in context ? context.channel : undefined,
}, },
); );

View File

@@ -161,7 +161,7 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
// Provide helpful note if project is already registered // Provide helpful note if project is already registered
const contextInfo = context.projectName const contextInfo = context.projectName
? `Note: This group is already registered as "${context.projectName}". You may be re-registering it.` ? `Note: This group is already registered as "${context.projectName}". You may be re-registering it.`
: `Registering project for this ${context.channel === "whatsapp" ? "WhatsApp" : "Telegram"} group (ID: ${actualGroupId.substring(0, 20)}...).`; : `Registering project for this ${context.channel} group (ID: ${actualGroupId.substring(0, 20)}...).`;
// 1. Check project not already registered (allow re-register if incomplete) // 1. Check project not already registered (allow re-register if incomplete)
const data = await readProjects(workspaceDir); const data = await readProjects(workspaceDir);
@@ -205,6 +205,7 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
baseBranch, baseBranch,
deployBranch, deployBranch,
autoChain: false, autoChain: false,
channel: context.channel,
roleExecution, roleExecution,
dev: emptyWorkerState([...DEV_TIERS]), dev: emptyWorkerState([...DEV_TIERS]),
qa: emptyWorkerState([...QA_TIERS]), qa: emptyWorkerState([...QA_TIERS]),

View File

@@ -298,8 +298,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
workspaceDir, workspaceDir,
config: notifyConfig, config: notifyConfig,
groupId, groupId,
// Channel detection: default to telegram if not available channel: project.channel ?? "telegram",
channel: "telegram",
}, },
); );