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)",
default: "parallel",
},
orchestratorDm: {
type: "string",
description: "Telegram/WhatsApp chat ID for orchestrator DM notifications (heartbeat summaries)",
},
notifications: {
type: "object",
description: "Notification settings for worker lifecycle events",

View File

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

View File

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

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.
*/
async function sendMessage(
target: string,
message: string,
channel: "telegram" | "whatsapp",
channel: string,
workspaceDir: string,
): Promise<boolean> {
try {
// Use openclaw agent to send via message tool
// The message tool requires action=send, target, message
await execFileAsync(
"openclaw",
[
"gateway",
"call",
"tools.invoke",
"--params",
JSON.stringify({
tool: "message",
params: {
action: "send",
target,
message,
},
}),
"message",
"send",
"--channel",
channel,
"--target",
target,
"--message",
message,
"--json",
],
{ timeout: 30_000 },
);
@@ -178,8 +174,8 @@ export async function notify(
config?: NotificationConfig;
/** Target for project-scoped notifications (groupId) */
groupId?: string;
/** Channel type for routing */
channel?: "telegram" | "whatsapp";
/** Channel type for routing (e.g. "telegram", "whatsapp", "discord", "slack") */
channel?: string;
/** Target for DM notifications (orchestrator) */
orchestratorDm?: string;
},

View File

@@ -23,6 +23,8 @@ export type Project = {
baseBranch: string;
deployBranch: string;
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 */
roleExecution?: "parallel" | "sequential";
maxDevWorkers?: number;
@@ -133,6 +135,9 @@ export async function readProjects(workspaceDir: string): Promise<ProjectsData>
if (project.autoChain === undefined) {
project.autoChain = false;
}
if (!project.channel) {
project.channel = "telegram";
}
}
return data;

View File

@@ -519,9 +519,10 @@ export function createHeartbeatTickTool(api: OpenClawPluginApi) {
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 orchestratorDm = pluginConfig?.orchestratorDm as string | undefined;
const orchestratorDm = context.type === "direct" ? context.chatId : undefined;
await notify(
{
@@ -541,7 +542,7 @@ export function createHeartbeatTickTool(api: OpenClawPluginApi) {
workspaceDir,
config: notifyConfig,
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
const contextInfo = context.projectName
? `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)
const data = await readProjects(workspaceDir);
@@ -205,6 +205,7 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
baseBranch,
deployBranch,
autoChain: false,
channel: context.channel,
roleExecution,
dev: emptyWorkerState([...DEV_TIERS]),
qa: emptyWorkerState([...QA_TIERS]),

View File

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