refactor(channels): share route format and binding helpers

This commit is contained in:
Vincent Koc 2026-03-20 09:29:58 -07:00
parent faa9faa767
commit 9b6f286ac2
12 changed files with 217 additions and 306 deletions

View File

@ -0,0 +1,66 @@
import type { ChannelPlugin } from "../api.js";
import {
resolveLineAccount,
type OpenClawConfig,
type ResolvedLineAccount,
} from "../runtime-api.js";
import { lineConfigAdapter } from "./config-adapter.js";
import { LineChannelConfigSchema } from "./config-schema.js";
export const lineChannelMeta = {
id: "line",
label: "LINE",
selectionLabel: "LINE (Messaging API)",
detailLabel: "LINE Bot",
docsPath: "/channels/line",
docsLabel: "line",
blurb: "LINE Messaging API bot for Japan/Taiwan/Thailand markets.",
systemImage: "message.fill",
} as const;
export const lineChannelPluginCommon = {
meta: {
...lineChannelMeta,
quickstartAllowFrom: true,
},
capabilities: {
chatTypes: ["direct", "group"],
reactions: false,
threads: false,
media: true,
nativeCommands: false,
blockStreaming: true,
},
reload: { configPrefixes: ["channels.line"] },
configSchema: LineChannelConfigSchema,
config: {
...lineConfigAdapter,
isConfigured: (account: ResolvedLineAccount) =>
Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
describeAccount: (account: ResolvedLineAccount) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
tokenSource: account.tokenSource ?? undefined,
}),
},
} satisfies Pick<
ChannelPlugin<ResolvedLineAccount>,
"meta" | "capabilities" | "reload" | "configSchema" | "config"
>;
export function isLineConfigured(cfg: OpenClawConfig, accountId: string): boolean {
const resolved = resolveLineAccount({ cfg, accountId });
return Boolean(resolved.channelAccessToken.trim() && resolved.channelSecret.trim());
}
export function parseLineAllowFromId(raw: string): string | null {
const trimmed = raw.trim().replace(/^line:(?:user:)?/i, "");
if (!/^U[a-f0-9]{32}$/i.test(trimmed)) {
return null;
}
return trimmed;
}
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../runtime-api.js";

View File

@ -1,52 +1,11 @@
import {
buildChannelConfigSchema,
LineConfigSchema,
type ChannelPlugin,
type ResolvedLineAccount,
} from "../api.js";
import { lineConfigAdapter } from "./config-adapter.js";
import { type ChannelPlugin, type ResolvedLineAccount } from "../api.js";
import { lineChannelPluginCommon } from "./channel-shared.js";
import { lineSetupAdapter } from "./setup-core.js";
import { lineSetupWizard } from "./setup-surface.js";
const meta = {
id: "line",
label: "LINE",
selectionLabel: "LINE (Messaging API)",
detailLabel: "LINE Bot",
docsPath: "/channels/line",
docsLabel: "line",
blurb: "LINE Messaging API bot for Japan/Taiwan/Thailand markets.",
systemImage: "message.fill",
} as const;
export const lineSetupPlugin: ChannelPlugin<ResolvedLineAccount> = {
id: "line",
meta: {
...meta,
quickstartAllowFrom: true,
},
capabilities: {
chatTypes: ["direct", "group"],
reactions: false,
threads: false,
media: true,
nativeCommands: false,
blockStreaming: true,
},
reload: { configPrefixes: ["channels.line"] },
configSchema: buildChannelConfigSchema(LineConfigSchema),
config: {
...lineConfigAdapter,
isConfigured: (account) =>
Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
tokenSource: account.tokenSource ?? undefined,
}),
},
...lineChannelPluginCommon,
setupWizard: lineSetupWizard,
setup: lineSetupAdapter,
};

View File

