fix(signal): stabilize daemon + add signal delivery
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Docs/agent tools: clarify that browser `wait` should be avoided by default and used only in exceptional cases.
|
- Docs/agent tools: clarify that browser `wait` should be avoided by default and used only in exceptional cases.
|
||||||
- Browser tools: `upload` supports auto-click refs, direct `inputRef`/`element` file inputs, and emits input/change after `setFiles` so JS-heavy sites pick up attachments.
|
- Browser tools: `upload` supports auto-click refs, direct `inputRef`/`element` file inputs, and emits input/change after `setFiles` so JS-heavy sites pick up attachments.
|
||||||
|
- Signal: fix daemon startup race (wait for `/api/v1/check`) and normalize JSON-RPC `version` probe parsing.
|
||||||
- macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.
|
- macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.
|
||||||
- macOS menu: add a Talk Mode action alongside the Open Dashboard/Chat/Canvas entries.
|
- macOS menu: add a Talk Mode action alongside the Open Dashboard/Chat/Canvas entries.
|
||||||
- macOS Debug: hide “Restart Gateway” when the app won’t start a local gateway (remote mode / attach-only).
|
- macOS Debug: hide “Restart Gateway” when the app won’t start a local gateway (remote mode / attach-only).
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ describe("getHealthSnapshot", () => {
|
|||||||
fs.writeFileSync(tokenFile, "t-file\n", "utf-8");
|
fs.writeFileSync(tokenFile, "t-file\n", "utf-8");
|
||||||
testConfig = { telegram: { tokenFile } };
|
testConfig = { telegram: { tokenFile } };
|
||||||
testStore = {};
|
testStore = {};
|
||||||
|
vi.stubEnv("TELEGRAM_BOT_TOKEN", "");
|
||||||
|
vi.stubEnv("DISCORD_BOT_TOKEN", "");
|
||||||
|
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
|
|||||||
@@ -456,7 +456,6 @@ export type ClawdisConfig = {
|
|||||||
canvasHost?: CanvasHostConfig;
|
canvasHost?: CanvasHostConfig;
|
||||||
talk?: TalkConfig;
|
talk?: TalkConfig;
|
||||||
gateway?: GatewayConfig;
|
gateway?: GatewayConfig;
|
||||||
skills?: SkillsConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -851,7 +850,9 @@ const ClawdisSchema = z.object({
|
|||||||
httpPort: z.number().int().positive().optional(),
|
httpPort: z.number().int().positive().optional(),
|
||||||
cliPath: z.string().optional(),
|
cliPath: z.string().optional(),
|
||||||
autoStart: z.boolean().optional(),
|
autoStart: z.boolean().optional(),
|
||||||
receiveMode: z.union([z.literal("on-start"), z.literal("manual")]).optional(),
|
receiveMode: z
|
||||||
|
.union([z.literal("on-start"), z.literal("manual")])
|
||||||
|
.optional(),
|
||||||
ignoreAttachments: z.boolean().optional(),
|
ignoreAttachments: z.boolean().optional(),
|
||||||
ignoreStories: z.boolean().optional(),
|
ignoreStories: z.boolean().optional(),
|
||||||
sendReadReceipts: z.boolean().optional(),
|
sendReadReceipts: z.boolean().optional(),
|
||||||
@@ -948,7 +949,8 @@ const ClawdisSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
entries: z.record(
|
entries: z
|
||||||
|
.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
@@ -957,7 +959,8 @@ const ClawdisSchema = z.object({
|
|||||||
env: z.record(z.string(), z.string()).optional(),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
.passthrough(),
|
.passthrough(),
|
||||||
).optional(),
|
)
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function pickSummaryFromPayloads(
|
|||||||
function resolveDeliveryTarget(
|
function resolveDeliveryTarget(
|
||||||
cfg: ClawdisConfig,
|
cfg: ClawdisConfig,
|
||||||
jobPayload: {
|
jobPayload: {
|
||||||
channel?: "last" | "whatsapp" | "telegram" | "discord";
|
channel?: "last" | "whatsapp" | "telegram" | "discord" | "signal";
|
||||||
to?: string;
|
to?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -79,7 +79,8 @@ function resolveDeliveryTarget(
|
|||||||
if (
|
if (
|
||||||
requestedChannel === "whatsapp" ||
|
requestedChannel === "whatsapp" ||
|
||||||
requestedChannel === "telegram" ||
|
requestedChannel === "telegram" ||
|
||||||
requestedChannel === "discord"
|
requestedChannel === "discord" ||
|
||||||
|
requestedChannel === "signal"
|
||||||
) {
|
) {
|
||||||
return requestedChannel;
|
return requestedChannel;
|
||||||
}
|
}
|
||||||
@@ -414,6 +415,44 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
return { status: "error", summary, error: String(err) };
|
return { status: "error", summary, error: String(err) };
|
||||||
return { status: "ok", summary };
|
return { status: "ok", summary };
|
||||||
}
|
}
|
||||||
|
} else if (resolvedDelivery.channel === "signal") {
|
||||||
|
if (!resolvedDelivery.to) {
|
||||||
|
if (!bestEffortDeliver)
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
summary,
|
||||||
|
error: "Cron delivery to Signal requires a recipient.",
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
status: "skipped",
|
||||||
|
summary: "Delivery skipped (no Signal recipient).",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const to = resolvedDelivery.to;
|
||||||
|
try {
|
||||||
|
for (const payload of payloads) {
|
||||||
|
const mediaList =
|
||||||
|
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
|
if (mediaList.length === 0) {
|
||||||
|
for (const chunk of chunkText(payload.text ?? "", 4000)) {
|
||||||
|
await params.deps.sendMessageSignal(to, chunk);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let first = true;
|
||||||
|
for (const url of mediaList) {
|
||||||
|
const caption = first ? (payload.text ?? "") : "";
|
||||||
|
first = false;
|
||||||
|
await params.deps.sendMessageSignal(to, caption, {
|
||||||
|
mediaUrl: url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!bestEffortDeliver)
|
||||||
|
return { status: "error", summary, error: String(err) };
|
||||||
|
return { status: "ok", summary };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type CronPayload =
|
|||||||
thinking?: string;
|
thinking?: string;
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
deliver?: boolean;
|
deliver?: boolean;
|
||||||
channel?: "last" | "whatsapp" | "telegram" | "discord";
|
channel?: "last" | "whatsapp" | "telegram" | "discord" | "signal";
|
||||||
to?: string;
|
to?: string;
|
||||||
bestEffortDeliver?: boolean;
|
bestEffortDeliver?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { createSubsystemLogger } from "../logging.js";
|
|||||||
import { getQueueSize } from "../process/command-queue.js";
|
import { getQueueSize } from "../process/command-queue.js";
|
||||||
import { webAuthExists } from "../providers/web/index.js";
|
import { webAuthExists } from "../providers/web/index.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
|
import { sendMessageSignal } from "../signal/send.js";
|
||||||
import { sendMessageTelegram } from "../telegram/send.js";
|
import { sendMessageTelegram } from "../telegram/send.js";
|
||||||
import { normalizeE164 } from "../utils.js";
|
import { normalizeE164 } from "../utils.js";
|
||||||
import { getActiveWebListener } from "../web/active-listener.js";
|
import { getActiveWebListener } from "../web/active-listener.js";
|
||||||
@@ -36,10 +37,11 @@ export type HeartbeatTarget =
|
|||||||
| "whatsapp"
|
| "whatsapp"
|
||||||
| "telegram"
|
| "telegram"
|
||||||
| "discord"
|
| "discord"
|
||||||
|
| "signal"
|
||||||
| "none";
|
| "none";
|
||||||
|
|
||||||
export type HeartbeatDeliveryTarget = {
|
export type HeartbeatDeliveryTarget = {
|
||||||
channel: "whatsapp" | "telegram" | "discord" | "none";
|
channel: "whatsapp" | "telegram" | "discord" | "signal" | "none";
|
||||||
to?: string;
|
to?: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
};
|
};
|
||||||
@@ -49,6 +51,7 @@ type HeartbeatDeps = {
|
|||||||
sendWhatsApp?: typeof sendMessageWhatsApp;
|
sendWhatsApp?: typeof sendMessageWhatsApp;
|
||||||
sendTelegram?: typeof sendMessageTelegram;
|
sendTelegram?: typeof sendMessageTelegram;
|
||||||
sendDiscord?: typeof sendMessageDiscord;
|
sendDiscord?: typeof sendMessageDiscord;
|
||||||
|
sendSignal?: typeof sendMessageSignal;
|
||||||
getQueueSize?: (lane?: string) => number;
|
getQueueSize?: (lane?: string) => number;
|
||||||
nowMs?: () => number;
|
nowMs?: () => number;
|
||||||
webAuthExists?: () => Promise<boolean>;
|
webAuthExists?: () => Promise<boolean>;
|
||||||
@@ -177,6 +180,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
|||||||
rawTarget === "whatsapp" ||
|
rawTarget === "whatsapp" ||
|
||||||
rawTarget === "telegram" ||
|
rawTarget === "telegram" ||
|
||||||
rawTarget === "discord" ||
|
rawTarget === "discord" ||
|
||||||
|
rawTarget === "signal" ||
|
||||||
rawTarget === "none" ||
|
rawTarget === "none" ||
|
||||||
rawTarget === "last"
|
rawTarget === "last"
|
||||||
? rawTarget
|
? rawTarget
|
||||||
@@ -197,10 +201,13 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const lastTo = typeof entry?.lastTo === "string" ? entry.lastTo.trim() : "";
|
const lastTo = typeof entry?.lastTo === "string" ? entry.lastTo.trim() : "";
|
||||||
|
|
||||||
const channel: "whatsapp" | "telegram" | "discord" | undefined =
|
const channel: "whatsapp" | "telegram" | "discord" | "signal" | undefined =
|
||||||
target === "last"
|
target === "last"
|
||||||
? lastChannel
|
? lastChannel
|
||||||
: target === "whatsapp" || target === "telegram" || target === "discord"
|
: target === "whatsapp" ||
|
||||||
|
target === "telegram" ||
|
||||||
|
target === "discord" ||
|
||||||
|
target === "signal"
|
||||||
? target
|
? target
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -267,12 +274,15 @@ function normalizeHeartbeatReply(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deliverHeartbeatReply(params: {
|
async function deliverHeartbeatReply(params: {
|
||||||
channel: "whatsapp" | "telegram" | "discord";
|
channel: "whatsapp" | "telegram" | "discord" | "signal";
|
||||||
to: string;
|
to: string;
|
||||||
text: string;
|
text: string;
|
||||||
mediaUrls: string[];
|
mediaUrls: string[];
|
||||||
deps: Required<
|
deps: Required<
|
||||||
Pick<HeartbeatDeps, "sendWhatsApp" | "sendTelegram" | "sendDiscord">
|
Pick<
|
||||||
|
HeartbeatDeps,
|
||||||
|
"sendWhatsApp" | "sendTelegram" | "sendDiscord" | "sendSignal"
|
||||||
|
>
|
||||||
>;
|
>;
|
||||||
}) {
|
}) {
|
||||||
const { channel, to, text, mediaUrls, deps } = params;
|
const { channel, to, text, mediaUrls, deps } = params;
|
||||||
@@ -292,6 +302,22 @@ async function deliverHeartbeatReply(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channel === "signal") {
|
||||||
|
if (mediaUrls.length === 0) {
|
||||||
|
for (const chunk of chunkText(text, 4000)) {
|
||||||
|
await deps.sendSignal(to, chunk);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let first = true;
|
||||||
|
for (const url of mediaUrls) {
|
||||||
|
const caption = first ? text : "";
|
||||||
|
first = false;
|
||||||
|
await deps.sendSignal(to, caption, { mediaUrl: url });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (channel === "telegram") {
|
if (channel === "telegram") {
|
||||||
if (mediaUrls.length === 0) {
|
if (mediaUrls.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, 4000)) {
|
||||||
@@ -437,6 +463,7 @@ export async function runHeartbeatOnce(opts: {
|
|||||||
sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp,
|
sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp,
|
||||||
sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram,
|
sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram,
|
||||||
sendDiscord: opts.deps?.sendDiscord ?? sendMessageDiscord,
|
sendDiscord: opts.deps?.sendDiscord ?? sendMessageDiscord,
|
||||||
|
sendSignal: opts.deps?.sendSignal ?? sendMessageSignal,
|
||||||
};
|
};
|
||||||
await deliverHeartbeatReply({
|
await deliverHeartbeatReply({
|
||||||
channel: delivery.channel,
|
channel: delivery.channel,
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import { chunkText } from "../auto-reply/chunk.js";
|
||||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
||||||
import { chunkText } from "../auto-reply/chunk.js";
|
|
||||||
import { danger, isVerbose, logVerbose } from "../globals.js";
|
import { danger, isVerbose, logVerbose } from "../globals.js";
|
||||||
import { mediaKindFromMime } from "../media/constants.js";
|
import { mediaKindFromMime } from "../media/constants.js";
|
||||||
import { saveMediaBuffer } from "../media/store.js";
|
import { saveMediaBuffer } from "../media/store.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { normalizeE164 } from "../utils.js";
|
import { normalizeE164 } from "../utils.js";
|
||||||
import { signalRpcRequest, streamSignalEvents } from "./client.js";
|
import { signalCheck, signalRpcRequest, streamSignalEvents } from "./client.js";
|
||||||
import { spawnSignalDaemon } from "./daemon.js";
|
import { spawnSignalDaemon } from "./daemon.js";
|
||||||
import { sendMessageSignal } from "./send.js";
|
import { sendMessageSignal } from "./send.js";
|
||||||
|
|
||||||
@@ -93,10 +93,7 @@ function resolveAccount(opts: MonitorSignalOpts): string | undefined {
|
|||||||
function resolveAllowFrom(opts: MonitorSignalOpts): string[] {
|
function resolveAllowFrom(opts: MonitorSignalOpts): string[] {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const raw =
|
const raw =
|
||||||
opts.allowFrom ??
|
opts.allowFrom ?? cfg.signal?.allowFrom ?? cfg.routing?.allowFrom ?? [];
|
||||||
cfg.signal?.allowFrom ??
|
|
||||||
cfg.routing?.allowFrom ??
|
|
||||||
[];
|
|
||||||
return raw.map((entry) => String(entry).trim()).filter(Boolean);
|
return raw.map((entry) => String(entry).trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +107,32 @@ function isAllowedSender(sender: string, allowFrom: string[]): boolean {
|
|||||||
return normalizedAllow.includes(normalizedSender);
|
return normalizedAllow.includes(normalizedSender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForSignalDaemonReady(params: {
|
||||||
|
baseUrl: string;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
timeoutMs: number;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
}): Promise<void> {
|
||||||
|
const started = Date.now();
|
||||||
|
let lastError: string | null = null;
|
||||||
|
|
||||||
|
while (Date.now() - started < params.timeoutMs) {
|
||||||
|
if (params.abortSignal?.aborted) return;
|
||||||
|
const res = await signalCheck(params.baseUrl, 1000);
|
||||||
|
if (res.ok) return;
|
||||||
|
lastError =
|
||||||
|
res.error ?? (res.status ? `HTTP ${res.status}` : "unreachable");
|
||||||
|
await new Promise((r) => setTimeout(r, 150));
|
||||||
|
}
|
||||||
|
|
||||||
|
params.runtime.error?.(
|
||||||
|
danger(
|
||||||
|
`signal: daemon not ready after ${params.timeoutMs}ms (${lastError ?? "unknown error"})`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
throw new Error(`signal daemon not ready (${lastError ?? "unknown error"})`);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchAttachment(params: {
|
async function fetchAttachment(params: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
account?: string;
|
account?: string;
|
||||||
@@ -202,9 +225,7 @@ export async function monitorSignalProvider(
|
|||||||
opts.ignoreAttachments ?? cfg.signal?.ignoreAttachments ?? false;
|
opts.ignoreAttachments ?? cfg.signal?.ignoreAttachments ?? false;
|
||||||
|
|
||||||
const autoStart =
|
const autoStart =
|
||||||
opts.autoStart ??
|
opts.autoStart ?? cfg.signal?.autoStart ?? !cfg.signal?.httpUrl;
|
||||||
cfg.signal?.autoStart ??
|
|
||||||
(cfg.signal?.httpUrl ? false : true);
|
|
||||||
let daemonHandle: ReturnType<typeof spawnSignalDaemon> | null = null;
|
let daemonHandle: ReturnType<typeof spawnSignalDaemon> | null = null;
|
||||||
|
|
||||||
if (autoStart) {
|
if (autoStart) {
|
||||||
@@ -220,8 +241,7 @@ export async function monitorSignalProvider(
|
|||||||
ignoreAttachments:
|
ignoreAttachments:
|
||||||
opts.ignoreAttachments ?? cfg.signal?.ignoreAttachments,
|
opts.ignoreAttachments ?? cfg.signal?.ignoreAttachments,
|
||||||
ignoreStories: opts.ignoreStories ?? cfg.signal?.ignoreStories,
|
ignoreStories: opts.ignoreStories ?? cfg.signal?.ignoreStories,
|
||||||
sendReadReceipts:
|
sendReadReceipts: opts.sendReadReceipts ?? cfg.signal?.sendReadReceipts,
|
||||||
opts.sendReadReceipts ?? cfg.signal?.sendReadReceipts,
|
|
||||||
runtime,
|
runtime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -232,6 +252,15 @@ export async function monitorSignalProvider(
|
|||||||
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (daemonHandle) {
|
||||||
|
await waitForSignalDaemonReady({
|
||||||
|
baseUrl,
|
||||||
|
abortSignal: opts.abortSignal,
|
||||||
|
timeoutMs: 10_000,
|
||||||
|
runtime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleEvent = async (event: { event?: string; data?: string }) => {
|
const handleEvent = async (event: { event?: string; data?: string }) => {
|
||||||
if (event.event !== "receive" || !event.data) return;
|
if (event.event !== "receive" || !event.data) return;
|
||||||
let payload: SignalReceivePayload | null = null;
|
let payload: SignalReceivePayload | null = null;
|
||||||
@@ -242,7 +271,9 @@ export async function monitorSignalProvider(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (payload?.exception?.message) {
|
if (payload?.exception?.message) {
|
||||||
runtime.error?.(`signal: receive exception: ${payload.exception.message}`);
|
runtime.error?.(
|
||||||
|
`signal: receive exception: ${payload.exception.message}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const envelope = payload?.envelope;
|
const envelope = payload?.envelope;
|
||||||
if (!envelope) return;
|
if (!envelope) return;
|
||||||
@@ -282,7 +313,8 @@ export async function monitorSignalProvider(
|
|||||||
});
|
});
|
||||||
if (fetched) {
|
if (fetched) {
|
||||||
mediaPath = fetched.path;
|
mediaPath = fetched.path;
|
||||||
mediaType = fetched.contentType ?? firstAttachment.contentType ?? undefined;
|
mediaType =
|
||||||
|
fetched.contentType ?? firstAttachment.contentType ?? undefined;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
runtime.error?.(
|
runtime.error?.(
|
||||||
@@ -317,7 +349,7 @@ export async function monitorSignalProvider(
|
|||||||
From: isGroup ? `group:${groupId}` : `signal:${sender}`,
|
From: isGroup ? `group:${groupId}` : `signal:${sender}`,
|
||||||
To: isGroup ? `group:${groupId}` : `signal:${sender}`,
|
To: isGroup ? `group:${groupId}` : `signal:${sender}`,
|
||||||
ChatType: isGroup ? "group" : "direct",
|
ChatType: isGroup ? "group" : "direct",
|
||||||
GroupSubject: isGroup ? groupName ?? undefined : undefined,
|
GroupSubject: isGroup ? (groupName ?? undefined) : undefined,
|
||||||
SenderName: envelope.sourceName ?? sender,
|
SenderName: envelope.sourceName ?? sender,
|
||||||
Surface: "signal" as const,
|
Surface: "signal" as const,
|
||||||
MessageSid: envelope.timestamp ? String(envelope.timestamp) : undefined,
|
MessageSid: envelope.timestamp ? String(envelope.timestamp) : undefined,
|
||||||
|
|||||||
46
src/signal/probe.test.ts
Normal file
46
src/signal/probe.test.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { probeSignal } from "./probe.js";
|
||||||
|
|
||||||
|
const signalCheckMock = vi.fn();
|
||||||
|
const signalRpcRequestMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("./client.js", () => ({
|
||||||
|
signalCheck: (...args: unknown[]) => signalCheckMock(...args),
|
||||||
|
signalRpcRequest: (...args: unknown[]) => signalRpcRequestMock(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("probeSignal", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts version from {version} result", async () => {
|
||||||
|
signalCheckMock.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
signalRpcRequestMock.mockResolvedValueOnce({ version: "0.13.22" });
|
||||||
|
|
||||||
|
const res = await probeSignal("http://127.0.0.1:8080", 1000);
|
||||||
|
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
expect(res.version).toBe("0.13.22");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns ok=false when /check fails", async () => {
|
||||||
|
signalCheckMock.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
status: 503,
|
||||||
|
error: "HTTP 503",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await probeSignal("http://127.0.0.1:8080", 1000);
|
||||||
|
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
expect(res.status).toBe(503);
|
||||||
|
expect(res.version).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,15 @@ export type SignalProbe = {
|
|||||||
version?: string | null;
|
version?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function parseSignalVersion(value: unknown): string | null {
|
||||||
|
if (typeof value === "string" && value.trim()) return value.trim();
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
const version = (value as { version?: unknown }).version;
|
||||||
|
if (typeof version === "string" && version.trim()) return version.trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function probeSignal(
|
export async function probeSignal(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
@@ -30,11 +39,11 @@ export async function probeSignal(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const version = await signalRpcRequest<string>("version", undefined, {
|
const version = await signalRpcRequest<unknown>("version", undefined, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
});
|
});
|
||||||
result.version = typeof version === "string" ? version : null;
|
result.version = parseSignalVersion(version);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
result.error = err instanceof Error ? err.message : String(err);
|
result.error = err instanceof Error ? err.message : String(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ function parseTarget(raw: string): SignalTarget {
|
|||||||
value = value.slice("signal:".length).trim();
|
value = value.slice("signal:".length).trim();
|
||||||
}
|
}
|
||||||
if (lower.startsWith("username:")) {
|
if (lower.startsWith("username:")) {
|
||||||
return { type: "username", username: value.slice("username:".length).trim() };
|
return {
|
||||||
|
type: "username",
|
||||||
|
username: value.slice("username:".length).trim(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (lower.startsWith("u:")) {
|
if (lower.startsWith("u:")) {
|
||||||
return { type: "username", username: value.trim() };
|
return { type: "username", username: value.trim() };
|
||||||
|
|||||||
Reference in New Issue
Block a user