mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 21:01:43 +03:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -16,7 +16,9 @@ vi.mock("../agents/model-auth.js", () => ({
|
||||
mode: "api-key",
|
||||
})),
|
||||
requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => {
|
||||
if (auth?.apiKey) return auth.apiKey;
|
||||
if (auth?.apiKey) {
|
||||
return auth.apiKey;
|
||||
}
|
||||
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -120,7 +120,9 @@ function appendFileBlocks(body: string | undefined, blocks: string[]): string {
|
||||
}
|
||||
|
||||
function resolveUtf16Charset(buffer?: Buffer): "utf-16le" | "utf-16be" | undefined {
|
||||
if (!buffer || buffer.length < 2) return undefined;
|
||||
if (!buffer || buffer.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
const b0 = buffer[0];
|
||||
const b1 = buffer[1];
|
||||
if (b0 === 0xff && b1 === 0xfe) {
|
||||
@@ -132,7 +134,9 @@ function resolveUtf16Charset(buffer?: Buffer): "utf-16le" | "utf-16be" | undefin
|
||||
const sampleLen = Math.min(buffer.length, 2048);
|
||||
let zeroCount = 0;
|
||||
for (let i = 0; i < sampleLen; i += 1) {
|
||||
if (buffer[i] === 0) zeroCount += 1;
|
||||
if (buffer[i] === 0) {
|
||||
zeroCount += 1;
|
||||
}
|
||||
}
|
||||
if (zeroCount / sampleLen > 0.2) {
|
||||
return "utf-16le";
|
||||
@@ -141,7 +145,9 @@ function resolveUtf16Charset(buffer?: Buffer): "utf-16le" | "utf-16be" | undefin
|
||||
}
|
||||
|
||||
function looksLikeUtf8Text(buffer?: Buffer): boolean {
|
||||
if (!buffer || buffer.length === 0) return false;
|
||||
if (!buffer || buffer.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const sampleLen = Math.min(buffer.length, 4096);
|
||||
let printable = 0;
|
||||
let other = 0;
|
||||
@@ -158,12 +164,16 @@ function looksLikeUtf8Text(buffer?: Buffer): boolean {
|
||||
}
|
||||
}
|
||||
const total = printable + other;
|
||||
if (total === 0) return false;
|
||||
if (total === 0) {
|
||||
return false;
|
||||
}
|
||||
return printable / total > 0.85;
|
||||
}
|
||||
|
||||
function decodeTextSample(buffer?: Buffer): string {
|
||||
if (!buffer || buffer.length === 0) return "";
|
||||
if (!buffer || buffer.length === 0) {
|
||||
return "";
|
||||
}
|
||||
const sample = buffer.subarray(0, Math.min(buffer.length, 8192));
|
||||
const utf16Charset = resolveUtf16Charset(sample);
|
||||
if (utf16Charset === "utf-16be") {
|
||||
@@ -181,7 +191,9 @@ function decodeTextSample(buffer?: Buffer): string {
|
||||
}
|
||||
|
||||
function guessDelimitedMime(text: string): string | undefined {
|
||||
if (!text) return undefined;
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
const line = text.split(/\r?\n/)[0] ?? "";
|
||||
const tabs = (line.match(/\t/g) ?? []).length;
|
||||
const commas = (line.match(/,/g) ?? []).length;
|
||||
@@ -195,7 +207,9 @@ function guessDelimitedMime(text: string): string | undefined {
|
||||
}
|
||||
|
||||
function resolveTextMimeFromName(name?: string): string | undefined {
|
||||
if (!name) return undefined;
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
const ext = path.extname(name).toLowerCase();
|
||||
return TEXT_EXT_MIME.get(ext);
|
||||
}
|
||||
@@ -363,7 +377,9 @@ export async function applyMediaUnderstanding(params: {
|
||||
const outputs: MediaUnderstandingOutput[] = [];
|
||||
const decisions: MediaUnderstandingDecision[] = [];
|
||||
for (const entry of results) {
|
||||
if (!entry) continue;
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
for (const output of entry.outputs) {
|
||||
outputs.push(output);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ const DEFAULT_MAX_ATTACHMENTS = 1;
|
||||
|
||||
function normalizeAttachmentPath(raw?: string | null): string | undefined {
|
||||
const value = raw?.trim();
|
||||
if (!value) return undefined;
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
if (value.startsWith("file://")) {
|
||||
try {
|
||||
return fileURLToPath(value);
|
||||
@@ -58,7 +60,9 @@ export function normalizeAttachments(ctx: MsgContext): MediaAttachment[] {
|
||||
const resolveMime = (count: number, index: number) => {
|
||||
const typeHint = typesFromArray?.[index];
|
||||
const trimmed = typeof typeHint === "string" ? typeHint.trim() : "";
|
||||
if (trimmed) return trimmed;
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
return count === 1 ? ctx.MediaType : undefined;
|
||||
};
|
||||
|
||||
@@ -89,7 +93,9 @@ export function normalizeAttachments(ctx: MsgContext): MediaAttachment[] {
|
||||
|
||||
const pathValue = ctx.MediaPath?.trim();
|
||||
const url = ctx.MediaUrl?.trim();
|
||||
if (!pathValue && !url) return [];
|
||||
if (!pathValue && !url) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
path: pathValue || undefined,
|
||||
@@ -104,12 +110,20 @@ export function resolveAttachmentKind(
|
||||
attachment: MediaAttachment,
|
||||
): "image" | "audio" | "video" | "document" | "unknown" {
|
||||
const kind = kindFromMime(attachment.mime);
|
||||
if (kind === "image" || kind === "audio" || kind === "video") return kind;
|
||||
if (kind === "image" || kind === "audio" || kind === "video") {
|
||||
return kind;
|
||||
}
|
||||
|
||||
const ext = getFileExtension(attachment.path ?? attachment.url);
|
||||
if (!ext) return "unknown";
|
||||
if ([".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"].includes(ext)) return "video";
|
||||
if (isAudioFileName(attachment.path ?? attachment.url)) return "audio";
|
||||
if (!ext) {
|
||||
return "unknown";
|
||||
}
|
||||
if ([".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"].includes(ext)) {
|
||||
return "video";
|
||||
}
|
||||
if (isAudioFileName(attachment.path ?? attachment.url)) {
|
||||
return "audio";
|
||||
}
|
||||
if ([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff", ".tif"].includes(ext)) {
|
||||
return "image";
|
||||
}
|
||||
@@ -129,14 +143,22 @@ export function isImageAttachment(attachment: MediaAttachment): boolean {
|
||||
}
|
||||
|
||||
function isAbortError(err: unknown): boolean {
|
||||
if (!err) return false;
|
||||
if (err instanceof Error && err.name === "AbortError") return true;
|
||||
if (!err) {
|
||||
return false;
|
||||
}
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") return input;
|
||||
if (input instanceof URL) return input.toString();
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
return input.url;
|
||||
}
|
||||
|
||||
@@ -144,8 +166,12 @@ function orderAttachments(
|
||||
attachments: MediaAttachment[],
|
||||
prefer?: MediaUnderstandingAttachmentsConfig["prefer"],
|
||||
): MediaAttachment[] {
|
||||
if (!prefer || prefer === "first") return attachments;
|
||||
if (prefer === "last") return [...attachments].toReversed();
|
||||
if (!prefer || prefer === "first") {
|
||||
return attachments;
|
||||
}
|
||||
if (prefer === "last") {
|
||||
return [...attachments].toReversed();
|
||||
}
|
||||
if (prefer === "path") {
|
||||
const withPath = attachments.filter((item) => item.path);
|
||||
const withoutPath = attachments.filter((item) => !item.path);
|
||||
@@ -166,11 +192,17 @@ export function selectAttachments(params: {
|
||||
}): MediaAttachment[] {
|
||||
const { capability, attachments, policy } = params;
|
||||
const matches = attachments.filter((item) => {
|
||||
if (capability === "image") return isImageAttachment(item);
|
||||
if (capability === "audio") return isAudioAttachment(item);
|
||||
if (capability === "image") {
|
||||
return isImageAttachment(item);
|
||||
}
|
||||
if (capability === "audio") {
|
||||
return isAudioAttachment(item);
|
||||
}
|
||||
return isVideoAttachment(item);
|
||||
});
|
||||
if (matches.length === 0) return [];
|
||||
if (matches.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ordered = orderAttachments(matches, policy?.prefer);
|
||||
const mode = policy?.mode ?? "first";
|
||||
@@ -367,13 +399,19 @@ export class MediaAttachmentCache {
|
||||
|
||||
private resolveLocalPath(attachment: MediaAttachment): string | undefined {
|
||||
const rawPath = normalizeAttachmentPath(attachment.path);
|
||||
if (!rawPath) return undefined;
|
||||
if (!rawPath) {
|
||||
return undefined;
|
||||
}
|
||||
return path.isAbsolute(rawPath) ? rawPath : path.resolve(rawPath);
|
||||
}
|
||||
|
||||
private async ensureLocalStat(entry: AttachmentCacheEntry): Promise<number | undefined> {
|
||||
if (!entry.resolvedPath) return undefined;
|
||||
if (entry.statSize !== undefined) return entry.statSize;
|
||||
if (!entry.resolvedPath) {
|
||||
return undefined;
|
||||
}
|
||||
if (entry.statSize !== undefined) {
|
||||
return entry.statSize;
|
||||
}
|
||||
try {
|
||||
const stat = await fs.stat(entry.resolvedPath);
|
||||
if (!stat.isFile()) {
|
||||
|
||||
@@ -4,7 +4,9 @@ export async function runWithConcurrency<T>(
|
||||
tasks: Array<() => Promise<T>>,
|
||||
limit: number,
|
||||
): Promise<T[]> {
|
||||
if (tasks.length === 0) return [];
|
||||
if (tasks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const resolvedLimit = Math.max(1, Math.min(limit, tasks.length));
|
||||
const results: T[] = Array.from({ length: tasks.length });
|
||||
let next = 0;
|
||||
@@ -13,7 +15,9 @@ export async function runWithConcurrency<T>(
|
||||
while (true) {
|
||||
const index = next;
|
||||
next += 1;
|
||||
if (index >= tasks.length) return;
|
||||
if (index >= tasks.length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
results[index] = await tasks[index]();
|
||||
} catch (err) {
|
||||
|
||||
@@ -5,8 +5,12 @@ const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
|
||||
|
||||
export function extractMediaUserText(body?: string): string | undefined {
|
||||
const trimmed = body?.trim() ?? "";
|
||||
if (!trimmed) return undefined;
|
||||
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
const cleaned = trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim();
|
||||
return cleaned || undefined;
|
||||
}
|
||||
@@ -87,6 +91,8 @@ export function formatMediaUnderstandingBody(params: {
|
||||
}
|
||||
|
||||
export function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string {
|
||||
if (outputs.length === 1) return outputs[0].text;
|
||||
if (outputs.length === 1) {
|
||||
return outputs[0].text;
|
||||
}
|
||||
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ import { describe, expect, it } from "vitest";
|
||||
import { transcribeDeepgramAudio } from "./audio.js";
|
||||
|
||||
const resolveRequestUrl = (input: RequestInfo | URL) => {
|
||||
if (typeof input === "string") return input;
|
||||
if (input instanceof URL) return input.toString();
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
return input.url;
|
||||
};
|
||||
|
||||
|
||||
@@ -28,10 +28,14 @@ export async function transcribeDeepgramAudio(
|
||||
|
||||
const url = new URL(`${baseUrl}/listen`);
|
||||
url.searchParams.set("model", model);
|
||||
if (params.language?.trim()) url.searchParams.set("language", params.language.trim());
|
||||
if (params.language?.trim()) {
|
||||
url.searchParams.set("language", params.language.trim());
|
||||
}
|
||||
if (params.query) {
|
||||
for (const [key, value] of Object.entries(params.query)) {
|
||||
if (value === undefined) continue;
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ const DEFAULT_GOOGLE_AUDIO_PROMPT = "Transcribe the audio.";
|
||||
|
||||
function resolveModel(model?: string): string {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) return DEFAULT_GOOGLE_AUDIO_MODEL;
|
||||
if (!trimmed) {
|
||||
return DEFAULT_GOOGLE_AUDIO_MODEL;
|
||||
}
|
||||
return normalizeGoogleModelId(trimmed);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,12 @@ import { describe, expect, it } from "vitest";
|
||||
import { describeGeminiVideo } from "./video.js";
|
||||
|
||||
const resolveRequestUrl = (input: RequestInfo | URL) => {
|
||||
if (typeof input === "string") return input;
|
||||
if (input instanceof URL) return input.toString();
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
return input.url;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ const DEFAULT_GOOGLE_VIDEO_PROMPT = "Describe the video.";
|
||||
|
||||
function resolveModel(model?: string): string {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) return DEFAULT_GOOGLE_VIDEO_MODEL;
|
||||
if (!trimmed) {
|
||||
return DEFAULT_GOOGLE_VIDEO_MODEL;
|
||||
}
|
||||
return normalizeGoogleModelId(trimmed);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ const PROVIDERS: MediaUnderstandingProvider[] = [
|
||||
|
||||
export function normalizeMediaProviderId(id: string): string {
|
||||
const normalized = normalizeProviderId(id);
|
||||
if (normalized === "gemini") return "google";
|
||||
if (normalized === "gemini") {
|
||||
return "google";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,12 @@ import { describe, expect, it } from "vitest";
|
||||
import { transcribeOpenAiCompatibleAudio } from "./audio.js";
|
||||
|
||||
const resolveRequestUrl = (input: RequestInfo | URL) => {
|
||||
if (typeof input === "string") return input;
|
||||
if (input instanceof URL) return input.toString();
|
||||
if (typeof input === "string") {
|
||||
return input;
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
}
|
||||
return input.url;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,8 +27,12 @@ export async function transcribeOpenAiCompatibleAudio(
|
||||
});
|
||||
form.append("file", blob, fileName);
|
||||
form.append("model", model);
|
||||
if (params.language?.trim()) form.append("language", params.language.trim());
|
||||
if (params.prompt?.trim()) form.append("prompt", params.prompt.trim());
|
||||
if (params.language?.trim()) {
|
||||
form.append("language", params.language.trim());
|
||||
}
|
||||
if (params.prompt?.trim()) {
|
||||
form.append("prompt", params.prompt.trim());
|
||||
}
|
||||
|
||||
const headers = new Headers(params.headers);
|
||||
if (!headers.has("authorization")) {
|
||||
|
||||
@@ -24,8 +24,12 @@ export async function readErrorResponse(res: Response): Promise<string | undefin
|
||||
try {
|
||||
const text = await res.text();
|
||||
const collapsed = text.replace(/\s+/g, " ").trim();
|
||||
if (!collapsed) return undefined;
|
||||
if (collapsed.length <= MAX_ERROR_CHARS) return collapsed;
|
||||
if (!collapsed) {
|
||||
return undefined;
|
||||
}
|
||||
if (collapsed.length <= MAX_ERROR_CHARS) {
|
||||
return collapsed;
|
||||
}
|
||||
return `${collapsed.slice(0, MAX_ERROR_CHARS)}…`;
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
@@ -27,7 +27,9 @@ export function resolvePrompt(
|
||||
maxChars?: number,
|
||||
): string {
|
||||
const base = prompt?.trim() || DEFAULT_PROMPT[capability];
|
||||
if (!maxChars || capability === "audio") return base;
|
||||
if (!maxChars || capability === "audio") {
|
||||
return base;
|
||||
}
|
||||
return `${base} Respond in at most ${maxChars} characters.`;
|
||||
}
|
||||
|
||||
@@ -40,7 +42,9 @@ export function resolveMaxChars(params: {
|
||||
const { capability, entry, cfg } = params;
|
||||
const configured =
|
||||
entry.maxChars ?? params.config?.maxChars ?? cfg.tools?.media?.[capability]?.maxChars;
|
||||
if (typeof configured === "number") return configured;
|
||||
if (typeof configured === "number") {
|
||||
return configured;
|
||||
}
|
||||
return DEFAULT_MAX_CHARS_BY_CAPABILITY[capability];
|
||||
}
|
||||
|
||||
@@ -54,7 +58,9 @@ export function resolveMaxBytes(params: {
|
||||
params.entry.maxBytes ??
|
||||
params.config?.maxBytes ??
|
||||
params.cfg.tools?.media?.[params.capability]?.maxBytes;
|
||||
if (typeof configured === "number") return configured;
|
||||
if (typeof configured === "number") {
|
||||
return configured;
|
||||
}
|
||||
return DEFAULT_MAX_BYTES[params.capability];
|
||||
}
|
||||
|
||||
@@ -82,9 +88,13 @@ function resolveEntryCapabilities(params: {
|
||||
providerRegistry: Map<string, { capabilities?: MediaUnderstandingCapability[] }>;
|
||||
}): MediaUnderstandingCapability[] | undefined {
|
||||
const entryType = params.entry.type ?? (params.entry.command ? "cli" : "provider");
|
||||
if (entryType === "cli") return undefined;
|
||||
if (entryType === "cli") {
|
||||
return undefined;
|
||||
}
|
||||
const providerId = normalizeMediaProviderId(params.entry.provider ?? "");
|
||||
if (!providerId) return undefined;
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
return params.providerRegistry.get(providerId)?.capabilities;
|
||||
}
|
||||
|
||||
@@ -100,7 +110,9 @@ export function resolveModelEntries(params: {
|
||||
...(config?.models ?? []).map((entry) => ({ entry, source: "capability" as const })),
|
||||
...sharedModels.map((entry) => ({ entry, source: "shared" as const })),
|
||||
];
|
||||
if (entries.length === 0) return [];
|
||||
if (entries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return entries
|
||||
.filter(({ entry, source }) => {
|
||||
@@ -147,14 +159,24 @@ export function resolveEntriesWithActiveFallback(params: {
|
||||
config: params.config,
|
||||
providerRegistry: params.providerRegistry,
|
||||
});
|
||||
if (entries.length > 0) return entries;
|
||||
if (params.config?.enabled !== true) return entries;
|
||||
if (entries.length > 0) {
|
||||
return entries;
|
||||
}
|
||||
if (params.config?.enabled !== true) {
|
||||
return entries;
|
||||
}
|
||||
const activeProviderRaw = params.activeModel?.provider?.trim();
|
||||
if (!activeProviderRaw) return entries;
|
||||
if (!activeProviderRaw) {
|
||||
return entries;
|
||||
}
|
||||
const activeProvider = normalizeMediaProviderId(activeProviderRaw);
|
||||
if (!activeProvider) return entries;
|
||||
if (!activeProvider) {
|
||||
return entries;
|
||||
}
|
||||
const capabilities = params.providerRegistry.get(activeProvider)?.capabilities;
|
||||
if (!capabilities || !capabilities.includes(params.capability)) return entries;
|
||||
if (!capabilities || !capabilities.includes(params.capability)) {
|
||||
return entries;
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: "provider",
|
||||
|
||||
@@ -89,10 +89,16 @@ const binaryCache = new Map<string, Promise<string | null>>();
|
||||
const geminiProbeCache = new Map<string, Promise<boolean>>();
|
||||
|
||||
function expandHomeDir(value: string): string {
|
||||
if (!value.startsWith("~")) return value;
|
||||
if (!value.startsWith("~")) {
|
||||
return value;
|
||||
}
|
||||
const home = os.homedir();
|
||||
if (value === "~") return home;
|
||||
if (value.startsWith("~/")) return path.join(home, value.slice(2));
|
||||
if (value === "~") {
|
||||
return home;
|
||||
}
|
||||
if (value.startsWith("~/")) {
|
||||
return path.join(home, value.slice(2));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -101,9 +107,13 @@ function hasPathSeparator(value: string): boolean {
|
||||
}
|
||||
|
||||
function candidateBinaryNames(name: string): string[] {
|
||||
if (process.platform !== "win32") return [name];
|
||||
if (process.platform !== "win32") {
|
||||
return [name];
|
||||
}
|
||||
const ext = path.extname(name);
|
||||
if (ext) return [name];
|
||||
if (ext) {
|
||||
return [name];
|
||||
}
|
||||
const pathext = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM")
|
||||
.split(";")
|
||||
.map((item) => item.trim())
|
||||
@@ -116,8 +126,12 @@ function candidateBinaryNames(name: string): string[] {
|
||||
async function isExecutable(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (!stat.isFile()) return false;
|
||||
if (process.platform === "win32") return true;
|
||||
if (!stat.isFile()) {
|
||||
return false;
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
return true;
|
||||
}
|
||||
await fs.access(filePath, fsConstants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
@@ -127,25 +141,35 @@ async function isExecutable(filePath: string): Promise<boolean> {
|
||||
|
||||
async function findBinary(name: string): Promise<string | null> {
|
||||
const cached = binaryCache.get(name);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const resolved = (async () => {
|
||||
const direct = expandHomeDir(name.trim());
|
||||
if (direct && hasPathSeparator(direct)) {
|
||||
for (const candidate of candidateBinaryNames(direct)) {
|
||||
if (await isExecutable(candidate)) return candidate;
|
||||
if (await isExecutable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const searchName = name.trim();
|
||||
if (!searchName) return null;
|
||||
if (!searchName) {
|
||||
return null;
|
||||
}
|
||||
const pathEntries = (process.env.PATH ?? "").split(path.delimiter);
|
||||
const candidates = candidateBinaryNames(searchName);
|
||||
for (const entryRaw of pathEntries) {
|
||||
const entry = expandHomeDir(entryRaw.trim().replace(/^"(.*)"$/, "$1"));
|
||||
if (!entry) continue;
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
const fullPath = path.join(entry, candidate);
|
||||
if (await isExecutable(fullPath)) return fullPath;
|
||||
if (await isExecutable(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +184,9 @@ async function hasBinary(name: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
async function fileExists(filePath?: string | null): Promise<boolean> {
|
||||
if (!filePath) return false;
|
||||
if (!filePath) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await fs.stat(filePath);
|
||||
return true;
|
||||
@@ -172,7 +198,9 @@ async function fileExists(filePath?: string | null): Promise<boolean> {
|
||||
function extractLastJsonObject(raw: string): unknown {
|
||||
const trimmed = raw.trim();
|
||||
const start = trimmed.lastIndexOf("{");
|
||||
if (start === -1) return null;
|
||||
if (start === -1) {
|
||||
return null;
|
||||
}
|
||||
const slice = trimmed.slice(start);
|
||||
try {
|
||||
return JSON.parse(slice);
|
||||
@@ -183,9 +211,13 @@ function extractLastJsonObject(raw: string): unknown {
|
||||
|
||||
function extractGeminiResponse(raw: string): string | null {
|
||||
const payload = extractLastJsonObject(raw);
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return null;
|
||||
}
|
||||
const response = (payload as { response?: unknown }).response;
|
||||
if (typeof response !== "string") return null;
|
||||
if (typeof response !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = response.trim();
|
||||
return trimmed || null;
|
||||
}
|
||||
@@ -193,9 +225,13 @@ function extractGeminiResponse(raw: string): string | null {
|
||||
function extractSherpaOnnxText(raw: string): string | null {
|
||||
const tryParse = (value: string): string | null => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const head = trimmed[0];
|
||||
if (head !== "{" && head !== '"') return null;
|
||||
if (head !== "{" && head !== '"') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
if (typeof parsed === "string") {
|
||||
@@ -212,7 +248,9 @@ function extractSherpaOnnxText(raw: string): string | null {
|
||||
};
|
||||
|
||||
const direct = tryParse(raw);
|
||||
if (direct) return direct;
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const lines = raw
|
||||
.split("\n")
|
||||
@@ -220,16 +258,22 @@ function extractSherpaOnnxText(raw: string): string | null {
|
||||
.filter(Boolean);
|
||||
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
||||
const parsed = tryParse(lines[i] ?? "");
|
||||
if (parsed) return parsed;
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function probeGeminiCli(): Promise<boolean> {
|
||||
const cached = geminiProbeCache.get("gemini");
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const resolved = (async () => {
|
||||
if (!(await hasBinary("gemini"))) return false;
|
||||
if (!(await hasBinary("gemini"))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const { stdout } = await runExec("gemini", ["--output-format", "json", "ok"], {
|
||||
timeoutMs: 8000,
|
||||
@@ -244,11 +288,15 @@ async function probeGeminiCli(): Promise<boolean> {
|
||||
}
|
||||
|
||||
async function resolveLocalWhisperCppEntry(): Promise<MediaUnderstandingModelConfig | null> {
|
||||
if (!(await hasBinary("whisper-cli"))) return null;
|
||||
if (!(await hasBinary("whisper-cli"))) {
|
||||
return null;
|
||||
}
|
||||
const envModel = process.env.WHISPER_CPP_MODEL?.trim();
|
||||
const defaultModel = "/opt/homebrew/share/whisper-cpp/for-tests-ggml-tiny.bin";
|
||||
const modelPath = envModel && (await fileExists(envModel)) ? envModel : defaultModel;
|
||||
if (!(await fileExists(modelPath))) return null;
|
||||
if (!(await fileExists(modelPath))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: "cli",
|
||||
command: "whisper-cli",
|
||||
@@ -257,7 +305,9 @@ async function resolveLocalWhisperCppEntry(): Promise<MediaUnderstandingModelCon
|
||||
}
|
||||
|
||||
async function resolveLocalWhisperEntry(): Promise<MediaUnderstandingModelConfig | null> {
|
||||
if (!(await hasBinary("whisper"))) return null;
|
||||
if (!(await hasBinary("whisper"))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: "cli",
|
||||
command: "whisper",
|
||||
@@ -276,17 +326,29 @@ async function resolveLocalWhisperEntry(): Promise<MediaUnderstandingModelConfig
|
||||
}
|
||||
|
||||
async function resolveSherpaOnnxEntry(): Promise<MediaUnderstandingModelConfig | null> {
|
||||
if (!(await hasBinary("sherpa-onnx-offline"))) return null;
|
||||
if (!(await hasBinary("sherpa-onnx-offline"))) {
|
||||
return null;
|
||||
}
|
||||
const modelDir = process.env.SHERPA_ONNX_MODEL_DIR?.trim();
|
||||
if (!modelDir) return null;
|
||||
if (!modelDir) {
|
||||
return null;
|
||||
}
|
||||
const tokens = path.join(modelDir, "tokens.txt");
|
||||
const encoder = path.join(modelDir, "encoder.onnx");
|
||||
const decoder = path.join(modelDir, "decoder.onnx");
|
||||
const joiner = path.join(modelDir, "joiner.onnx");
|
||||
if (!(await fileExists(tokens))) return null;
|
||||
if (!(await fileExists(encoder))) return null;
|
||||
if (!(await fileExists(decoder))) return null;
|
||||
if (!(await fileExists(joiner))) return null;
|
||||
if (!(await fileExists(tokens))) {
|
||||
return null;
|
||||
}
|
||||
if (!(await fileExists(encoder))) {
|
||||
return null;
|
||||
}
|
||||
if (!(await fileExists(decoder))) {
|
||||
return null;
|
||||
}
|
||||
if (!(await fileExists(joiner))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: "cli",
|
||||
command: "sherpa-onnx-offline",
|
||||
@@ -302,16 +364,22 @@ async function resolveSherpaOnnxEntry(): Promise<MediaUnderstandingModelConfig |
|
||||
|
||||
async function resolveLocalAudioEntry(): Promise<MediaUnderstandingModelConfig | null> {
|
||||
const sherpa = await resolveSherpaOnnxEntry();
|
||||
if (sherpa) return sherpa;
|
||||
if (sherpa) {
|
||||
return sherpa;
|
||||
}
|
||||
const whisperCpp = await resolveLocalWhisperCppEntry();
|
||||
if (whisperCpp) return whisperCpp;
|
||||
if (whisperCpp) {
|
||||
return whisperCpp;
|
||||
}
|
||||
return await resolveLocalWhisperEntry();
|
||||
}
|
||||
|
||||
async function resolveGeminiCliEntry(
|
||||
_capability: MediaUnderstandingCapability,
|
||||
): Promise<MediaUnderstandingModelConfig | null> {
|
||||
if (!(await probeGeminiCli())) return null;
|
||||
if (!(await probeGeminiCli())) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: "cli",
|
||||
command: "gemini",
|
||||
@@ -341,10 +409,18 @@ async function resolveKeyEntry(params: {
|
||||
model?: string,
|
||||
): Promise<MediaUnderstandingModelConfig | null> => {
|
||||
const provider = getMediaUnderstandingProvider(providerId, providerRegistry);
|
||||
if (!provider) return null;
|
||||
if (capability === "audio" && !provider.transcribeAudio) return null;
|
||||
if (capability === "image" && !provider.describeImage) return null;
|
||||
if (capability === "video" && !provider.describeVideo) return null;
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
if (capability === "audio" && !provider.transcribeAudio) {
|
||||
return null;
|
||||
}
|
||||
if (capability === "image" && !provider.describeImage) {
|
||||
return null;
|
||||
}
|
||||
if (capability === "video" && !provider.describeVideo) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await resolveApiKeyForProvider({ provider: providerId, cfg, agentDir });
|
||||
return { type: "provider" as const, provider: providerId, model };
|
||||
@@ -357,12 +433,16 @@ async function resolveKeyEntry(params: {
|
||||
const activeProvider = params.activeModel?.provider?.trim();
|
||||
if (activeProvider) {
|
||||
const activeEntry = await checkProvider(activeProvider, params.activeModel?.model);
|
||||
if (activeEntry) return activeEntry;
|
||||
if (activeEntry) {
|
||||
return activeEntry;
|
||||
}
|
||||
}
|
||||
for (const providerId of AUTO_IMAGE_KEY_PROVIDERS) {
|
||||
const model = DEFAULT_IMAGE_MODELS[providerId];
|
||||
const entry = await checkProvider(providerId, model);
|
||||
if (entry) return entry;
|
||||
if (entry) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -371,11 +451,15 @@ async function resolveKeyEntry(params: {
|
||||
const activeProvider = params.activeModel?.provider?.trim();
|
||||
if (activeProvider) {
|
||||
const activeEntry = await checkProvider(activeProvider, params.activeModel?.model);
|
||||
if (activeEntry) return activeEntry;
|
||||
if (activeEntry) {
|
||||
return activeEntry;
|
||||
}
|
||||
}
|
||||
for (const providerId of AUTO_VIDEO_KEY_PROVIDERS) {
|
||||
const entry = await checkProvider(providerId, undefined);
|
||||
if (entry) return entry;
|
||||
if (entry) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -383,11 +467,15 @@ async function resolveKeyEntry(params: {
|
||||
const activeProvider = params.activeModel?.provider?.trim();
|
||||
if (activeProvider) {
|
||||
const activeEntry = await checkProvider(activeProvider, params.activeModel?.model);
|
||||
if (activeEntry) return activeEntry;
|
||||
if (activeEntry) {
|
||||
return activeEntry;
|
||||
}
|
||||
}
|
||||
for (const providerId of AUTO_AUDIO_KEY_PROVIDERS) {
|
||||
const entry = await checkProvider(providerId, undefined);
|
||||
if (entry) return entry;
|
||||
if (entry) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -400,15 +488,23 @@ async function resolveAutoEntries(params: {
|
||||
activeModel?: ActiveMediaModel;
|
||||
}): Promise<MediaUnderstandingModelConfig[]> {
|
||||
const activeEntry = await resolveActiveModelEntry(params);
|
||||
if (activeEntry) return [activeEntry];
|
||||
if (activeEntry) {
|
||||
return [activeEntry];
|
||||
}
|
||||
if (params.capability === "audio") {
|
||||
const localAudio = await resolveLocalAudioEntry();
|
||||
if (localAudio) return [localAudio];
|
||||
if (localAudio) {
|
||||
return [localAudio];
|
||||
}
|
||||
}
|
||||
const gemini = await resolveGeminiCliEntry(params.capability);
|
||||
if (gemini) return [gemini];
|
||||
if (gemini) {
|
||||
return [gemini];
|
||||
}
|
||||
const keys = await resolveKeyEntry(params);
|
||||
if (keys) return [keys];
|
||||
if (keys) {
|
||||
return [keys];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -419,11 +515,17 @@ export async function resolveAutoImageModel(params: {
|
||||
}): Promise<ActiveMediaModel | null> {
|
||||
const providerRegistry = buildProviderRegistry();
|
||||
const toActive = (entry: MediaUnderstandingModelConfig | null): ActiveMediaModel | null => {
|
||||
if (!entry || entry.type === "cli") return null;
|
||||
if (!entry || entry.type === "cli") {
|
||||
return null;
|
||||
}
|
||||
const provider = entry.provider;
|
||||
if (!provider) return null;
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
const model = entry.model ?? DEFAULT_IMAGE_MODELS[provider];
|
||||
if (!model) return null;
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
return { provider, model };
|
||||
};
|
||||
const activeEntry = await resolveActiveModelEntry({
|
||||
@@ -434,7 +536,9 @@ export async function resolveAutoImageModel(params: {
|
||||
activeModel: params.activeModel,
|
||||
});
|
||||
const resolvedActive = toActive(activeEntry);
|
||||
if (resolvedActive) return resolvedActive;
|
||||
if (resolvedActive) {
|
||||
return resolvedActive;
|
||||
}
|
||||
const keyEntry = await resolveKeyEntry({
|
||||
cfg: params.cfg,
|
||||
agentDir: params.agentDir,
|
||||
@@ -453,14 +557,26 @@ async function resolveActiveModelEntry(params: {
|
||||
activeModel?: ActiveMediaModel;
|
||||
}): Promise<MediaUnderstandingModelConfig | null> {
|
||||
const activeProviderRaw = params.activeModel?.provider?.trim();
|
||||
if (!activeProviderRaw) return null;
|
||||
if (!activeProviderRaw) {
|
||||
return null;
|
||||
}
|
||||
const providerId = normalizeMediaProviderId(activeProviderRaw);
|
||||
if (!providerId) return null;
|
||||
if (!providerId) {
|
||||
return null;
|
||||
}
|
||||
const provider = getMediaUnderstandingProvider(providerId, params.providerRegistry);
|
||||
if (!provider) return null;
|
||||
if (params.capability === "audio" && !provider.transcribeAudio) return null;
|
||||
if (params.capability === "image" && !provider.describeImage) return null;
|
||||
if (params.capability === "video" && !provider.describeVideo) return null;
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
if (params.capability === "audio" && !provider.transcribeAudio) {
|
||||
return null;
|
||||
}
|
||||
if (params.capability === "image" && !provider.describeImage) {
|
||||
return null;
|
||||
}
|
||||
if (params.capability === "video" && !provider.describeVideo) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await resolveApiKeyForProvider({
|
||||
provider: providerId,
|
||||
@@ -479,7 +595,9 @@ async function resolveActiveModelEntry(params: {
|
||||
|
||||
function trimOutput(text: string, maxChars?: number): string {
|
||||
const trimmed = text.trim();
|
||||
if (!maxChars || trimmed.length <= maxChars) return trimmed;
|
||||
if (!maxChars || trimmed.length <= maxChars) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.slice(0, maxChars).trim();
|
||||
}
|
||||
|
||||
@@ -491,7 +609,9 @@ function findArgValue(args: string[], keys: string[]): string | undefined {
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (keys.includes(args[i] ?? "")) {
|
||||
const value = args[i + 1];
|
||||
if (value) return value;
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -504,17 +624,25 @@ function hasArg(args: string[], keys: string[]): boolean {
|
||||
function resolveWhisperOutputPath(args: string[], mediaPath: string): string | null {
|
||||
const outputDir = findArgValue(args, ["--output_dir", "-o"]);
|
||||
const outputFormat = findArgValue(args, ["--output_format"]);
|
||||
if (!outputDir || !outputFormat) return null;
|
||||
if (!outputDir || !outputFormat) {
|
||||
return null;
|
||||
}
|
||||
const formats = outputFormat.split(",").map((value) => value.trim());
|
||||
if (!formats.includes("txt")) return null;
|
||||
if (!formats.includes("txt")) {
|
||||
return null;
|
||||
}
|
||||
const base = path.parse(mediaPath).name;
|
||||
return path.join(outputDir, `${base}.txt`);
|
||||
}
|
||||
|
||||
function resolveWhisperCppOutputPath(args: string[]): string | null {
|
||||
if (!hasArg(args, ["-otxt", "--output-txt"])) return null;
|
||||
if (!hasArg(args, ["-otxt", "--output-txt"])) {
|
||||
return null;
|
||||
}
|
||||
const outputBase = findArgValue(args, ["-of", "--output-file"]);
|
||||
if (!outputBase) return null;
|
||||
if (!outputBase) {
|
||||
return null;
|
||||
}
|
||||
return `${outputBase}.txt`;
|
||||
}
|
||||
|
||||
@@ -534,18 +662,24 @@ async function resolveCliOutput(params: {
|
||||
if (fileOutput && (await fileExists(fileOutput))) {
|
||||
try {
|
||||
const content = await fs.readFile(fileOutput, "utf8");
|
||||
if (content.trim()) return content.trim();
|
||||
if (content.trim()) {
|
||||
return content.trim();
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (commandId === "gemini") {
|
||||
const response = extractGeminiResponse(params.stdout);
|
||||
if (response) return response;
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
if (commandId === "sherpa-onnx-offline") {
|
||||
const response = extractSherpaOnnxText(params.stdout);
|
||||
if (response) return response;
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
return params.stdout.trim();
|
||||
@@ -556,10 +690,14 @@ type ProviderQuery = Record<string, string | number | boolean>;
|
||||
function normalizeProviderQuery(
|
||||
options?: Record<string, string | number | boolean>,
|
||||
): ProviderQuery | undefined {
|
||||
if (!options) return undefined;
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
const query: ProviderQuery = {};
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (value === undefined) continue;
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
query[key] = value;
|
||||
}
|
||||
return Object.keys(query).length > 0 ? query : undefined;
|
||||
@@ -570,11 +708,19 @@ function buildDeepgramCompatQuery(options?: {
|
||||
punctuate?: boolean;
|
||||
smartFormat?: boolean;
|
||||
}): ProviderQuery | undefined {
|
||||
if (!options) return undefined;
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
const query: ProviderQuery = {};
|
||||
if (typeof options.detectLanguage === "boolean") query.detect_language = options.detectLanguage;
|
||||
if (typeof options.punctuate === "boolean") query.punctuate = options.punctuate;
|
||||
if (typeof options.smartFormat === "boolean") query.smart_format = options.smartFormat;
|
||||
if (typeof options.detectLanguage === "boolean") {
|
||||
query.detect_language = options.detectLanguage;
|
||||
}
|
||||
if (typeof options.punctuate === "boolean") {
|
||||
query.punctuate = options.punctuate;
|
||||
}
|
||||
if (typeof options.smartFormat === "boolean") {
|
||||
query.smart_format = options.smartFormat;
|
||||
}
|
||||
return Object.keys(query).length > 0 ? query : undefined;
|
||||
}
|
||||
|
||||
@@ -908,7 +1054,9 @@ async function runCliEntry(params: {
|
||||
mediaPath,
|
||||
});
|
||||
const text = trimOutput(resolved, maxChars);
|
||||
if (!text) return null;
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
kind: capability === "audio" ? "audio.transcription" : `${capability}.description`,
|
||||
attachmentIndex: params.attachmentIndex,
|
||||
@@ -964,8 +1112,12 @@ async function runAttachmentEntries(params: {
|
||||
});
|
||||
if (result) {
|
||||
const decision = buildModelDecision({ entry, entryType, outcome: "success" });
|
||||
if (result.provider) decision.provider = result.provider;
|
||||
if (result.model) decision.model = result.model;
|
||||
if (result.provider) {
|
||||
decision.provider = result.provider;
|
||||
}
|
||||
if (result.model) {
|
||||
decision.model = result.model;
|
||||
}
|
||||
attempts.push(decision);
|
||||
return { output: result, attempts };
|
||||
}
|
||||
@@ -1129,7 +1281,9 @@ export async function runCapability(params: {
|
||||
entries: resolvedEntries,
|
||||
config,
|
||||
});
|
||||
if (output) outputs.push(output);
|
||||
if (output) {
|
||||
outputs.push(output);
|
||||
}
|
||||
attachmentDecisions.push({
|
||||
attachmentIndex: attachment.index,
|
||||
attempts,
|
||||
|
||||
@@ -5,8 +5,12 @@ export type MediaUnderstandingScopeDecision = "allow" | "deny";
|
||||
|
||||
function normalizeDecision(value?: string | null): MediaUnderstandingScopeDecision | undefined {
|
||||
const normalized = value?.trim().toLowerCase();
|
||||
if (normalized === "allow") return "allow";
|
||||
if (normalized === "deny") return "deny";
|
||||
if (normalized === "allow") {
|
||||
return "allow";
|
||||
}
|
||||
if (normalized === "deny") {
|
||||
return "deny";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -26,23 +30,33 @@ export function resolveMediaUnderstandingScope(params: {
|
||||
chatType?: string;
|
||||
}): MediaUnderstandingScopeDecision {
|
||||
const scope = params.scope;
|
||||
if (!scope) return "allow";
|
||||
if (!scope) {
|
||||
return "allow";
|
||||
}
|
||||
|
||||
const channel = normalizeMatch(params.channel);
|
||||
const chatType = normalizeMediaUnderstandingChatType(params.chatType);
|
||||
const sessionKey = normalizeMatch(params.sessionKey) ?? "";
|
||||
|
||||
for (const rule of scope.rules ?? []) {
|
||||
if (!rule) continue;
|
||||
if (!rule) {
|
||||
continue;
|
||||
}
|
||||
const action = normalizeDecision(rule.action) ?? "allow";
|
||||
const match = rule.match ?? {};
|
||||
const matchChannel = normalizeMatch(match.channel);
|
||||
const matchChatType = normalizeMediaUnderstandingChatType(match.chatType);
|
||||
const matchPrefix = normalizeMatch(match.keyPrefix);
|
||||
|
||||
if (matchChannel && matchChannel !== channel) continue;
|
||||
if (matchChatType && matchChatType !== chatType) continue;
|
||||
if (matchPrefix && !sessionKey.startsWith(matchPrefix)) continue;
|
||||
if (matchChannel && matchChannel !== channel) {
|
||||
continue;
|
||||
}
|
||||
if (matchChatType && matchChatType !== chatType) {
|
||||
continue;
|
||||
}
|
||||
if (matchPrefix && !sessionKey.startsWith(matchPrefix)) {
|
||||
continue;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user