@ -9,12 +9,10 @@ import {
} from "openclaw/plugin-sdk/channel-runtime";
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
import {
buildChannelConfigSchema,
buildComputedAccountStatusSnapshot,
buildTokenChannelStatusSummary,
clearAccountEntryFields,
DEFAULT_ACCOUNT_ID,
LineConfigSchema,
processLineMessage,
type ChannelPlugin,
type ChannelStatusIssue,
@ -23,24 +21,12 @@ import {
type OpenClawConfig,
type ResolvedLineAccount,
} from "../api.js";
import { lineConfigAdapter } from "./config-adapter.js";
import { lineChannelPluginCommon } from "./channel-shared.js";
import { resolveLineGroupRequireMention } from "./group-policy.js";
import { getLineRuntime } from "./runtime.js";
import { lineSetupAdapter } from "./setup-core.js";
import { lineSetupWizard } from "./setup-surface.js";
// LINE channel metadata
const meta = {
id: "line",
label: "LINE",
selectionLabel: "LINE (Messaging API)",
detailLabel: "LINE Bot",
docsPath: "/channels/line",
docsLabel: "line",
blurb: "LINE Messaging API bot for Japan/Taiwan/Thailand markets.",
systemImage: "message.fill",
};
const resolveLineDmPolicy = createScopedDmSecurityResolver<ResolvedLineAccount>({
channelKey: "line",
resolvePolicy: (account) => account.config.dmPolicy,
@ -63,10 +49,7 @@ const collectLineSecurityWarnings =
export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
id: "line",
meta: {
...meta,
quickstartAllowFrom: true,
},
...lineChannelPluginCommon,
pairing: createTextPairingAdapter({
idLabel: "lineUserId",
message: "OpenClaw: your access has been approved.",
@ -83,29 +66,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
});
},
}),
capabilities: {
chatTypes: ["direct", "group"],
reactions: false,
threads: false,
media: true,
nativeCommands: false,
blockStreaming: true,
},
reload: { configPrefixes: ["channels.line"] },
configSchema: buildChannelConfigSchema(LineConfigSchema),
setupWizard: lineSetupWizard,
config: {
...lineConfigAdapter,
isConfigured: (account) =>
Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
tokenSource: account.tokenSource ?? undefined,
}),
},
security: {
resolveDmPolicy: resolveLineDmPolicy,
collectWarnings: collectLineSecurityWarnings,

View File

@ -1,4 +1,5 @@
import MarkdownIt from "markdown-it";
import { isAutoLinkedFileRef } from "openclaw/plugin-sdk/text-runtime";
const md = new MarkdownIt({
html: false,
@ -10,38 +11,6 @@ const md = new MarkdownIt({
md.enable("strikethrough");
const { escapeHtml } = md.utils;
/**
* Keep bare file references like README.md from becoming external http:// links.
* Telegram already hardens this path; Matrix should not turn common code/docs
* filenames into clickable registrar-style URLs either.
*/
const FILE_EXTENSIONS_WITH_TLD = new Set(["md", "go", "py", "pl", "sh", "am", "at", "be", "cc"]);
function isAutoLinkedFileRef(href: string, label: string): boolean {
const stripped = href.replace(/^https?:\/\//i, "");
if (stripped !== label) {
return false;
}
const dotIndex = label.lastIndexOf(".");
if (dotIndex < 1) {
return false;
}
const ext = label.slice(dotIndex + 1).toLowerCase();
if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) {
return false;
}
const segments = label.split("/");
if (segments.length > 1) {
for (let i = 0; i < segments.length - 1; i += 1) {
if (segments[i]?.includes(".")) {
return false;
}
}
}
return true;
}
function shouldSuppressAutoLink(
tokens: Parameters<NonNullable<typeof md.renderer.rules.link_open>>[0],
idx: number,

View File

@ -1,3 +1,4 @@
import { resolveThreadBindingLifecycle } from "openclaw/plugin-sdk/channel-runtime";
import type {
BindingTargetKind,
SessionBindingRecord,
@ -74,32 +75,7 @@ export function resolveEffectiveBindingExpiry(params: {
expiresAt?: number;
reason?: "idle-expired" | "max-age-expired";
} {
const idleTimeoutMs =
typeof params.record.idleTimeoutMs === "number"
? Math.max(0, Math.floor(params.record.idleTimeoutMs))
: params.defaultIdleTimeoutMs;
const maxAgeMs =
typeof params.record.maxAgeMs === "number"
? Math.max(0, Math.floor(params.record.maxAgeMs))
: params.defaultMaxAgeMs;
const inactivityExpiresAt =
idleTimeoutMs > 0
? Math.max(params.record.lastActivityAt, params.record.boundAt) + idleTimeoutMs
: undefined;
const maxAgeExpiresAt = maxAgeMs > 0 ? params.record.boundAt + maxAgeMs : undefined;
if (inactivityExpiresAt != null && maxAgeExpiresAt != null) {
return inactivityExpiresAt <= maxAgeExpiresAt
? { expiresAt: inactivityExpiresAt, reason: "idle-expired" }
: { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" };
}
if (inactivityExpiresAt != null) {
return { expiresAt: inactivityExpiresAt, reason: "idle-expired" };
}
if (maxAgeExpiresAt != null) {
return { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" };
}
return {};
return resolveThreadBindingLifecycle(params);
}
export function toSessionBindingRecord(

View File

@ -1,6 +1,8 @@
import type { MarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
import {
chunkMarkdownIR,
FILE_REF_EXTENSIONS_WITH_TLD,
isAutoLinkedFileRef,
markdownToIR,
type MarkdownLinkSpan,
type MarkdownIR,
@ -31,44 +33,6 @@ function escapeHtmlAttr(text: string): string {
*
* Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io)
*/
const FILE_EXTENSIONS_WITH_TLD = new Set([
"md", // Markdown (Moldova) - very common in repos
"go", // Go language - common in Go projects
"py", // Python (Paraguay) - common in Python projects
"pl", // Perl (Poland) - common in Perl projects
"sh", // Shell (Saint Helena) - common for scripts
"am", // Automake files (Armenia)
"at", // Assembly (Austria)
"be", // Backend files (Belgium)
"cc", // C++ source (Cocos Islands)
]);
/** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */
function isAutoLinkedFileRef(href: string, label: string): boolean {
const stripped = href.replace(/^https?:\/\//i, "");
if (stripped !== label) {
return false;
}
const dotIndex = label.lastIndexOf(".");
if (dotIndex < 1) {
return false;
}
const ext = label.slice(dotIndex + 1).toLowerCase();
if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) {
return false;
}
// Reject if any path segment before the filename contains a dot (looks like a domain)
const segments = label.split("/");
if (segments.length > 1) {
for (let i = 0; i < segments.length - 1; i++) {
if (segments[i].includes(".")) {
return false;
}
}
}
return true;
}
function buildTelegramLink(link: MarkdownLinkSpan, text: string) {
const href = link.href.trim();
if (!href) {
@ -139,7 +103,7 @@ function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const FILE_EXTENSIONS_PATTERN = Array.from(FILE_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
const FILE_EXTENSIONS_PATTERN = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
const FILE_REFERENCE_PATTERN = new RegExp(
`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`,

View File

@ -2,6 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { resolveThreadBindingConversationIdFromBindingId } from "openclaw/plugin-sdk/channel-runtime";
import { resolveThreadBindingEffectiveExpiresAt } from "openclaw/plugin-sdk/channel-runtime";
import { formatThreadBindingDurationLabel } from "openclaw/plugin-sdk/channel-runtime";
import {
registerSessionBindingAdapter,
@ -115,32 +116,6 @@ function toTelegramTargetKind(raw: BindingTargetKind): TelegramBindingTargetKind
return raw === "subagent" ? "subagent" : "acp";
}
function resolveEffectiveBindingExpiresAt(params: {
record: TelegramThreadBindingRecord;
defaultIdleTimeoutMs: number;
defaultMaxAgeMs: number;
}): number | undefined {
const idleTimeoutMs =
typeof params.record.idleTimeoutMs === "number"
? Math.max(0, Math.floor(params.record.idleTimeoutMs))
: params.defaultIdleTimeoutMs;
const maxAgeMs =
typeof params.record.maxAgeMs === "number"
? Math.max(0, Math.floor(params.record.maxAgeMs))
: params.defaultMaxAgeMs;
const inactivityExpiresAt =
idleTimeoutMs > 0
? Math.max(params.record.lastActivityAt, params.record.boundAt) + idleTimeoutMs
: undefined;
const maxAgeExpiresAt = maxAgeMs > 0 ? params.record.boundAt + maxAgeMs : undefined;
if (inactivityExpiresAt != null && maxAgeExpiresAt != null) {
return Math.min(inactivityExpiresAt, maxAgeExpiresAt);
}
return inactivityExpiresAt ?? maxAgeExpiresAt;
}
function toSessionBindingRecord(
record: TelegramThreadBindingRecord,
defaults: { idleTimeoutMs: number; maxAgeMs: number },
@ -159,7 +134,7 @@ function toSessionBindingRecord(
},
status: "active",
boundAt: record.boundAt,
expiresAt: resolveEffectiveBindingExpiresAt({
expiresAt: resolveThreadBindingEffectiveExpiresAt({
record,
defaultIdleTimeoutMs: defaults.idleTimeoutMs,
defaultMaxAgeMs: defaults.maxAgeMs,

View File

@ -39,7 +39,12 @@ import { probeZalouser } from "./probe.js";
import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
import { getZalouserRuntime } from "./runtime.js";
import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
import { resolveZalouserOutboundSessionRoute } from "./session-route.js";
import {
normalizeZalouserTarget,
parseZalouserDirectoryGroupId,
parseZalouserOutboundTarget,
resolveZalouserOutboundSessionRoute,
} from "./session-route.js";
import { zalouserSetupAdapter } from "./setup-core.js";
import { zalouserSetupWizard } from "./setup-surface.js";
import { createZalouserPluginBase } from "./shared.js";
@ -56,97 +61,6 @@ import {
const ZALOUSER_TEXT_CHUNK_LIMIT = 2000;
function stripZalouserTargetPrefix(raw: string): string {
return raw
.trim()
.replace(/^(zalouser|zlu):/i, "")
.trim();
}
function normalizePrefixedTarget(raw: string): string | undefined {
const trimmed = stripZalouserTargetPrefix(raw);
if (!trimmed) {
return undefined;
}
const lower = trimmed.toLowerCase();
if (lower.startsWith("group:")) {
const id = trimmed.slice("group:".length).trim();
return id ? `group:${id}` : undefined;
}
if (lower.startsWith("g:")) {
const id = trimmed.slice("g:".length).trim();
return id ? `group:${id}` : undefined;
}
if (lower.startsWith("user:")) {
const id = trimmed.slice("user:".length).trim();
return id ? `user:${id}` : undefined;
}
if (lower.startsWith("dm:")) {
const id = trimmed.slice("dm:".length).trim();
return id ? `user:${id}` : undefined;
}
if (lower.startsWith("u:")) {
const id = trimmed.slice("u:".length).trim();
return id ? `user:${id}` : undefined;
}
if (/^g-\S+$/i.test(trimmed)) {
return `group:${trimmed}`;
}
if (/^u-\S+$/i.test(trimmed)) {
return `user:${trimmed}`;
}
return trimmed;
}
function parseZalouserOutboundTarget(raw: string): {
threadId: string;
isGroup: boolean;
} {
const normalized = normalizePrefixedTarget(raw);
if (!normalized) {
throw new Error("Zalouser target is required");
}
const lowered = normalized.toLowerCase();
if (lowered.startsWith("group:")) {
const threadId = normalized.slice("group:".length).trim();
if (!threadId) {
throw new Error("Zalouser group target is missing group id");
}
return { threadId, isGroup: true };
}
if (lowered.startsWith("user:")) {
const threadId = normalized.slice("user:".length).trim();
if (!threadId) {
throw new Error("Zalouser user target is missing user id");
}
return { threadId, isGroup: false };
}
// Backward-compatible fallback for bare IDs.
// Group sends should use explicit `group:<id>` targets.
return { threadId: normalized, isGroup: false };
}
function parseZalouserDirectoryGroupId(raw: string): string {
const normalized = normalizePrefixedTarget(raw);
if (!normalized) {
throw new Error("Zalouser group target is required");
}
const lowered = normalized.toLowerCase();
if (lowered.startsWith("group:")) {
const groupId = normalized.slice("group:".length).trim();
if (!groupId) {
throw new Error("Zalouser group target is missing group id");
}
return groupId;
}
if (lowered.startsWith("user:")) {
throw new Error("Zalouser group members lookup requires a group target (group:<id>)");
}
return normalized;
}
function resolveZalouserQrProfile(accountId?: string | null): string {
const normalized = normalizeAccountId(accountId);
if (!normalized || normalized === DEFAULT_ACCOUNT_ID) {
@ -318,11 +232,11 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
},
actions: zalouserMessageActions,
messaging: {
normalizeTarget: (raw) => normalizePrefixedTarget(raw),
normalizeTarget: (raw) => normalizeZalouserTarget(raw),
resolveOutboundSessionRoute: (params) => resolveZalouserOutboundSessionRoute(params),
targetResolver: {
looksLikeId: (raw) => {
const normalized = normalizePrefixedTarget(raw);
const normalized = normalizeZalouserTarget(raw);
if (!normalized) {
return false;
}

View File

@ -3,14 +3,14 @@ import {
type ChannelOutboundSessionRouteParams,
} from "openclaw/plugin-sdk/core";
function stripZalouserTargetPrefix(raw: string): string {
export function stripZalouserTargetPrefix(raw: string): string {
return raw
.trim()
.replace(/^(zalouser|zlu):/i, "")
.trim();
}
function normalizePrefixedTarget(raw: string): string | undefined {
export function normalizeZalouserTarget(raw: string): string | undefined {
const trimmed = stripZalouserTargetPrefix(raw);
if (!trimmed) {
return undefined;
@ -47,8 +47,55 @@ function normalizePrefixedTarget(raw: string): string | undefined {
return trimmed;
}
export function parseZalouserOutboundTarget(raw: string): {
threadId: string;
isGroup: boolean;
} {
const normalized = normalizeZalouserTarget(raw);
if (!normalized) {
throw new Error("Zalouser target is required");
}
const lowered = normalized.toLowerCase();
if (lowered.startsWith("group:")) {
const threadId = normalized.slice("group:".length).trim();
if (!threadId) {
throw new Error("Zalouser group target is missing group id");
}
return { threadId, isGroup: true };
}
if (lowered.startsWith("user:")) {
const threadId = normalized.slice("user:".length).trim();
if (!threadId) {
throw new Error("Zalouser user target is missing user id");
}
return { threadId, isGroup: false };
}
// Backward-compatible fallback for bare IDs.
// Group sends should use explicit `group:<id>` targets.
return { threadId: normalized, isGroup: false };
}
export function parseZalouserDirectoryGroupId(raw: string): string {
const normalized = normalizeZalouserTarget(raw);
if (!normalized) {
throw new Error("Zalouser group target is required");
}
const lowered = normalized.toLowerCase();
if (lowered.startsWith("group:")) {
const groupId = normalized.slice("group:".length).trim();
if (!groupId) {
throw new Error("Zalouser group target is missing group id");
}
return groupId;
}
if (lowered.startsWith("user:")) {
throw new Error("Zalouser group members lookup requires a group target (group:<id>)");
}
return normalized;
}
export function resolveZalouserOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {
const normalized = normalizePrefixedTarget(params.target);
const normalized = normalizeZalouserTarget(params.target);
if (!normalized) {
return null;
}

View File

@ -73,6 +73,58 @@ export function resolveThreadBindingMaxAgeMs(params: {
return Math.floor(maxAgeHours * 60 * 60 * 1000);
}
type ThreadBindingLifecycleRecord = {
boundAt: number;
lastActivityAt: number;
idleTimeoutMs?: number;
maxAgeMs?: number;
};
export function resolveThreadBindingLifecycle(params: {
record: ThreadBindingLifecycleRecord;
defaultIdleTimeoutMs: number;
defaultMaxAgeMs: number;
}): {
expiresAt?: number;
reason?: "idle-expired" | "max-age-expired";
} {
const idleTimeoutMs =
typeof params.record.idleTimeoutMs === "number"
? Math.max(0, Math.floor(params.record.idleTimeoutMs))
: params.defaultIdleTimeoutMs;
const maxAgeMs =
typeof params.record.maxAgeMs === "number"
? Math.max(0, Math.floor(params.record.maxAgeMs))
: params.defaultMaxAgeMs;
const inactivityExpiresAt =
idleTimeoutMs > 0
? Math.max(params.record.lastActivityAt, params.record.boundAt) + idleTimeoutMs
: undefined;
const maxAgeExpiresAt = maxAgeMs > 0 ? params.record.boundAt + maxAgeMs : undefined;
if (inactivityExpiresAt != null && maxAgeExpiresAt != null) {
return inactivityExpiresAt <= maxAgeExpiresAt
? { expiresAt: inactivityExpiresAt, reason: "idle-expired" }
: { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" };
}
if (inactivityExpiresAt != null) {
return { expiresAt: inactivityExpiresAt, reason: "idle-expired" };
}
if (maxAgeExpiresAt != null) {
return { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" };
}
return {};
}
export function resolveThreadBindingEffectiveExpiresAt(params: {
record: ThreadBindingLifecycleRecord;
defaultIdleTimeoutMs: number;
defaultMaxAgeMs: number;
}): number | undefined {
return resolveThreadBindingLifecycle(params).expiresAt;
}
export function resolveThreadBindingsEnabled(params: {
channelEnabledRaw: unknown;
sessionEnabledRaw: unknown;

View File

@ -13,6 +13,7 @@ export * from "../shared/global-singleton.js";
export * from "../shared/string-normalization.js";
export * from "../shared/string-sample.js";
export * from "../shared/text/assistant-visible-text.js";
export * from "../shared/text/auto-linked-file-ref.js";
export * from "../shared/text/code-regions.js";
export * from "../shared/text/reasoning-tags.js";
export * from "../terminal/safe-text.js";

View File

@ -0,0 +1,27 @@
const FILE_REF_EXTENSIONS = ["md", "go", "py", "pl", "sh", "am", "at", "be", "cc"] as const;
export const FILE_REF_EXTENSIONS_WITH_TLD = new Set<string>(FILE_REF_EXTENSIONS);
export function isAutoLinkedFileRef(href: string, label: string): boolean {
const stripped = href.replace(/^https?:\/\//i, "");
if (stripped !== label) {
return false;
}
const dotIndex = label.lastIndexOf(".");
if (dotIndex < 1) {
return false;
}
const ext = label.slice(dotIndex + 1).toLowerCase();
if (!FILE_REF_EXTENSIONS_WITH_TLD.has(ext)) {
return false;
}
const segments = label.split("/");
if (segments.length > 1) {
for (let i = 0; i < segments.length - 1; i += 1) {
if (segments[i]?.includes(".")) {
return false;
}
}
}
return true;
}