mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:51:26 +00:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -52,6 +52,8 @@ export async function runAgentStep(params: {
|
||||
},
|
||||
timeoutMs: stepWaitMs + 2000,
|
||||
});
|
||||
if (wait?.status !== "ok") return undefined;
|
||||
if (wait?.status !== "ok") {
|
||||
return undefined;
|
||||
}
|
||||
return await readLatestAssistantReply({ sessionKey: params.sessionKey });
|
||||
}
|
||||
|
||||
@@ -59,16 +59,22 @@ export function createAgentsListTool(opts?: {
|
||||
const configuredNameMap = new Map<string, string>();
|
||||
for (const entry of configuredAgents) {
|
||||
const name = entry?.name?.trim() ?? "";
|
||||
if (!name) continue;
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
configuredNameMap.set(normalizeAgentId(entry.id), name);
|
||||
}
|
||||
|
||||
const allowed = new Set<string>();
|
||||
allowed.add(requesterAgentId);
|
||||
if (allowAny) {
|
||||
for (const id of configuredIds) allowed.add(id);
|
||||
for (const id of configuredIds) {
|
||||
allowed.add(id);
|
||||
}
|
||||
} else {
|
||||
for (const id of allowSet) allowed.add(id);
|
||||
for (const id of allowSet) {
|
||||
allowed.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
const all = Array.from(allowed);
|
||||
|
||||
@@ -70,7 +70,9 @@ async function resolveBrowserNodeTarget(params: {
|
||||
if (params.sandboxBridgeUrl?.trim() && params.target !== "node" && !params.requestedNode) {
|
||||
return null;
|
||||
}
|
||||
if (params.target && params.target !== "node") return null;
|
||||
if (params.target && params.target !== "node") {
|
||||
return null;
|
||||
}
|
||||
if (mode === "manual" && params.target !== "node" && !params.requestedNode) {
|
||||
return null;
|
||||
}
|
||||
@@ -101,7 +103,9 @@ async function resolveBrowserNodeTarget(params: {
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === "manual") return null;
|
||||
if (mode === "manual") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (browserNodes.length === 1) {
|
||||
const node = browserNodes[0];
|
||||
@@ -152,7 +156,9 @@ async function callBrowserProxy(params: {
|
||||
}
|
||||
|
||||
async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {
|
||||
if (!files || files.length === 0) return new Map<string, string>();
|
||||
if (!files || files.length === 0) {
|
||||
return new Map<string, string>();
|
||||
}
|
||||
const mapping = new Map<string, string>();
|
||||
for (const file of files) {
|
||||
const buffer = Buffer.from(file.base64, "base64");
|
||||
@@ -163,7 +169,9 @@ async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {
|
||||
}
|
||||
|
||||
function applyProxyPaths(result: unknown, mapping: Map<string, string>) {
|
||||
if (!result || typeof result !== "object") return;
|
||||
if (!result || typeof result !== "object") {
|
||||
return;
|
||||
}
|
||||
const obj = result as Record<string, unknown>;
|
||||
if (typeof obj.path === "string" && mapping.has(obj.path)) {
|
||||
obj.path = mapping.get(obj.path);
|
||||
@@ -402,8 +410,11 @@ export function createBrowserTool(opts?: {
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
if (targetId) await browserCloseTab(baseUrl, targetId, { profile });
|
||||
else await browserAct(baseUrl, { kind: "close" }, { profile });
|
||||
if (targetId) {
|
||||
await browserCloseTab(baseUrl, targetId, { profile });
|
||||
} else {
|
||||
await browserAct(baseUrl, { kind: "close" }, { profile });
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "snapshot": {
|
||||
@@ -592,7 +603,9 @@ export function createBrowserTool(opts?: {
|
||||
}
|
||||
case "upload": {
|
||||
const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : [];
|
||||
if (paths.length === 0) throw new Error("paths required");
|
||||
if (paths.length === 0) {
|
||||
throw new Error("paths required");
|
||||
}
|
||||
const ref = readStringParam(params, "ref");
|
||||
const inputRef = readStringParam(params, "inputRef");
|
||||
const element = readStringParam(params, "element");
|
||||
|
||||
@@ -164,7 +164,9 @@ export function createCanvasTool(): AnyAgentTool {
|
||||
: typeof params.jsonlPath === "string" && params.jsonlPath.trim()
|
||||
? await fs.readFile(params.jsonlPath.trim(), "utf8")
|
||||
: "";
|
||||
if (!jsonl.trim()) throw new Error("jsonl or jsonlPath required");
|
||||
if (!jsonl.trim()) {
|
||||
throw new Error("jsonl or jsonlPath required");
|
||||
}
|
||||
await invoke("canvas.a2ui.pushJSONL", { jsonl });
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ export function createActionGate<T extends Record<string, boolean | undefined>>(
|
||||
): ActionGate<T> {
|
||||
return (key, defaultValue = true) => {
|
||||
const value = actions?.[key];
|
||||
if (value === undefined) return defaultValue;
|
||||
if (value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value !== false;
|
||||
};
|
||||
}
|
||||
@@ -48,12 +50,16 @@ export function readStringParam(
|
||||
const { required = false, trim = true, label = key, allowEmpty = false } = options;
|
||||
const raw = params[key];
|
||||
if (typeof raw !== "string") {
|
||||
if (required) throw new Error(`${label} required`);
|
||||
if (required) {
|
||||
throw new Error(`${label} required`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const value = trim ? raw.trim() : raw;
|
||||
if (!value && !allowEmpty) {
|
||||
if (required) throw new Error(`${label} required`);
|
||||
if (required) {
|
||||
throw new Error(`${label} required`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
@@ -71,9 +77,13 @@ export function readStringOrNumberParam(
|
||||
}
|
||||
if (typeof raw === "string") {
|
||||
const value = raw.trim();
|
||||
if (value) return value;
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
if (required) {
|
||||
throw new Error(`${label} required`);
|
||||
}
|
||||
if (required) throw new Error(`${label} required`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -91,11 +101,15 @@ export function readNumberParam(
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed) {
|
||||
const parsed = Number.parseFloat(trimmed);
|
||||
if (Number.isFinite(parsed)) value = parsed;
|
||||
if (Number.isFinite(parsed)) {
|
||||
value = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value === undefined) {
|
||||
if (required) throw new Error(`${label} required`);
|
||||
if (required) {
|
||||
throw new Error(`${label} required`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return integer ? Math.trunc(value) : value;
|
||||
@@ -124,7 +138,9 @@ export function readStringArrayParam(
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
if (values.length === 0) {
|
||||
if (required) throw new Error(`${label} required`);
|
||||
if (required) {
|
||||
throw new Error(`${label} required`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return values;
|
||||
@@ -132,12 +148,16 @@ export function readStringArrayParam(
|
||||
if (typeof raw === "string") {
|
||||
const value = raw.trim();
|
||||
if (!value) {
|
||||
if (required) throw new Error(`${label} required`);
|
||||
if (required) {
|
||||
throw new Error(`${label} required`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return [value];
|
||||
}
|
||||
if (required) throw new Error(`${label} required`);
|
||||
if (required) {
|
||||
throw new Error(`${label} required`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,12 +51,16 @@ type ChatMessage = {
|
||||
|
||||
function stripExistingContext(text: string) {
|
||||
const index = text.indexOf(REMINDER_CONTEXT_MARKER);
|
||||
if (index === -1) return text;
|
||||
if (index === -1) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(0, index).trim();
|
||||
}
|
||||
|
||||
function truncateText(input: string, maxLen: number) {
|
||||
if (input.length <= maxLen) return input;
|
||||
if (input.length <= maxLen) {
|
||||
return input;
|
||||
}
|
||||
const truncated = truncateUtf16Safe(input, Math.max(0, maxLen - 3)).trimEnd();
|
||||
return `${truncated}...`;
|
||||
}
|
||||
@@ -67,17 +71,25 @@ function normalizeContextText(raw: string) {
|
||||
|
||||
function extractMessageText(message: ChatMessage): { role: string; text: string } | null {
|
||||
const role = typeof message.role === "string" ? message.role : "";
|
||||
if (role !== "user" && role !== "assistant") return null;
|
||||
if (role !== "user" && role !== "assistant") {
|
||||
return null;
|
||||
}
|
||||
const content = message.content;
|
||||
if (typeof content === "string") {
|
||||
const normalized = normalizeContextText(content);
|
||||
return normalized ? { role, text: normalized } : null;
|
||||
}
|
||||
if (!Array.isArray(content)) return null;
|
||||
if (!Array.isArray(content)) {
|
||||
return null;
|
||||
}
|
||||
const chunks: string[] = [];
|
||||
for (const block of content) {
|
||||
if (!block || typeof block !== "object") continue;
|
||||
if ((block as { type?: unknown }).type !== "text") continue;
|
||||
if (!block || typeof block !== "object") {
|
||||
continue;
|
||||
}
|
||||
if ((block as { type?: unknown }).type !== "text") {
|
||||
continue;
|
||||
}
|
||||
const text = (block as { text?: unknown }).text;
|
||||
if (typeof text === "string" && text.trim()) {
|
||||
chunks.push(text);
|
||||
@@ -96,9 +108,13 @@ async function buildReminderContextLines(params: {
|
||||
REMINDER_CONTEXT_MESSAGES_MAX,
|
||||
Math.max(0, Math.floor(params.contextMessages)),
|
||||
);
|
||||
if (maxMessages <= 0) return [];
|
||||
if (maxMessages <= 0) {
|
||||
return [];
|
||||
}
|
||||
const sessionKey = params.agentSessionKey?.trim();
|
||||
if (!sessionKey) return [];
|
||||
if (!sessionKey) {
|
||||
return [];
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const resolvedKey = resolveInternalSessionKey({ key: sessionKey, alias, mainKey });
|
||||
@@ -112,7 +128,9 @@ async function buildReminderContextLines(params: {
|
||||
.map((msg) => extractMessageText(msg as ChatMessage))
|
||||
.filter((msg): msg is { role: string; text: string } => Boolean(msg));
|
||||
const recent = parsed.slice(-maxMessages);
|
||||
if (recent.length === 0) return [];
|
||||
if (recent.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const lines: string[] = [];
|
||||
let total = 0;
|
||||
for (const entry of recent) {
|
||||
@@ -120,7 +138,9 @@ async function buildReminderContextLines(params: {
|
||||
const text = truncateText(entry.text, REMINDER_CONTEXT_PER_MESSAGE_MAX);
|
||||
const line = `- ${label}: ${text}`;
|
||||
total += line.length;
|
||||
if (total > REMINDER_CONTEXT_TOTAL_MAX) break;
|
||||
if (total > REMINDER_CONTEXT_TOTAL_MAX) {
|
||||
break;
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
return lines;
|
||||
|
||||
@@ -30,8 +30,12 @@ import {
|
||||
} from "./common.js";
|
||||
|
||||
function readParentIdParam(params: Record<string, unknown>): string | null | undefined {
|
||||
if (params.clearParent === true) return null;
|
||||
if (params.parentId === null) return null;
|
||||
if (params.clearParent === true) {
|
||||
return null;
|
||||
}
|
||||
if (params.parentId === null) {
|
||||
return null;
|
||||
}
|
||||
return readStringParam(params, "parentId");
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,9 @@ export async function handleDiscordMessagingAction(
|
||||
);
|
||||
const accountId = readStringParam(params, "accountId");
|
||||
const normalizeMessage = (message: unknown) => {
|
||||
if (!message || typeof message !== "object") return message;
|
||||
if (!message || typeof message !== "object") {
|
||||
return message;
|
||||
}
|
||||
return withNormalizedTimestamp(
|
||||
message as Record<string, unknown>,
|
||||
(message as { timestamp?: unknown }).timestamp,
|
||||
|
||||
@@ -14,7 +14,9 @@ import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool } from "./gateway.js";
|
||||
|
||||
function resolveBaseHashFromSnapshot(snapshot: unknown): string | undefined {
|
||||
if (!snapshot || typeof snapshot !== "object") return undefined;
|
||||
if (!snapshot || typeof snapshot !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const hashValue = (snapshot as { hash?: unknown }).hash;
|
||||
const rawValue = (snapshot as { raw?: unknown }).raw;
|
||||
const hash = resolveConfigSnapshotHash({
|
||||
|
||||
@@ -12,7 +12,9 @@ export function decodeDataUrl(dataUrl: string): {
|
||||
} {
|
||||
const trimmed = dataUrl.trim();
|
||||
const match = /^data:([^;,]+);base64,([a-z0-9+/=\r\n]+)$/i.exec(trimmed);
|
||||
if (!match) throw new Error("Invalid data URL (expected base64 data: URL).");
|
||||
if (!match) {
|
||||
throw new Error("Invalid data URL (expected base64 data: URL).");
|
||||
}
|
||||
const mimeType = (match[1] ?? "").trim().toLowerCase();
|
||||
if (!mimeType.startsWith("image/")) {
|
||||
throw new Error(`Unsupported data URL type: ${mimeType || "unknown"}`);
|
||||
@@ -43,7 +45,9 @@ export function coerceImageAssistantText(params: {
|
||||
throw new Error(`Image model failed (${params.provider}/${params.model}): ${errorMessage}`);
|
||||
}
|
||||
const text = extractAssistantText(params.message);
|
||||
if (text.trim()) return text.trim();
|
||||
if (text.trim()) {
|
||||
return text.trim();
|
||||
}
|
||||
throw new Error(`Image model returned no text (${params.provider}/${params.model}).`);
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,9 @@ describe("image tool implicit imageModel config", () => {
|
||||
};
|
||||
const tool = createImageTool({ config: cfg, agentDir, sandboxRoot });
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) throw new Error("expected image tool");
|
||||
if (!tool) {
|
||||
throw new Error("expected image tool");
|
||||
}
|
||||
|
||||
await expect(tool.execute("t1", { image: "https://example.com/a.png" })).rejects.toThrow(
|
||||
/Sandboxed image tool does not allow remote URLs/i,
|
||||
@@ -198,7 +200,9 @@ describe("image tool implicit imageModel config", () => {
|
||||
};
|
||||
const tool = createImageTool({ config: cfg, agentDir, sandboxRoot });
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) throw new Error("expected image tool");
|
||||
if (!tool) {
|
||||
throw new Error("expected image tool");
|
||||
}
|
||||
|
||||
const res = await tool.execute("t1", {
|
||||
prompt: "Describe the image.",
|
||||
@@ -266,7 +270,9 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
};
|
||||
const tool = createImageTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) throw new Error("expected image tool");
|
||||
if (!tool) {
|
||||
throw new Error("expected image tool");
|
||||
}
|
||||
|
||||
const res = await tool.execute("t1", {
|
||||
prompt: "Describe the image.",
|
||||
@@ -308,7 +314,9 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
};
|
||||
const tool = createImageTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) throw new Error("expected image tool");
|
||||
if (!tool) {
|
||||
throw new Error("expected image tool");
|
||||
}
|
||||
|
||||
await expect(
|
||||
tool.execute("t1", {
|
||||
|
||||
@@ -48,7 +48,9 @@ function resolveDefaultModelRef(cfg?: OpenClawConfig): {
|
||||
}
|
||||
|
||||
function hasAuthForProvider(params: { provider: string; agentDir: string }): boolean {
|
||||
if (resolveEnvApiKey(params.provider)?.apiKey) return true;
|
||||
if (resolveEnvApiKey(params.provider)?.apiKey) {
|
||||
return true;
|
||||
}
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
@@ -89,8 +91,12 @@ export function resolveImageModelConfigForTool(params: {
|
||||
const fallbacks: string[] = [];
|
||||
const addFallback = (modelRef: string | null) => {
|
||||
const ref = (modelRef ?? "").trim();
|
||||
if (!ref) return;
|
||||
if (fallbacks.includes(ref)) return;
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
if (fallbacks.includes(ref)) {
|
||||
return;
|
||||
}
|
||||
fallbacks.push(ref);
|
||||
};
|
||||
|
||||
@@ -117,8 +123,12 @@ export function resolveImageModelConfigForTool(params: {
|
||||
}
|
||||
|
||||
if (preferred?.trim()) {
|
||||
if (openaiOk) addFallback("openai/gpt-5-mini");
|
||||
if (anthropicOk) addFallback("anthropic/claude-opus-4-5");
|
||||
if (openaiOk) {
|
||||
addFallback("openai/gpt-5-mini");
|
||||
}
|
||||
if (anthropicOk) {
|
||||
addFallback("anthropic/claude-opus-4-5");
|
||||
}
|
||||
// Don't duplicate primary in fallbacks.
|
||||
const pruned = fallbacks.filter((ref) => ref !== preferred);
|
||||
return {
|
||||
@@ -129,7 +139,9 @@ export function resolveImageModelConfigForTool(params: {
|
||||
|
||||
// Cross-provider fallback when we can't pair with the primary provider.
|
||||
if (openaiOk) {
|
||||
if (anthropicOk) addFallback("anthropic/claude-opus-4-5");
|
||||
if (anthropicOk) {
|
||||
addFallback("anthropic/claude-opus-4-5");
|
||||
}
|
||||
return {
|
||||
primary: "openai/gpt-5-mini",
|
||||
...(fallbacks.length ? { fallbacks } : {}),
|
||||
@@ -305,7 +317,9 @@ export function createImageTool(options?: {
|
||||
cfg: options?.config,
|
||||
agentDir,
|
||||
});
|
||||
if (!imageModelConfig) return null;
|
||||
if (!imageModelConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If model has native vision, images in the prompt are auto-injected
|
||||
// so this tool is only needed when image wasn't provided in the prompt
|
||||
@@ -329,7 +343,9 @@ export function createImageTool(options?: {
|
||||
const imageRaw = imageRawInput.startsWith("@")
|
||||
? imageRawInput.slice(1).trim()
|
||||
: imageRawInput;
|
||||
if (!imageRaw) throw new Error("image required");
|
||||
if (!imageRaw) {
|
||||
throw new Error("image required");
|
||||
}
|
||||
|
||||
// The tool accepts file paths, file/data URLs, or http(s) URLs. In some
|
||||
// agent/model contexts, images can be referenced as pseudo-URIs like
|
||||
@@ -371,8 +387,12 @@ export function createImageTool(options?: {
|
||||
}
|
||||
|
||||
const resolvedImage = (() => {
|
||||
if (sandboxRoot) return imageRaw;
|
||||
if (imageRaw.startsWith("~")) return resolveUserPath(imageRaw);
|
||||
if (sandboxRoot) {
|
||||
return imageRaw;
|
||||
}
|
||||
if (imageRaw.startsWith("~")) {
|
||||
return resolveUserPath(imageRaw);
|
||||
}
|
||||
return imageRaw;
|
||||
})();
|
||||
const resolvedPathInfo: { resolved: string; rewrittenFrom?: string } = isDataUrl
|
||||
|
||||
@@ -34,7 +34,9 @@ describe("memory tools", () => {
|
||||
const cfg = { agents: { list: [{ id: "main", default: true }] } };
|
||||
const tool = createMemorySearchTool({ config: cfg });
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) throw new Error("tool missing");
|
||||
if (!tool) {
|
||||
throw new Error("tool missing");
|
||||
}
|
||||
|
||||
const result = await tool.execute("call_1", { query: "hello" });
|
||||
expect(result.details).toEqual({
|
||||
@@ -48,7 +50,9 @@ describe("memory tools", () => {
|
||||
const cfg = { agents: { list: [{ id: "main", default: true }] } };
|
||||
const tool = createMemoryGetTool({ config: cfg });
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) throw new Error("tool missing");
|
||||
if (!tool) {
|
||||
throw new Error("tool missing");
|
||||
}
|
||||
|
||||
const result = await tool.execute("call_2", { path: "memory/NOPE.md" });
|
||||
expect(result.details).toEqual({
|
||||
|
||||
@@ -24,12 +24,16 @@ export function createMemorySearchTool(options: {
|
||||
agentSessionKey?: string;
|
||||
}): AnyAgentTool | null {
|
||||
const cfg = options.config;
|
||||
if (!cfg) return null;
|
||||
if (!cfg) {
|
||||
return null;
|
||||
}
|
||||
const agentId = resolveSessionAgentId({
|
||||
sessionKey: options.agentSessionKey,
|
||||
config: cfg,
|
||||
});
|
||||
if (!resolveMemorySearchConfig(cfg, agentId)) return null;
|
||||
if (!resolveMemorySearchConfig(cfg, agentId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: "Memory Search",
|
||||
name: "memory_search",
|
||||
@@ -73,12 +77,16 @@ export function createMemoryGetTool(options: {
|
||||
agentSessionKey?: string;
|
||||
}): AnyAgentTool | null {
|
||||
const cfg = options.config;
|
||||
if (!cfg) return null;
|
||||
if (!cfg) {
|
||||
return null;
|
||||
}
|
||||
const agentId = resolveSessionAgentId({
|
||||
sessionKey: options.agentSessionKey,
|
||||
config: cfg,
|
||||
});
|
||||
if (!resolveMemorySearchConfig(cfg, agentId)) return null;
|
||||
if (!resolveMemorySearchConfig(cfg, agentId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: "Memory Get",
|
||||
name: "memory_get",
|
||||
|
||||
@@ -88,8 +88,12 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boole
|
||||
),
|
||||
),
|
||||
};
|
||||
if (!options.includeButtons) delete props.buttons;
|
||||
if (!options.includeCards) delete props.card;
|
||||
if (!options.includeButtons) {
|
||||
delete props.buttons;
|
||||
}
|
||||
if (!options.includeCards) {
|
||||
delete props.card;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
@@ -262,7 +266,9 @@ function buildMessageToolSchema(cfg: OpenClawConfig) {
|
||||
|
||||
function resolveAgentAccountId(value?: string): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeAccountId(trimmed);
|
||||
}
|
||||
|
||||
@@ -272,9 +278,13 @@ function filterActionsForContext(params: {
|
||||
currentChannelId?: string;
|
||||
}): ChannelMessageActionName[] {
|
||||
const channel = normalizeMessageChannel(params.channel);
|
||||
if (!channel || channel !== "bluebubbles") return params.actions;
|
||||
if (!channel || channel !== "bluebubbles") {
|
||||
return params.actions;
|
||||
}
|
||||
const currentChannelId = params.currentChannelId?.trim();
|
||||
if (!currentChannelId) return params.actions;
|
||||
if (!currentChannelId) {
|
||||
return params.actions;
|
||||
}
|
||||
const normalizedTarget =
|
||||
normalizeTargetForProvider(channel, currentChannelId) ?? currentChannelId;
|
||||
const lowered = normalizedTarget.trim().toLowerCase();
|
||||
@@ -283,7 +293,9 @@ function filterActionsForContext(params: {
|
||||
lowered.startsWith("chat_id:") ||
|
||||
lowered.startsWith("chat_identifier:") ||
|
||||
lowered.startsWith("group:");
|
||||
if (isGroupTarget) return params.actions;
|
||||
if (isGroupTarget) {
|
||||
return params.actions;
|
||||
}
|
||||
return params.actions.filter((action) => !BLUEBUBBLES_GROUP_ACTIONS.has(action));
|
||||
}
|
||||
|
||||
@@ -396,7 +408,9 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
});
|
||||
|
||||
const toolResult = getToolResult(result);
|
||||
if (toolResult) return toolResult;
|
||||
if (toolResult) {
|
||||
return toolResult;
|
||||
}
|
||||
return jsonResult(result.payload);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -89,11 +89,15 @@ function pickDefaultNode(nodes: NodeListNode[]): NodeListNode | null {
|
||||
const withCanvas = nodes.filter((n) =>
|
||||
Array.isArray(n.caps) ? n.caps.includes("canvas") : true,
|
||||
);
|
||||
if (withCanvas.length === 0) return null;
|
||||
if (withCanvas.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const connected = withCanvas.filter((n) => n.connected);
|
||||
const candidates = connected.length > 0 ? connected : withCanvas;
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
if (candidates.length === 1) {
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
const local = candidates.filter(
|
||||
(n) =>
|
||||
@@ -101,7 +105,9 @@ function pickDefaultNode(nodes: NodeListNode[]): NodeListNode | null {
|
||||
typeof n.nodeId === "string" &&
|
||||
n.nodeId.startsWith("mac-"),
|
||||
);
|
||||
if (local.length === 1) return local[0];
|
||||
if (local.length === 1) {
|
||||
return local[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -119,22 +125,34 @@ export function resolveNodeIdFromList(
|
||||
if (!q) {
|
||||
if (allowDefault) {
|
||||
const picked = pickDefaultNode(nodes);
|
||||
if (picked) return picked.nodeId;
|
||||
if (picked) {
|
||||
return picked.nodeId;
|
||||
}
|
||||
}
|
||||
throw new Error("node required");
|
||||
}
|
||||
|
||||
const qNorm = normalizeNodeKey(q);
|
||||
const matches = nodes.filter((n) => {
|
||||
if (n.nodeId === q) return true;
|
||||
if (typeof n.remoteIp === "string" && n.remoteIp === q) return true;
|
||||
if (n.nodeId === q) {
|
||||
return true;
|
||||
}
|
||||
if (typeof n.remoteIp === "string" && n.remoteIp === q) {
|
||||
return true;
|
||||
}
|
||||
const name = typeof n.displayName === "string" ? n.displayName : "";
|
||||
if (name && normalizeNodeKey(name) === qNorm) return true;
|
||||
if (q.length >= 6 && n.nodeId.startsWith(q)) return true;
|
||||
if (name && normalizeNodeKey(name) === qNorm) {
|
||||
return true;
|
||||
}
|
||||
if (q.length >= 6 && n.nodeId.startsWith(q)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (matches.length === 1) return matches[0].nodeId;
|
||||
if (matches.length === 1) {
|
||||
return matches[0].nodeId;
|
||||
}
|
||||
if (matches.length === 0) {
|
||||
const known = nodes
|
||||
.map((n) => n.displayName || n.remoteIp || n.nodeId)
|
||||
|
||||
@@ -55,7 +55,9 @@ const SessionStatusToolSchema = Type.Object({
|
||||
|
||||
function formatApiKeySnippet(apiKey: string): string {
|
||||
const compact = apiKey.replace(/\s+/g, "");
|
||||
if (!compact) return "unknown";
|
||||
if (!compact) {
|
||||
return "unknown";
|
||||
}
|
||||
const edge = compact.length >= 12 ? 6 : 4;
|
||||
const head = compact.slice(0, edge);
|
||||
const tail = compact.slice(-edge);
|
||||
@@ -69,7 +71,9 @@ function resolveModelAuthLabel(params: {
|
||||
agentDir?: string;
|
||||
}): string | undefined {
|
||||
const resolvedProvider = params.provider?.trim();
|
||||
if (!resolvedProvider) return undefined;
|
||||
if (!resolvedProvider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const providerKey = normalizeProviderId(resolvedProvider);
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
@@ -126,7 +130,9 @@ function resolveSessionEntry(params: {
|
||||
mainKey: string;
|
||||
}): { key: string; entry: SessionEntry } | null {
|
||||
const keyRaw = params.keyRaw.trim();
|
||||
if (!keyRaw) return null;
|
||||
if (!keyRaw) {
|
||||
return null;
|
||||
}
|
||||
const internal = resolveInternalSessionKey({
|
||||
key: keyRaw,
|
||||
alias: params.alias,
|
||||
@@ -149,7 +155,9 @@ function resolveSessionEntry(params: {
|
||||
|
||||
for (const key of candidates) {
|
||||
const entry = params.store[key];
|
||||
if (entry) return { key, entry };
|
||||
if (entry) {
|
||||
return { key, entry };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -161,11 +169,17 @@ function resolveSessionKeyFromSessionId(params: {
|
||||
agentId?: string;
|
||||
}): string | null {
|
||||
const trimmed = params.sessionId.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const { store } = loadCombinedSessionStoreForGateway(params.cfg);
|
||||
const match = Object.entries(store).find(([key, entry]) => {
|
||||
if (entry?.sessionId !== trimmed) return false;
|
||||
if (!params.agentId) return true;
|
||||
if (entry?.sessionId !== trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (!params.agentId) {
|
||||
return true;
|
||||
}
|
||||
return resolveAgentIdFromSessionKey(key) === params.agentId;
|
||||
});
|
||||
return match?.[0] ?? null;
|
||||
@@ -186,8 +200,12 @@ async function resolveModelOverride(params: {
|
||||
}
|
||||
> {
|
||||
const raw = params.raw.trim();
|
||||
if (!raw) return { kind: "reset" };
|
||||
if (raw.toLowerCase() === "default") return { kind: "reset" };
|
||||
if (!raw) {
|
||||
return { kind: "reset" };
|
||||
}
|
||||
if (raw.toLowerCase() === "default") {
|
||||
return { kind: "reset" };
|
||||
}
|
||||
|
||||
const configDefault = resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
@@ -256,7 +274,9 @@ export function createSessionStatusTool(opts?: {
|
||||
opts?.agentSessionKey ?? requestedKeyRaw,
|
||||
);
|
||||
const ensureAgentAccess = (targetAgentId: string) => {
|
||||
if (targetAgentId === requesterAgentId) return;
|
||||
if (targetAgentId === requesterAgentId) {
|
||||
return;
|
||||
}
|
||||
// Gate cross-agent access behind tools.agentToAgent settings.
|
||||
if (!a2aPolicy.enabled) {
|
||||
throw new Error(
|
||||
|
||||
@@ -46,7 +46,9 @@ export async function resolveAnnounceTarget(params: {
|
||||
const accountId =
|
||||
(typeof deliveryContext?.accountId === "string" ? deliveryContext.accountId : undefined) ??
|
||||
(typeof match?.lastAccountId === "string" ? match.lastAccountId : undefined);
|
||||
if (channel && to) return { channel, to, accountId };
|
||||
if (channel && to) {
|
||||
return { channel, to, accountId };
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -53,13 +53,19 @@ export function resolveMainSessionAlias(cfg: OpenClawConfig) {
|
||||
}
|
||||
|
||||
export function resolveDisplaySessionKey(params: { key: string; alias: string; mainKey: string }) {
|
||||
if (params.key === params.alias) return "main";
|
||||
if (params.key === params.mainKey) return "main";
|
||||
if (params.key === params.alias) {
|
||||
return "main";
|
||||
}
|
||||
if (params.key === params.mainKey) {
|
||||
return "main";
|
||||
}
|
||||
return params.key;
|
||||
}
|
||||
|
||||
export function resolveInternalSessionKey(params: { key: string; alias: string; mainKey: string }) {
|
||||
if (params.key === "main") return params.alias;
|
||||
if (params.key === "main") {
|
||||
return params.alias;
|
||||
}
|
||||
return params.key;
|
||||
}
|
||||
|
||||
@@ -74,20 +80,32 @@ export function createAgentToAgentPolicy(cfg: OpenClawConfig): AgentToAgentPolic
|
||||
const enabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
|
||||
const matchesAllow = (agentId: string) => {
|
||||
if (allowPatterns.length === 0) return true;
|
||||
if (allowPatterns.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return allowPatterns.some((pattern) => {
|
||||
const raw = String(pattern ?? "").trim();
|
||||
if (!raw) return false;
|
||||
if (raw === "*") return true;
|
||||
if (!raw.includes("*")) return raw === agentId;
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
if (raw === "*") {
|
||||
return true;
|
||||
}
|
||||
if (!raw.includes("*")) {
|
||||
return raw === agentId;
|
||||
}
|
||||
const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`, "i");
|
||||
return re.test(agentId);
|
||||
});
|
||||
};
|
||||
const isAllowed = (requesterAgentId: string, targetAgentId: string) => {
|
||||
if (requesterAgentId === targetAgentId) return true;
|
||||
if (!enabled) return false;
|
||||
if (requesterAgentId === targetAgentId) {
|
||||
return true;
|
||||
}
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
return matchesAllow(requesterAgentId) && matchesAllow(targetAgentId);
|
||||
};
|
||||
return { enabled, matchesAllow, isAllowed };
|
||||
@@ -101,14 +119,28 @@ export function looksLikeSessionId(value: string): boolean {
|
||||
|
||||
export function looksLikeSessionKey(value: string): boolean {
|
||||
const raw = value.trim();
|
||||
if (!raw) return false;
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
// These are canonical key shapes that should never be treated as sessionIds.
|
||||
if (raw === "main" || raw === "global" || raw === "unknown") return true;
|
||||
if (isAcpSessionKey(raw)) return true;
|
||||
if (raw.startsWith("agent:")) return true;
|
||||
if (raw.startsWith("cron:") || raw.startsWith("hook:")) return true;
|
||||
if (raw.startsWith("node-") || raw.startsWith("node:")) return true;
|
||||
if (raw.includes(":group:") || raw.includes(":channel:")) return true;
|
||||
if (raw === "main" || raw === "global" || raw === "unknown") {
|
||||
return true;
|
||||
}
|
||||
if (isAcpSessionKey(raw)) {
|
||||
return true;
|
||||
}
|
||||
if (raw.startsWith("agent:")) {
|
||||
return true;
|
||||
}
|
||||
if (raw.startsWith("cron:") || raw.startsWith("hook:")) {
|
||||
return true;
|
||||
}
|
||||
if (raw.startsWith("node-") || raw.startsWith("node:")) {
|
||||
return true;
|
||||
}
|
||||
if (raw.includes(":group:") || raw.includes(":channel:")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -196,7 +228,9 @@ async function resolveSessionKeyFromKey(params: {
|
||||
},
|
||||
});
|
||||
const key = typeof result?.key === "string" ? result.key.trim() : "";
|
||||
if (!key) return null;
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
key,
|
||||
@@ -229,7 +263,9 @@ export async function resolveSessionReference(params: {
|
||||
requesterInternalKey: params.requesterInternalKey,
|
||||
restrictToSpawned: params.restrictToSpawned,
|
||||
});
|
||||
if (resolvedByKey) return resolvedByKey;
|
||||
if (resolvedByKey) {
|
||||
return resolvedByKey;
|
||||
}
|
||||
return await resolveSessionKeyFromSessionId({
|
||||
sessionId: raw,
|
||||
alias: params.alias,
|
||||
@@ -259,11 +295,21 @@ export function classifySessionKind(params: {
|
||||
mainKey: string;
|
||||
}): SessionKind {
|
||||
const key = params.key;
|
||||
if (key === params.alias || key === params.mainKey) return "main";
|
||||
if (key.startsWith("cron:")) return "cron";
|
||||
if (key.startsWith("hook:")) return "hook";
|
||||
if (key.startsWith("node-") || key.startsWith("node:")) return "node";
|
||||
if (params.gatewayKind === "group") return "group";
|
||||
if (key === params.alias || key === params.mainKey) {
|
||||
return "main";
|
||||
}
|
||||
if (key.startsWith("cron:")) {
|
||||
return "cron";
|
||||
}
|
||||
if (key.startsWith("hook:")) {
|
||||
return "hook";
|
||||
}
|
||||
if (key.startsWith("node-") || key.startsWith("node:")) {
|
||||
return "node";
|
||||
}
|
||||
if (params.gatewayKind === "group") {
|
||||
return "group";
|
||||
}
|
||||
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||
return "group";
|
||||
}
|
||||
@@ -276,11 +322,17 @@ export function deriveChannel(params: {
|
||||
channel?: string | null;
|
||||
lastChannel?: string | null;
|
||||
}): string {
|
||||
if (params.kind === "cron" || params.kind === "hook" || params.kind === "node") return "internal";
|
||||
if (params.kind === "cron" || params.kind === "hook" || params.kind === "node") {
|
||||
return "internal";
|
||||
}
|
||||
const channel = normalizeKey(params.channel ?? undefined);
|
||||
if (channel) return channel;
|
||||
if (channel) {
|
||||
return channel;
|
||||
}
|
||||
const lastChannel = normalizeKey(params.lastChannel ?? undefined);
|
||||
if (lastChannel) return lastChannel;
|
||||
if (lastChannel) {
|
||||
return lastChannel;
|
||||
}
|
||||
const parts = params.key.split(":").filter(Boolean);
|
||||
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
|
||||
return parts[0];
|
||||
@@ -290,7 +342,9 @@ export function deriveChannel(params: {
|
||||
|
||||
export function stripToolMessages(messages: unknown[]): unknown[] {
|
||||
return messages.filter((msg) => {
|
||||
if (!msg || typeof msg !== "object") return true;
|
||||
if (!msg || typeof msg !== "object") {
|
||||
return true;
|
||||
}
|
||||
const role = (msg as { role?: unknown }).role;
|
||||
return role !== "toolResult";
|
||||
});
|
||||
@@ -301,19 +355,31 @@ export function stripToolMessages(messages: unknown[]): unknown[] {
|
||||
* This ensures user-facing text doesn't leak internal tool representations.
|
||||
*/
|
||||
export function sanitizeTextContent(text: string): string {
|
||||
if (!text) return text;
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
return stripThinkingTagsFromText(stripDowngradedToolCallText(stripMinimaxToolCallXml(text)));
|
||||
}
|
||||
|
||||
export function extractAssistantText(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") return undefined;
|
||||
if ((message as { role?: unknown }).role !== "assistant") return undefined;
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
if ((message as { role?: unknown }).role !== "assistant") {
|
||||
return undefined;
|
||||
}
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (!Array.isArray(content)) return undefined;
|
||||
if (!Array.isArray(content)) {
|
||||
return undefined;
|
||||
}
|
||||
const chunks: string[] = [];
|
||||
for (const block of content) {
|
||||
if (!block || typeof block !== "object") continue;
|
||||
if ((block as { type?: unknown }).type !== "text") continue;
|
||||
if (!block || typeof block !== "object") {
|
||||
continue;
|
||||
}
|
||||
if ((block as { type?: unknown }).type !== "text") {
|
||||
continue;
|
||||
}
|
||||
const text = (block as { text?: unknown }).text;
|
||||
if (typeof text === "string") {
|
||||
const sanitized = sanitizeTextContent(text);
|
||||
|
||||
@@ -97,20 +97,32 @@ export function createSessionsListTool(opts?: {
|
||||
const rows: SessionListRow[] = [];
|
||||
|
||||
for (const entry of sessions) {
|
||||
if (!entry || typeof entry !== "object") continue;
|
||||
if (!entry || typeof entry !== "object") {
|
||||
continue;
|
||||
}
|
||||
const key = typeof entry.key === "string" ? entry.key : "";
|
||||
if (!key) continue;
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryAgentId = resolveAgentIdFromSessionKey(key);
|
||||
const crossAgent = entryAgentId !== requesterAgentId;
|
||||
if (crossAgent && !a2aPolicy.isAllowed(requesterAgentId, entryAgentId)) continue;
|
||||
if (crossAgent && !a2aPolicy.isAllowed(requesterAgentId, entryAgentId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "unknown") continue;
|
||||
if (key === "global" && alias !== "global") continue;
|
||||
if (key === "unknown") {
|
||||
continue;
|
||||
}
|
||||
if (key === "global" && alias !== "global") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const gatewayKind = typeof entry.kind === "string" ? entry.kind : undefined;
|
||||
const kind = classifySessionKind({ key, gatewayKind, alias, mainKey });
|
||||
if (allowedKinds && !allowedKinds.has(kind)) continue;
|
||||
if (allowedKinds && !allowedKinds.has(kind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const displayKey = resolveDisplaySessionKey({
|
||||
key,
|
||||
|
||||
@@ -20,9 +20,13 @@ export type AnnounceTarget = {
|
||||
export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget | null {
|
||||
const rawParts = sessionKey.split(":").filter(Boolean);
|
||||
const parts = rawParts.length >= 3 && rawParts[0] === "agent" ? rawParts.slice(2) : rawParts;
|
||||
if (parts.length < 3) return null;
|
||||
if (parts.length < 3) {
|
||||
return null;
|
||||
}
|
||||
const [channelRaw, kind, ...rest] = parts;
|
||||
if (kind !== "group" && kind !== "channel") return null;
|
||||
if (kind !== "group" && kind !== "channel") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract topic/thread ID from rest (supports both :topic: and :thread:)
|
||||
// Telegram uses :topic:, other platforms use :thread:
|
||||
@@ -39,12 +43,18 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget
|
||||
// Remove :topic:N or :thread:N suffix from ID for target
|
||||
const id = match ? restJoined.replace(/:(topic|thread):\d+$/, "") : restJoined.trim();
|
||||
|
||||
if (!id) return null;
|
||||
if (!channelRaw) return null;
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
if (!channelRaw) {
|
||||
return null;
|
||||
}
|
||||
const normalizedChannel = normalizeAnyChannelId(channelRaw) ?? normalizeChatChannelId(channelRaw);
|
||||
const channel = normalizedChannel ?? channelRaw.toLowerCase();
|
||||
const kindTarget = (() => {
|
||||
if (!normalizedChannel) return id;
|
||||
if (!normalizedChannel) {
|
||||
return id;
|
||||
}
|
||||
if (normalizedChannel === "discord" || normalizedChannel === "slack") {
|
||||
return `channel:${id}`;
|
||||
}
|
||||
@@ -148,7 +158,9 @@ export function isReplySkip(text?: string) {
|
||||
export function resolvePingPongTurns(cfg?: OpenClawConfig) {
|
||||
const raw = cfg?.session?.agentToAgent?.maxPingPongTurns;
|
||||
const fallback = DEFAULT_PING_PONG_TURNS;
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) return fallback;
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
||||
return fallback;
|
||||
}
|
||||
const rounded = Math.floor(raw);
|
||||
return Math.max(0, Math.min(MAX_PING_PONG_TURNS, rounded));
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ export async function runSessionsSendA2AFlow(params: {
|
||||
latestReply = primaryReply;
|
||||
}
|
||||
}
|
||||
if (!latestReply) return;
|
||||
if (!latestReply) {
|
||||
return;
|
||||
}
|
||||
|
||||
const announceTarget = await resolveAnnounceTarget({
|
||||
sessionKey: params.targetSessionKey,
|
||||
|
||||
@@ -38,11 +38,17 @@ const SessionsSpawnToolSchema = Type.Object({
|
||||
});
|
||||
|
||||
function splitModelRef(ref?: string) {
|
||||
if (!ref) return { provider: undefined, model: undefined };
|
||||
if (!ref) {
|
||||
return { provider: undefined, model: undefined };
|
||||
}
|
||||
const trimmed = ref.trim();
|
||||
if (!trimmed) return { provider: undefined, model: undefined };
|
||||
if (!trimmed) {
|
||||
return { provider: undefined, model: undefined };
|
||||
}
|
||||
const [provider, model] = trimmed.split("/", 2);
|
||||
if (model) return { provider, model };
|
||||
if (model) {
|
||||
return { provider, model };
|
||||
}
|
||||
return { provider: undefined, model: trimmed };
|
||||
}
|
||||
|
||||
@@ -51,9 +57,13 @@ function normalizeModelSelection(value: unknown): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
if (!value || typeof value !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const primary = (value as { primary?: unknown }).primary;
|
||||
if (typeof primary === "string" && primary.trim()) return primary.trim();
|
||||
if (typeof primary === "string" && primary.trim()) {
|
||||
return primary.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -96,7 +106,9 @@ export function createSessionsSpawnTool(opts?: {
|
||||
typeof params.runTimeoutSeconds === "number" && Number.isFinite(params.runTimeoutSeconds)
|
||||
? Math.max(0, Math.floor(params.runTimeoutSeconds))
|
||||
: undefined;
|
||||
if (explicit !== undefined) return explicit;
|
||||
if (explicit !== undefined) {
|
||||
return explicit;
|
||||
}
|
||||
const legacy =
|
||||
typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds)
|
||||
? Math.max(0, Math.floor(params.timeoutSeconds))
|
||||
|
||||
@@ -49,16 +49,24 @@ function resolveThreadTsFromContext(
|
||||
context: SlackActionContext | undefined,
|
||||
): string | undefined {
|
||||
// Agent explicitly provided threadTs - use it
|
||||
if (explicitThreadTs) return explicitThreadTs;
|
||||
if (explicitThreadTs) {
|
||||
return explicitThreadTs;
|
||||
}
|
||||
// No context or missing required fields
|
||||
if (!context?.currentThreadTs || !context?.currentChannelId) return undefined;
|
||||
if (!context?.currentThreadTs || !context?.currentChannelId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsedTarget = parseSlackTarget(targetChannel, { defaultKind: "channel" });
|
||||
if (!parsedTarget || parsedTarget.kind !== "channel") return undefined;
|
||||
if (!parsedTarget || parsedTarget.kind !== "channel") {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedTarget = parsedTarget.id;
|
||||
|
||||
// Different channel - don't inject
|
||||
if (normalizedTarget !== context.currentChannelId) return undefined;
|
||||
if (normalizedTarget !== context.currentChannelId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check replyToMode
|
||||
if (context.replyToMode === "all") {
|
||||
@@ -93,15 +101,21 @@ export async function handleSlackAction(
|
||||
|
||||
// Choose the most appropriate token for Slack read/write operations.
|
||||
const getTokenForOperation = (operation: "read" | "write") => {
|
||||
if (operation === "read") return userToken ?? botToken;
|
||||
if (!allowUserWrites) return botToken;
|
||||
if (operation === "read") {
|
||||
return userToken ?? botToken;
|
||||
}
|
||||
if (!allowUserWrites) {
|
||||
return botToken;
|
||||
}
|
||||
return botToken ?? userToken;
|
||||
};
|
||||
|
||||
const buildActionOpts = (operation: "read" | "write") => {
|
||||
const token = getTokenForOperation(operation);
|
||||
const tokenOverride = token && token !== botToken ? token : undefined;
|
||||
if (!accountId && !tokenOverride) return undefined;
|
||||
if (!accountId && !tokenOverride) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(accountId ? { accountId } : {}),
|
||||
...(tokenOverride ? { token: tokenOverride } : {}),
|
||||
|
||||
@@ -32,7 +32,9 @@ export function readTelegramButtons(
|
||||
params: Record<string, unknown>,
|
||||
): TelegramButton[][] | undefined {
|
||||
const raw = params.buttons;
|
||||
if (raw == null) return undefined;
|
||||
if (raw == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (!Array.isArray(raw)) {
|
||||
throw new Error("buttons must be an array of button rows");
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ export function createTtsTool(opts?: {
|
||||
if (result.success && result.audioPath) {
|
||||
const lines: string[] = [];
|
||||
// Tag Telegram Opus output as a voice bubble instead of a file attachment.
|
||||
if (result.voiceCompatible) lines.push("[[audio_as_voice]]");
|
||||
if (result.voiceCompatible) {
|
||||
lines.push("[[audio_as_voice]]");
|
||||
}
|
||||
lines.push(`MEDIA:${result.audioPath}`);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
|
||||
@@ -34,7 +34,9 @@ export function htmlToMarkdown(html: string): { text: string; title?: string } {
|
||||
.replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
|
||||
text = text.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, body) => {
|
||||
const label = normalizeWhitespace(stripTags(body));
|
||||
if (!label) return href;
|
||||
if (!label) {
|
||||
return href;
|
||||
}
|
||||
return `[${label}](${href})`;
|
||||
});
|
||||
text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, body) => {
|
||||
@@ -72,7 +74,9 @@ export function truncateText(
|
||||
value: string,
|
||||
maxChars: number,
|
||||
): { text: string; truncated: boolean } {
|
||||
if (value.length <= maxChars) return { text: value, truncated: false };
|
||||
if (value.length <= maxChars) {
|
||||
return { text: value, truncated: false };
|
||||
}
|
||||
return { text: value.slice(0, maxChars), truncated: true };
|
||||
}
|
||||
|
||||
@@ -102,7 +106,9 @@ export async function extractReadableContent(params: {
|
||||
}
|
||||
const reader = new Readability(document, { charThreshold: 0 });
|
||||
const parsed = reader.parse();
|
||||
if (!parsed?.content) return fallback();
|
||||
if (!parsed?.content) {
|
||||
return fallback();
|
||||
}
|
||||
const title = parsed.title || undefined;
|
||||
if (params.extractMode === "text") {
|
||||
const text = normalizeWhitespace(parsed.textContent ?? "");
|
||||
|
||||
@@ -80,24 +80,34 @@ type FirecrawlFetchConfig =
|
||||
|
||||
function resolveFetchConfig(cfg?: OpenClawConfig): WebFetchConfig {
|
||||
const fetch = cfg?.tools?.web?.fetch;
|
||||
if (!fetch || typeof fetch !== "object") return undefined;
|
||||
if (!fetch || typeof fetch !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return fetch as WebFetchConfig;
|
||||
}
|
||||
|
||||
function resolveFetchEnabled(params: { fetch?: WebFetchConfig; sandboxed?: boolean }): boolean {
|
||||
if (typeof params.fetch?.enabled === "boolean") return params.fetch.enabled;
|
||||
if (typeof params.fetch?.enabled === "boolean") {
|
||||
return params.fetch.enabled;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveFetchReadabilityEnabled(fetch?: WebFetchConfig): boolean {
|
||||
if (typeof fetch?.readability === "boolean") return fetch.readability;
|
||||
if (typeof fetch?.readability === "boolean") {
|
||||
return fetch.readability;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveFirecrawlConfig(fetch?: WebFetchConfig): FirecrawlFetchConfig {
|
||||
if (!fetch || typeof fetch !== "object") return undefined;
|
||||
if (!fetch || typeof fetch !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const firecrawl = "firecrawl" in fetch ? fetch.firecrawl : undefined;
|
||||
if (!firecrawl || typeof firecrawl !== "object") return undefined;
|
||||
if (!firecrawl || typeof firecrawl !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return firecrawl as FirecrawlFetchConfig;
|
||||
}
|
||||
|
||||
@@ -114,7 +124,9 @@ function resolveFirecrawlEnabled(params: {
|
||||
firecrawl?: FirecrawlFetchConfig;
|
||||
apiKey?: string;
|
||||
}): boolean {
|
||||
if (typeof params.firecrawl?.enabled === "boolean") return params.firecrawl.enabled;
|
||||
if (typeof params.firecrawl?.enabled === "boolean") {
|
||||
return params.firecrawl.enabled;
|
||||
}
|
||||
return Boolean(params.apiKey);
|
||||
}
|
||||
|
||||
@@ -127,7 +139,9 @@ function resolveFirecrawlBaseUrl(firecrawl?: FirecrawlFetchConfig): string {
|
||||
}
|
||||
|
||||
function resolveFirecrawlOnlyMainContent(firecrawl?: FirecrawlFetchConfig): boolean {
|
||||
if (typeof firecrawl?.onlyMainContent === "boolean") return firecrawl.onlyMainContent;
|
||||
if (typeof firecrawl?.onlyMainContent === "boolean") {
|
||||
return firecrawl.onlyMainContent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -136,14 +150,18 @@ function resolveFirecrawlMaxAgeMs(firecrawl?: FirecrawlFetchConfig): number | un
|
||||
firecrawl && "maxAgeMs" in firecrawl && typeof firecrawl.maxAgeMs === "number"
|
||||
? firecrawl.maxAgeMs
|
||||
: undefined;
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) return undefined;
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Math.max(0, Math.floor(raw));
|
||||
return parsed > 0 ? parsed : undefined;
|
||||
}
|
||||
|
||||
function resolveFirecrawlMaxAgeMsOrDefault(firecrawl?: FirecrawlFetchConfig): number {
|
||||
const resolved = resolveFirecrawlMaxAgeMs(firecrawl);
|
||||
if (typeof resolved === "number") return resolved;
|
||||
if (typeof resolved === "number") {
|
||||
return resolved;
|
||||
}
|
||||
return DEFAULT_FIRECRAWL_MAX_AGE_MS;
|
||||
}
|
||||
|
||||
@@ -159,7 +177,9 @@ function resolveMaxRedirects(value: unknown, fallback: number): number {
|
||||
|
||||
function looksLikeHtml(value: string): boolean {
|
||||
const trimmed = value.trimStart();
|
||||
if (!trimmed) return false;
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
const head = trimmed.slice(0, 256).toLowerCase();
|
||||
return head.startsWith("<!doctype html") || head.startsWith("<html");
|
||||
}
|
||||
@@ -243,7 +263,9 @@ function formatWebFetchErrorDetail(params: {
|
||||
maxChars: number;
|
||||
}): string {
|
||||
const { detail, contentType, maxChars } = params;
|
||||
if (!detail) return "";
|
||||
if (!detail) {
|
||||
return "";
|
||||
}
|
||||
let text = detail;
|
||||
const contentTypeLower = contentType?.toLowerCase();
|
||||
if (contentTypeLower?.includes("text/html") || looksLikeHtml(detail)) {
|
||||
@@ -351,7 +373,9 @@ async function runWebFetch(params: {
|
||||
`fetch:${params.url}:${params.extractMode}:${params.maxChars}`,
|
||||
);
|
||||
const cached = readCache(FETCH_CACHE, cacheKey);
|
||||
if (cached) return { ...cached.value, cached: true };
|
||||
if (cached) {
|
||||
return { ...cached.value, cached: true };
|
||||
}
|
||||
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
@@ -535,7 +559,9 @@ async function tryFirecrawlFallback(params: {
|
||||
firecrawlStoreInCache: boolean;
|
||||
firecrawlTimeoutSeconds: number;
|
||||
}): Promise<{ text: string; title?: string } | null> {
|
||||
if (!params.firecrawlEnabled || !params.firecrawlApiKey) return null;
|
||||
if (!params.firecrawlEnabled || !params.firecrawlApiKey) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const firecrawl = await fetchFirecrawlContent({
|
||||
url: params.url,
|
||||
@@ -556,7 +582,9 @@ async function tryFirecrawlFallback(params: {
|
||||
|
||||
function resolveFirecrawlEndpoint(baseUrl: string): string {
|
||||
const trimmed = baseUrl.trim();
|
||||
if (!trimmed) return `${DEFAULT_FIRECRAWL_BASE_URL}/v2/scrape`;
|
||||
if (!trimmed) {
|
||||
return `${DEFAULT_FIRECRAWL_BASE_URL}/v2/scrape`;
|
||||
}
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
if (url.pathname && url.pathname !== "/") {
|
||||
@@ -574,7 +602,9 @@ export function createWebFetchTool(options?: {
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool | null {
|
||||
const fetch = resolveFetchConfig(options?.config);
|
||||
if (!resolveFetchEnabled({ fetch, sandboxed: options?.sandboxed })) return null;
|
||||
if (!resolveFetchEnabled({ fetch, sandboxed: options?.sandboxed })) {
|
||||
return null;
|
||||
}
|
||||
const readabilityEnabled = resolveFetchReadabilityEnabled(fetch);
|
||||
const firecrawl = resolveFirecrawlConfig(fetch);
|
||||
const firecrawlApiKey = resolveFirecrawlApiKey(firecrawl);
|
||||
|
||||
@@ -105,13 +105,19 @@ type PerplexityBaseUrlHint = "direct" | "openrouter";
|
||||
|
||||
function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
|
||||
const search = cfg?.tools?.web?.search;
|
||||
if (!search || typeof search !== "object") return undefined;
|
||||
if (!search || typeof search !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return search as WebSearchConfig;
|
||||
}
|
||||
|
||||
function resolveSearchEnabled(params: { search?: WebSearchConfig; sandboxed?: boolean }): boolean {
|
||||
if (typeof params.search?.enabled === "boolean") return params.search.enabled;
|
||||
if (params.sandboxed) return true;
|
||||
if (typeof params.search?.enabled === "boolean") {
|
||||
return params.search.enabled;
|
||||
}
|
||||
if (params.sandboxed) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -143,15 +149,23 @@ function resolveSearchProvider(search?: WebSearchConfig): (typeof SEARCH_PROVIDE
|
||||
search && "provider" in search && typeof search.provider === "string"
|
||||
? search.provider.trim().toLowerCase()
|
||||
: "";
|
||||
if (raw === "perplexity") return "perplexity";
|
||||
if (raw === "brave") return "brave";
|
||||
if (raw === "perplexity") {
|
||||
return "perplexity";
|
||||
}
|
||||
if (raw === "brave") {
|
||||
return "brave";
|
||||
}
|
||||
return "brave";
|
||||
}
|
||||
|
||||
function resolvePerplexityConfig(search?: WebSearchConfig): PerplexityConfig {
|
||||
if (!search || typeof search !== "object") return {};
|
||||
if (!search || typeof search !== "object") {
|
||||
return {};
|
||||
}
|
||||
const perplexity = "perplexity" in search ? search.perplexity : undefined;
|
||||
if (!perplexity || typeof perplexity !== "object") return {};
|
||||
if (!perplexity || typeof perplexity !== "object") {
|
||||
return {};
|
||||
}
|
||||
return perplexity as PerplexityConfig;
|
||||
}
|
||||
|
||||
@@ -182,7 +196,9 @@ function normalizeApiKey(key: unknown): string {
|
||||
}
|
||||
|
||||
function inferPerplexityBaseUrlFromApiKey(apiKey?: string): PerplexityBaseUrlHint | undefined {
|
||||
if (!apiKey) return undefined;
|
||||
if (!apiKey) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = apiKey.toLowerCase();
|
||||
if (PERPLEXITY_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
return "direct";
|
||||
@@ -202,13 +218,23 @@ function resolvePerplexityBaseUrl(
|
||||
perplexity && "baseUrl" in perplexity && typeof perplexity.baseUrl === "string"
|
||||
? perplexity.baseUrl.trim()
|
||||
: "";
|
||||
if (fromConfig) return fromConfig;
|
||||
if (apiKeySource === "perplexity_env") return PERPLEXITY_DIRECT_BASE_URL;
|
||||
if (apiKeySource === "openrouter_env") return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
if (fromConfig) {
|
||||
return fromConfig;
|
||||
}
|
||||
if (apiKeySource === "perplexity_env") {
|
||||
return PERPLEXITY_DIRECT_BASE_URL;
|
||||
}
|
||||
if (apiKeySource === "openrouter_env") {
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
}
|
||||
if (apiKeySource === "config") {
|
||||
const inferred = inferPerplexityBaseUrlFromApiKey(apiKey);
|
||||
if (inferred === "direct") return PERPLEXITY_DIRECT_BASE_URL;
|
||||
if (inferred === "openrouter") return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
if (inferred === "direct") {
|
||||
return PERPLEXITY_DIRECT_BASE_URL;
|
||||
}
|
||||
if (inferred === "openrouter") {
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
}
|
||||
}
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
}
|
||||
@@ -228,27 +254,43 @@ function resolveSearchCount(value: unknown, fallback: number): number {
|
||||
}
|
||||
|
||||
function normalizeFreshness(value: string | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (BRAVE_FRESHNESS_SHORTCUTS.has(lower)) return lower;
|
||||
if (BRAVE_FRESHNESS_SHORTCUTS.has(lower)) {
|
||||
return lower;
|
||||
}
|
||||
|
||||
const match = trimmed.match(BRAVE_FRESHNESS_RANGE);
|
||||
if (!match) return undefined;
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [, start, end] = match;
|
||||
if (!isValidIsoDate(start) || !isValidIsoDate(end)) return undefined;
|
||||
if (start > end) return undefined;
|
||||
if (!isValidIsoDate(start) || !isValidIsoDate(end)) {
|
||||
return undefined;
|
||||
}
|
||||
if (start > end) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${start}to${end}`;
|
||||
}
|
||||
|
||||
function isValidIsoDate(value: string): boolean {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
return false;
|
||||
}
|
||||
const [year, month, day] = value.split("-").map((part) => Number.parseInt(part, 10));
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return false;
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
return (
|
||||
@@ -257,7 +299,9 @@ function isValidIsoDate(value: string): boolean {
|
||||
}
|
||||
|
||||
function resolveSiteName(url: string | undefined): string | undefined {
|
||||
if (!url) return undefined;
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
@@ -326,7 +370,9 @@ async function runWebSearch(params: {
|
||||
: `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`,
|
||||
);
|
||||
const cached = readCache(SEARCH_CACHE, cacheKey);
|
||||
if (cached) return { ...cached.value, cached: true };
|
||||
if (cached) {
|
||||
return { ...cached.value, cached: true };
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
@@ -411,7 +457,9 @@ export function createWebSearchTool(options?: {
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool | null {
|
||||
const search = resolveSearchConfig(options?.config);
|
||||
if (!resolveSearchEnabled({ search, sandboxed: options?.sandboxed })) return null;
|
||||
if (!resolveSearchEnabled({ search, sandboxed: options?.sandboxed })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = resolveSearchProvider(search);
|
||||
const perplexityConfig = resolvePerplexityConfig(search);
|
||||
|
||||
@@ -28,7 +28,9 @@ export function readCache<T>(
|
||||
key: string,
|
||||
): { value: T; cached: boolean } | null {
|
||||
const entry = cache.get(key);
|
||||
if (!entry) return null;
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
cache.delete(key);
|
||||
return null;
|
||||
@@ -42,10 +44,14 @@ export function writeCache<T>(
|
||||
value: T,
|
||||
ttlMs: number,
|
||||
) {
|
||||
if (ttlMs <= 0) return;
|
||||
if (ttlMs <= 0) {
|
||||
return;
|
||||
}
|
||||
if (cache.size >= DEFAULT_CACHE_MAX_ENTRIES) {
|
||||
const oldest = cache.keys().next();
|
||||
if (!oldest.done) cache.delete(oldest.value);
|
||||
if (!oldest.done) {
|
||||
cache.delete(oldest.value);
|
||||
}
|
||||
}
|
||||
cache.set(key, {
|
||||
value,
|
||||
@@ -55,7 +61,9 @@ export function writeCache<T>(
|
||||
}
|
||||
|
||||
export function withTimeout(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
|
||||
if (timeoutMs <= 0) return signal ?? new AbortController().signal;
|
||||
if (timeoutMs <= 0) {
|
||||
return signal ?? new AbortController().signal;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
if (signal) {
|
||||
|
||||
@@ -65,9 +65,15 @@ function errorHtmlResponse(
|
||||
};
|
||||
}
|
||||
function requestUrl(input: RequestInfo): string {
|
||||
if (typeof input === "string") return input;
|
||||
if (input instanceof URL) return input.toString();
|
||||
if ("url" in input && typeof input.url === "string") return input.url;
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
if ("url" in input && typeof input.url === "string") {
|
||||
return input.url;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user