mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 06:00:45 +00:00
Merged via squash.
Prepared head SHA: 04f6a8740a
Co-authored-by: coletebou <12384893+coletebou@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
135 lines
5.4 KiB
TypeScript
135 lines
5.4 KiB
TypeScript
import {
|
|
AllowFromListSchema,
|
|
buildChannelConfigSchema,
|
|
buildCatchallMultiAccountChannelSchema,
|
|
DmPolicySchema,
|
|
GroupPolicySchema,
|
|
MarkdownConfigSchema,
|
|
ToolPolicySchema,
|
|
} from "openclaw/plugin-sdk/channel-config-schema";
|
|
import { z } from "openclaw/plugin-sdk/zod";
|
|
import { bluebubblesChannelConfigUiHints } from "./config-ui-hints.js";
|
|
import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";
|
|
|
|
const bluebubblesActionSchema = z
|
|
.object({
|
|
reactions: z.boolean().default(true),
|
|
edit: z.boolean().default(true),
|
|
unsend: z.boolean().default(true),
|
|
reply: z.boolean().default(true),
|
|
sendWithEffect: z.boolean().default(true),
|
|
renameGroup: z.boolean().default(true),
|
|
setGroupIcon: z.boolean().default(true),
|
|
addParticipant: z.boolean().default(true),
|
|
removeParticipant: z.boolean().default(true),
|
|
leaveGroup: z.boolean().default(true),
|
|
sendAttachment: z.boolean().default(true),
|
|
})
|
|
.optional();
|
|
|
|
const bluebubblesGroupConfigSchema = z.object({
|
|
requireMention: z.boolean().optional(),
|
|
tools: ToolPolicySchema,
|
|
/**
|
|
* Free-form directive appended to the system prompt for every turn that
|
|
* handles a message in this group. Use it for per-group persona tweaks or
|
|
* behavioral rules (reply-threading, tapback conventions, etc.).
|
|
*/
|
|
systemPrompt: z.string().optional(),
|
|
});
|
|
|
|
const bluebubblesNetworkSchema = z
|
|
.object({
|
|
/** Dangerous opt-in for same-host or trusted private/internal BlueBubbles deployments. */
|
|
dangerouslyAllowPrivateNetwork: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
const bluebubblesCatchupSchema = z
|
|
.object({
|
|
/** Replay messages delivered while the gateway was unreachable. Defaults to on. */
|
|
enabled: z.boolean().optional(),
|
|
/** Hard ceiling on lookback window. Clamped to [1, 720] minutes. */
|
|
maxAgeMinutes: z.number().int().positive().optional(),
|
|
/** Upper bound on messages replayed in a single startup pass. Clamped to [1, 500]. */
|
|
perRunLimit: z.number().int().positive().optional(),
|
|
/** First-run lookback used when no cursor has been persisted yet. Clamped to [1, 720]. */
|
|
firstRunLookbackMinutes: z.number().int().positive().optional(),
|
|
/**
|
|
* Consecutive-failure ceiling per message GUID. After this many failed
|
|
* processMessage attempts against the same GUID, catchup logs a WARN
|
|
* and skips the message on subsequent sweeps (letting the cursor
|
|
* advance past a permanently malformed payload). Defaults to 10.
|
|
* Clamped to [1, 1000].
|
|
*/
|
|
maxFailureRetries: z.number().int().positive().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
const bluebubblesAccountSchema = z
|
|
.object({
|
|
name: z.string().optional(),
|
|
enabled: z.boolean().optional(),
|
|
markdown: MarkdownConfigSchema,
|
|
actions: bluebubblesActionSchema,
|
|
serverUrl: z.string().optional(),
|
|
password: buildSecretInputSchema().optional(),
|
|
webhookPath: z.string().optional(),
|
|
dmPolicy: DmPolicySchema.optional(),
|
|
allowFrom: AllowFromListSchema,
|
|
groupAllowFrom: AllowFromListSchema,
|
|
groupPolicy: GroupPolicySchema.optional(),
|
|
enrichGroupParticipantsFromContacts: z.boolean().optional().default(true),
|
|
historyLimit: z.number().int().min(0).optional(),
|
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
sendTimeoutMs: z.number().int().positive().optional(),
|
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
mediaMaxMb: z.number().int().positive().optional(),
|
|
mediaLocalRoots: z.array(z.string()).optional(),
|
|
sendReadReceipts: z.boolean().optional(),
|
|
network: bluebubblesNetworkSchema,
|
|
catchup: bluebubblesCatchupSchema,
|
|
blockStreaming: z.boolean().optional(),
|
|
/**
|
|
* When an inbound reply lands without `replyToBody`/`replyToSender` and the
|
|
* in-memory reply cache misses (e.g., multi-instance deployments sharing
|
|
* one BlueBubbles account, after process restarts, or after long-lived
|
|
* cache eviction), opt in to fetching the original message from the
|
|
* BlueBubbles HTTP API as a best-effort fallback. Off by default.
|
|
*
|
|
* Left as `.optional()` rather than `.optional().default(false)` so that a
|
|
* channel-level `channels.bluebubbles.replyContextApiFallback: true` still
|
|
* propagates to accounts that omit the field. With a hard per-account
|
|
* default, the merge would clobber the channel value with `false` and
|
|
* operators would have to duplicate the flag under every `accounts.<id>`.
|
|
* (PR #71820 review)
|
|
*/
|
|
replyContextApiFallback: z.boolean().optional(),
|
|
groups: z.object({}).catchall(bluebubblesGroupConfigSchema).optional(),
|
|
coalesceSameSenderDms: z.boolean().optional(),
|
|
})
|
|
.superRefine((value, ctx) => {
|
|
const serverUrl = value.serverUrl?.trim() ?? "";
|
|
const passwordConfigured = hasConfiguredSecretInput(value.password);
|
|
if (serverUrl && !passwordConfigured) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["password"],
|
|
message: "password is required when serverUrl is configured",
|
|
});
|
|
}
|
|
});
|
|
|
|
export const BlueBubblesConfigSchema = buildCatchallMultiAccountChannelSchema(
|
|
bluebubblesAccountSchema,
|
|
).safeExtend({
|
|
actions: bluebubblesActionSchema,
|
|
});
|
|
|
|
export const BlueBubblesChannelConfigSchema = buildChannelConfigSchema(BlueBubblesConfigSchema, {
|
|
uiHints: bluebubblesChannelConfigUiHints,
|
|
});
|