Merge f34ee37ef9 into 09be5d45d5
This commit is contained in:
commit
b73ef57460
@ -47,6 +47,13 @@ export type HumanDelayConfig = {
|
|||||||
maxMs?: number;
|
maxMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChunkDelayConfig = {
|
||||||
|
perCharMs?: number;
|
||||||
|
baseMs?: number;
|
||||||
|
maxMs?: number;
|
||||||
|
jitter?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type SessionSendPolicyAction = "allow" | "deny";
|
export type SessionSendPolicyAction = "allow" | "deny";
|
||||||
export type SessionSendPolicyMatch = {
|
export type SessionSendPolicyMatch = {
|
||||||
channel?: string;
|
channel?: string;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
BlockStreamingCoalesceConfig,
|
BlockStreamingCoalesceConfig,
|
||||||
|
ChunkDelayConfig,
|
||||||
DmPolicy,
|
DmPolicy,
|
||||||
GroupPolicy,
|
GroupPolicy,
|
||||||
MarkdownConfig,
|
MarkdownConfig,
|
||||||
@ -84,6 +85,8 @@ export type SignalAccountConfig = {
|
|||||||
reactionLevel?: SignalReactionLevel;
|
reactionLevel?: SignalReactionLevel;
|
||||||
/** Heartbeat visibility settings for this channel. */
|
/** Heartbeat visibility settings for this channel. */
|
||||||
heartbeat?: ChannelHeartbeatVisibilityConfig;
|
heartbeat?: ChannelHeartbeatVisibilityConfig;
|
||||||
|
/** Chunk delay config for realistic typing delays between message chunks. */
|
||||||
|
chunkDelay?: ChunkDelayConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SignalConfig = {
|
export type SignalConfig = {
|
||||||
|
|||||||
@ -239,6 +239,15 @@ export const HumanDelaySchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
export const ChunkDelaySchema = z
|
||||||
|
.object({
|
||||||
|
perCharMs: z.number().min(0).optional(),
|
||||||
|
baseMs: z.number().min(0).optional(),
|
||||||
|
maxMs: z.number().min(0).optional(),
|
||||||
|
jitter: z.number().min(0).max(1).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const CliBackendSchema = z
|
export const CliBackendSchema = z
|
||||||
.object({
|
.object({
|
||||||
command: z.string(),
|
command: z.string(),
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
import {
|
import {
|
||||||
BlockStreamingChunkSchema,
|
BlockStreamingChunkSchema,
|
||||||
BlockStreamingCoalesceSchema,
|
BlockStreamingCoalesceSchema,
|
||||||
|
ChunkDelaySchema,
|
||||||
DmConfigSchema,
|
DmConfigSchema,
|
||||||
DmPolicySchema,
|
DmPolicySchema,
|
||||||
ExecutableTokenSchema,
|
ExecutableTokenSchema,
|
||||||
@ -534,6 +535,7 @@ export const SignalAccountSchemaBase = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(),
|
reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(),
|
||||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||||
|
chunkDelay: ChunkDelaySchema.optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { chunkTextWithMode, resolveChunkMode, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
import { chunkTextWithMode, resolveChunkMode, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "../auto-reply/reply/history.js";
|
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "../auto-reply/reply/history.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
|
import type { ChunkDelayConfig } from "../config/types.base.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import type { SignalReactionNotificationMode } from "../config/types.js";
|
import type { SignalReactionNotificationMode } from "../config/types.js";
|
||||||
@ -13,7 +14,7 @@ import { signalCheck, signalRpcRequest } from "./client.js";
|
|||||||
import { spawnSignalDaemon } from "./daemon.js";
|
import { spawnSignalDaemon } from "./daemon.js";
|
||||||
import { isSignalSenderAllowed, type resolveSignalSender } from "./identity.js";
|
import { isSignalSenderAllowed, type resolveSignalSender } from "./identity.js";
|
||||||
import { createSignalEventHandler } from "./monitor/event-handler.js";
|
import { createSignalEventHandler } from "./monitor/event-handler.js";
|
||||||
import { sendMessageSignal } from "./send.js";
|
import { sendMessageSignal, sendTypingSignal } from "./send.js";
|
||||||
import { runSignalSseLoop } from "./sse-reconnect.js";
|
import { runSignalSseLoop } from "./sse-reconnect.js";
|
||||||
|
|
||||||
type SignalReactionMessage = {
|
type SignalReactionMessage = {
|
||||||
@ -206,6 +207,34 @@ async function fetchAttachment(params: {
|
|||||||
return { path: saved.path, contentType: saved.contentType };
|
return { path: saved.path, contentType: saved.contentType };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const CHUNK_DELAY_DEFAULTS = {
|
||||||
|
perCharMs: 50,
|
||||||
|
baseMs: 800,
|
||||||
|
maxMs: 6000,
|
||||||
|
jitter: 0.25,
|
||||||
|
};
|
||||||
|
|
||||||
|
function computeChunkDelay(config: ChunkDelayConfig, chunkLength: number): number {
|
||||||
|
const perCharMs = config.perCharMs ?? CHUNK_DELAY_DEFAULTS.perCharMs;
|
||||||
|
const baseMs = config.baseMs ?? CHUNK_DELAY_DEFAULTS.baseMs;
|
||||||
|
const maxMs = config.maxMs ?? CHUNK_DELAY_DEFAULTS.maxMs;
|
||||||
|
const jitter = config.jitter ?? CHUNK_DELAY_DEFAULTS.jitter;
|
||||||
|
|
||||||
|
let delayMs = baseMs + chunkLength * perCharMs;
|
||||||
|
delayMs = Math.min(delayMs, maxMs);
|
||||||
|
|
||||||
|
// Apply jitter: random variance within ±jitter range
|
||||||
|
if (jitter > 0) {
|
||||||
|
const variance = delayMs * jitter;
|
||||||
|
delayMs += (Math.random() * 2 - 1) * variance;
|
||||||
|
delayMs = Math.max(0, Math.round(delayMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
return delayMs;
|
||||||
|
}
|
||||||
|
|
||||||
async function deliverReplies(params: {
|
async function deliverReplies(params: {
|
||||||
replies: ReplyPayload[];
|
replies: ReplyPayload[];
|
||||||
target: string;
|
target: string;
|
||||||
@ -216,15 +245,53 @@ async function deliverReplies(params: {
|
|||||||
maxBytes: number;
|
maxBytes: number;
|
||||||
textLimit: number;
|
textLimit: number;
|
||||||
chunkMode: "length" | "newline";
|
chunkMode: "length" | "newline";
|
||||||
|
chunkDelay?: ChunkDelayConfig;
|
||||||
}) {
|
}) {
|
||||||
const { replies, target, baseUrl, account, accountId, runtime, maxBytes, textLimit, chunkMode } =
|
const {
|
||||||
params;
|
replies,
|
||||||
|
target,
|
||||||
|
baseUrl,
|
||||||
|
account,
|
||||||
|
accountId,
|
||||||
|
runtime,
|
||||||
|
maxBytes,
|
||||||
|
textLimit,
|
||||||
|
chunkMode,
|
||||||
|
chunkDelay,
|
||||||
|
} = params;
|
||||||
|
let isFirstPayload = true;
|
||||||
for (const payload of replies) {
|
for (const payload of replies) {
|
||||||
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
const text = payload.text ?? "";
|
const text = payload.text ?? "";
|
||||||
if (!text && mediaList.length === 0) continue;
|
if (!text && mediaList.length === 0) continue;
|
||||||
|
if (!isFirstPayload && chunkDelay) {
|
||||||
|
const delayMs = computeChunkDelay(chunkDelay, (text || "").length);
|
||||||
|
if (delayMs > 0) {
|
||||||
|
try {
|
||||||
|
await sendTypingSignal(target, { baseUrl, account, accountId });
|
||||||
|
} catch {
|
||||||
|
/* typing failure is non-fatal */
|
||||||
|
}
|
||||||
|
await sleep(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isFirstPayload = false;
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
for (const chunk of chunkTextWithMode(text, textLimit, chunkMode)) {
|
const chunks = chunkTextWithMode(text, textLimit, chunkMode);
|
||||||
|
let isFirst = true;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
if (!isFirst && chunkDelay) {
|
||||||
|
const delayMs = computeChunkDelay(chunkDelay, chunk.length);
|
||||||
|
if (delayMs > 0) {
|
||||||
|
try {
|
||||||
|
await sendTypingSignal(target, { baseUrl, account, accountId });
|
||||||
|
} catch {
|
||||||
|
/* typing failure is non-fatal */
|
||||||
|
}
|
||||||
|
await sleep(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isFirst = false;
|
||||||
await sendMessageSignal(target, chunk, {
|
await sendMessageSignal(target, chunk, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
account,
|
account,
|
||||||
@ -284,6 +351,7 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
|
|||||||
const mediaMaxBytes = (opts.mediaMaxMb ?? accountInfo.config.mediaMaxMb ?? 8) * 1024 * 1024;
|
const mediaMaxBytes = (opts.mediaMaxMb ?? accountInfo.config.mediaMaxMb ?? 8) * 1024 * 1024;
|
||||||
const ignoreAttachments = opts.ignoreAttachments ?? accountInfo.config.ignoreAttachments ?? false;
|
const ignoreAttachments = opts.ignoreAttachments ?? accountInfo.config.ignoreAttachments ?? false;
|
||||||
const sendReadReceipts = Boolean(opts.sendReadReceipts ?? accountInfo.config.sendReadReceipts);
|
const sendReadReceipts = Boolean(opts.sendReadReceipts ?? accountInfo.config.sendReadReceipts);
|
||||||
|
const chunkDelay = accountInfo.config.chunkDelay;
|
||||||
|
|
||||||
const autoStart = opts.autoStart ?? accountInfo.config.autoStart ?? !accountInfo.config.httpUrl;
|
const autoStart = opts.autoStart ?? accountInfo.config.autoStart ?? !accountInfo.config.httpUrl;
|
||||||
const startupTimeoutMs = Math.min(
|
const startupTimeoutMs = Math.min(
|
||||||
@ -347,6 +415,7 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
|
|||||||
ignoreAttachments,
|
ignoreAttachments,
|
||||||
sendReadReceipts,
|
sendReadReceipts,
|
||||||
readReceiptsViaDaemon,
|
readReceiptsViaDaemon,
|
||||||
|
chunkDelay,
|
||||||
fetchAttachment,
|
fetchAttachment,
|
||||||
deliverReplies: (params) => deliverReplies({ ...params, chunkMode }),
|
deliverReplies: (params) => deliverReplies({ ...params, chunkMode }),
|
||||||
resolveSignalReactionTargets,
|
resolveSignalReactionTargets,
|
||||||
|
|||||||
@ -207,6 +207,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
|||||||
runtime: deps.runtime,
|
runtime: deps.runtime,
|
||||||
maxBytes: deps.mediaMaxBytes,
|
maxBytes: deps.mediaMaxBytes,
|
||||||
textLimit: deps.textLimit,
|
textLimit: deps.textLimit,
|
||||||
|
chunkDelay: deps.chunkDelay,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
||||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import type { ChunkDelayConfig } from "../../config/types.base.js";
|
||||||
import type { DmPolicy, GroupPolicy, SignalReactionNotificationMode } from "../../config/types.js";
|
import type { DmPolicy, GroupPolicy, SignalReactionNotificationMode } from "../../config/types.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import type { SignalSender } from "../identity.js";
|
import type { SignalSender } from "../identity.js";
|
||||||
@ -78,6 +79,7 @@ export type SignalEventHandlerDeps = {
|
|||||||
ignoreAttachments: boolean;
|
ignoreAttachments: boolean;
|
||||||
sendReadReceipts: boolean;
|
sendReadReceipts: boolean;
|
||||||
readReceiptsViaDaemon: boolean;
|
readReceiptsViaDaemon: boolean;
|
||||||
|
chunkDelay?: ChunkDelayConfig;
|
||||||
fetchAttachment: (params: {
|
fetchAttachment: (params: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
account?: string;
|
account?: string;
|
||||||
@ -95,6 +97,7 @@ export type SignalEventHandlerDeps = {
|
|||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
maxBytes: number;
|
maxBytes: number;
|
||||||
textLimit: number;
|
textLimit: number;
|
||||||
|
chunkDelay?: ChunkDelayConfig;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
resolveSignalReactionTargets: (reaction: SignalReactionMessage) => SignalReactionTarget[];
|
resolveSignalReactionTargets: (reaction: SignalReactionMessage) => SignalReactionTarget[];
|
||||||
isSignalReactionMessage: (
|
isSignalReactionMessage: (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user