agent: deliver via rpc and voice forward
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
import { agentCommand } from "../commands/agent.js";
|
||||
import { healthCommand } from "../commands/health.js";
|
||||
import { sendCommand } from "../commands/send.js";
|
||||
import { sessionsCommand } from "../commands/sessions.js";
|
||||
import { healthCommand } from "../commands/health.js";
|
||||
import { statusCommand } from "../commands/status.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { danger, info, setVerbose } from "../globals.js";
|
||||
@@ -236,6 +236,10 @@ Examples:
|
||||
if (!line.trim()) return;
|
||||
try {
|
||||
const cmd = JSON.parse(line);
|
||||
if (cmd.type === "status") {
|
||||
respond({ type: "result", ok: true });
|
||||
return;
|
||||
}
|
||||
if (cmd.type !== "send" || !cmd.text) {
|
||||
respond({ type: "error", error: "unsupported command" });
|
||||
return;
|
||||
@@ -253,12 +257,14 @@ Examples:
|
||||
to?: string;
|
||||
sessionId?: string;
|
||||
thinking?: string;
|
||||
deliver?: boolean;
|
||||
json: boolean;
|
||||
} = {
|
||||
message: String(cmd.text),
|
||||
to: cmd.to ? String(cmd.to) : undefined,
|
||||
sessionId: cmd.session ? String(cmd.session) : undefined,
|
||||
thinking: cmd.thinking ? String(cmd.thinking) : undefined,
|
||||
deliver: Boolean(cmd.deliver),
|
||||
json: true,
|
||||
};
|
||||
|
||||
@@ -572,20 +578,29 @@ Examples:
|
||||
|
||||
program
|
||||
.command("health")
|
||||
.description("Probe WhatsApp Web health (creds + Baileys connect) and session store")
|
||||
.description(
|
||||
"Probe WhatsApp Web health (creds + Baileys connect) and session store",
|
||||
)
|
||||
.option("--json", "Output JSON instead of text", false)
|
||||
.option("--timeout <ms>", "Connection timeout in milliseconds", "10000")
|
||||
.option("--verbose", "Verbose logging", false)
|
||||
.action(async (opts) => {
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
const timeout = opts.timeout ? Number.parseInt(String(opts.timeout), 10) : undefined;
|
||||
const timeout = opts.timeout
|
||||
? Number.parseInt(String(opts.timeout), 10)
|
||||
: undefined;
|
||||
if (timeout !== undefined && (Number.isNaN(timeout) || timeout <= 0)) {
|
||||
defaultRuntime.error("--timeout must be a positive integer (milliseconds)");
|
||||
defaultRuntime.error(
|
||||
"--timeout must be a positive integer (milliseconds)",
|
||||
);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await healthCommand({ json: Boolean(opts.json), timeoutMs: timeout }, defaultRuntime);
|
||||
await healthCommand(
|
||||
{ json: Boolean(opts.json), timeoutMs: timeout },
|
||||
defaultRuntime,
|
||||
);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(String(err));
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../config/sessions.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { normalizeE164 } from "../utils.js";
|
||||
import { sendViaIpc } from "../web/ipc.js";
|
||||
|
||||
type AgentCommandOpts = {
|
||||
@@ -162,13 +163,13 @@ export async function agentCommand(
|
||||
if (!opts.to && !opts.sessionId) {
|
||||
throw new Error("Pass --to <E.164> or --session-id to choose a session");
|
||||
}
|
||||
if (opts.deliver && !opts.to) {
|
||||
throw new Error("Delivering to WhatsApp requires --to <E.164>");
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const replyCfg = assertCommandConfig(cfg);
|
||||
const sessionCfg = replyCfg.session;
|
||||
const allowFrom = (cfg.inbound?.allowFrom ?? [])
|
||||
.map((val) => normalizeE164(val))
|
||||
.filter((val) => val.length > 1);
|
||||
|
||||
const thinkOverride = normalizeThinkLevel(opts.thinking);
|
||||
if (opts.thinking && !thinkOverride) {
|
||||
@@ -340,6 +341,12 @@ export async function agentCommand(
|
||||
}
|
||||
|
||||
const deliver = opts.deliver === true;
|
||||
const targetTo = opts.to ? normalizeE164(opts.to) : allowFrom[0];
|
||||
if (deliver && !targetTo) {
|
||||
throw new Error(
|
||||
"Delivering to WhatsApp requires --to <E.164> or inbound.allowFrom[0]",
|
||||
);
|
||||
}
|
||||
|
||||
for (const payload of payloads) {
|
||||
const lines: string[] = [];
|
||||
@@ -351,29 +358,29 @@ export async function agentCommand(
|
||||
}
|
||||
runtime.log(lines.join("\n"));
|
||||
|
||||
if (deliver && opts.to) {
|
||||
if (deliver && targetTo) {
|
||||
const text = payload.text ?? "";
|
||||
const media = mediaList;
|
||||
// Prefer IPC to reuse the running relay; fall back to direct web send.
|
||||
let sentViaIpc = false;
|
||||
const ipcResult = await sendViaIpc(opts.to, text, media[0]);
|
||||
const ipcResult = await sendViaIpc(targetTo, text, media[0]);
|
||||
if (ipcResult) {
|
||||
sentViaIpc = ipcResult.success;
|
||||
if (ipcResult.success && media.length > 1) {
|
||||
for (const extra of media.slice(1)) {
|
||||
await sendViaIpc(opts.to, "", extra);
|
||||
await sendViaIpc(targetTo, "", extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sentViaIpc) {
|
||||
if (text || media.length === 0) {
|
||||
await deps.sendMessageWeb(opts.to, text, {
|
||||
await deps.sendMessageWeb(targetTo, text, {
|
||||
verbose: false,
|
||||
mediaUrl: media[0],
|
||||
});
|
||||
}
|
||||
for (const extra of media.slice(1)) {
|
||||
await deps.sendMessageWeb(opts.to, "", {
|
||||
await deps.sendMessageWeb(targetTo, "", {
|
||||
verbose: false,
|
||||
mediaUrl: extra,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { healthCommand } from "./health.js";
|
||||
|
||||
@@ -23,7 +23,10 @@ const waitForWaConnection = vi.fn();
|
||||
const webAuthExists = vi.fn();
|
||||
|
||||
vi.mock("../web/session.js", () => ({
|
||||
createWaSocket: vi.fn(async () => ({ ws: { close: vi.fn() }, ev: { on: vi.fn() } })),
|
||||
createWaSocket: vi.fn(async () => ({
|
||||
ws: { close: vi.fn() },
|
||||
ev: { on: vi.fn() },
|
||||
})),
|
||||
waitForWaConnection: (...args: unknown[]) => waitForWaConnection(...args),
|
||||
webAuthExists: (...args: unknown[]) => webAuthExists(...args),
|
||||
getStatusCode: vi.fn(() => 440),
|
||||
|
||||
@@ -2,10 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
||||
import { info } from "../globals.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||
@@ -37,7 +34,11 @@ type HealthSummary = {
|
||||
sessions: {
|
||||
path: string;
|
||||
count: number;
|
||||
recent: Array<{ key: string; updatedAt: number | null; age: number | null }>;
|
||||
recent: Array<{
|
||||
key: string;
|
||||
updatedAt: number | null;
|
||||
age: number | null;
|
||||
}>;
|
||||
};
|
||||
ipc: { path: string; exists: boolean };
|
||||
};
|
||||
@@ -54,7 +55,12 @@ async function probeWebConnect(timeoutMs: number): Promise<HealthConnect> {
|
||||
setTimeout(() => reject(new Error("timeout")), timeoutMs),
|
||||
),
|
||||
]);
|
||||
return { ok: true, status: null, error: null, elapsedMs: Date.now() - started };
|
||||
return {
|
||||
ok: true,
|
||||
status: null,
|
||||
error: null,
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -126,18 +132,25 @@ export async function healthCommand(
|
||||
}
|
||||
if (connect) {
|
||||
const base = connect.ok
|
||||
? info(`Connect: ok (${connect.elapsedMs}ms)`) : `Connect: failed (${connect.status ?? "unknown"})`;
|
||||
? info(`Connect: ok (${connect.elapsedMs}ms)`)
|
||||
: `Connect: failed (${connect.status ?? "unknown"})`;
|
||||
runtime.log(base + (connect.error ? ` - ${connect.error}` : ""));
|
||||
}
|
||||
runtime.log(info(`Heartbeat interval: ${heartbeatSeconds}s`));
|
||||
runtime.log(info(`Session store: ${storePath} (${sessions.length} entries)`));
|
||||
runtime.log(
|
||||
info(`Session store: ${storePath} (${sessions.length} entries)`),
|
||||
);
|
||||
if (recent.length > 0) {
|
||||
runtime.log("Recent sessions:");
|
||||
for (const r of recent) {
|
||||
runtime.log(`- ${r.key} (${r.updatedAt ? `${Math.round((Date.now() - r.updatedAt) / 60000)}m ago` : "no activity"})`);
|
||||
runtime.log(
|
||||
`- ${r.key} (${r.updatedAt ? `${Math.round((Date.now() - r.updatedAt) / 60000)}m ago` : "no activity"})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
runtime.log(info(`IPC socket: ${ipcExists ? "present" : "missing"} (${ipcPath})`));
|
||||
runtime.log(
|
||||
info(`IPC socket: ${ipcExists ? "present" : "missing"} (${ipcPath})`),
|
||||
);
|
||||
}
|
||||
|
||||
if (fatal) {
|
||||
|
||||
@@ -55,9 +55,9 @@ describe("loginWeb coverage", () => {
|
||||
output: { statusCode: DisconnectReason.loggedOut },
|
||||
});
|
||||
|
||||
await expect(loginWeb(false, "web", waitForWaConnection as never)).rejects.toThrow(
|
||||
/cache cleared/i,
|
||||
);
|
||||
await expect(
|
||||
loginWeb(false, "web", waitForWaConnection as never),
|
||||
).rejects.toThrow(/cache cleared/i);
|
||||
expect(rmMock).toHaveBeenCalledWith("/tmp/wa-creds", {
|
||||
recursive: true,
|
||||
force: true,
|
||||
@@ -66,9 +66,9 @@ describe("loginWeb coverage", () => {
|
||||
|
||||
it("formats and rethrows generic errors", async () => {
|
||||
waitForWaConnection.mockRejectedValueOnce(new Error("boom"));
|
||||
await expect(loginWeb(false, "web", waitForWaConnection as never)).rejects.toThrow(
|
||||
"formatted:Error: boom",
|
||||
);
|
||||
await expect(
|
||||
loginWeb(false, "web", waitForWaConnection as never),
|
||||
).rejects.toThrow("formatted:Error: boom");
|
||||
expect(formatError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user