mirror of https://github.com/openclaw/openclaw.git
Compare commits
4 Commits
a64b03cda1
...
d683c4e6a4
| Author | SHA1 | Date |
|---|---|---|
|
|
d683c4e6a4 | |
|
|
133fb1a300 | |
|
|
c4265a5f16 | |
|
|
26e0a3ee9a |
|
|
@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
|
||||||
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
|
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
|
||||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
||||||
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
|
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
|
||||||
|
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
|
||||||
|
|
||||||
## 2026.3.13
|
## 2026.3.13
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -512,6 +512,146 @@ function sliceLinkSpans(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sliceMarkdownIR(ir: MarkdownIR, start: number, end: number): MarkdownIR {
|
||||||
|
return {
|
||||||
|
text: ir.text.slice(start, end),
|
||||||
|
styles: sliceStyleSpans(ir.styles, start, end),
|
||||||
|
links: sliceLinkSpans(ir.links, start, end),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAdjacentStyleSpans(styles: MarkdownIR["styles"]): MarkdownIR["styles"] {
|
||||||
|
const merged: MarkdownIR["styles"] = [];
|
||||||
|
for (const span of styles) {
|
||||||
|
const last = merged.at(-1);
|
||||||
|
if (last && last.style === span.style && span.start <= last.end) {
|
||||||
|
last.end = Math.max(last.end, span.end);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
merged.push({ ...span });
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAdjacentLinkSpans(links: MarkdownIR["links"]): MarkdownIR["links"] {
|
||||||
|
const merged: MarkdownIR["links"] = [];
|
||||||
|
for (const link of links) {
|
||||||
|
const last = merged.at(-1);
|
||||||
|
if (last && last.href === link.href && link.start <= last.end) {
|
||||||
|
last.end = Math.max(last.end, link.end);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
merged.push({ ...link });
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeMarkdownIRChunks(left: MarkdownIR, right: MarkdownIR): MarkdownIR {
|
||||||
|
const offset = left.text.length;
|
||||||
|
return {
|
||||||
|
text: left.text + right.text,
|
||||||
|
styles: mergeAdjacentStyleSpans([
|
||||||
|
...left.styles,
|
||||||
|
...right.styles.map((span) => ({
|
||||||
|
...span,
|
||||||
|
start: span.start + offset,
|
||||||
|
end: span.end + offset,
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
links: mergeAdjacentLinkSpans([
|
||||||
|
...left.links,
|
||||||
|
...right.links.map((link) => ({
|
||||||
|
...link,
|
||||||
|
start: link.start + offset,
|
||||||
|
end: link.end + offset,
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTelegramChunkHtml(ir: MarkdownIR): string {
|
||||||
|
return wrapFileReferencesInHtml(renderTelegramHtml(ir));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMarkdownIRPreservedSplitIndex(text: string, start: number, limit: number): number {
|
||||||
|
const maxEnd = Math.min(text.length, start + limit);
|
||||||
|
if (maxEnd >= text.length) {
|
||||||
|
return text.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastOutsideParenNewlineBreak = -1;
|
||||||
|
let lastOutsideParenWhitespaceBreak = -1;
|
||||||
|
let lastOutsideParenWhitespaceRunStart = -1;
|
||||||
|
let lastAnyNewlineBreak = -1;
|
||||||
|
let lastAnyWhitespaceBreak = -1;
|
||||||
|
let lastAnyWhitespaceRunStart = -1;
|
||||||
|
let parenDepth = 0;
|
||||||
|
let sawNonWhitespace = false;
|
||||||
|
|
||||||
|
for (let index = start; index < maxEnd; index += 1) {
|
||||||
|
const char = text[index];
|
||||||
|
if (char === "(") {
|
||||||
|
sawNonWhitespace = true;
|
||||||
|
parenDepth += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === ")" && parenDepth > 0) {
|
||||||
|
sawNonWhitespace = true;
|
||||||
|
parenDepth -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!/\s/.test(char)) {
|
||||||
|
sawNonWhitespace = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!sawNonWhitespace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "\n") {
|
||||||
|
lastAnyNewlineBreak = index + 1;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
lastOutsideParenNewlineBreak = index + 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const whitespaceRunStart =
|
||||||
|
index === start || !/\s/.test(text[index - 1] ?? "") ? index : lastAnyWhitespaceRunStart;
|
||||||
|
lastAnyWhitespaceBreak = index + 1;
|
||||||
|
lastAnyWhitespaceRunStart = whitespaceRunStart;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
lastOutsideParenWhitespaceBreak = index + 1;
|
||||||
|
lastOutsideParenWhitespaceRunStart = whitespaceRunStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveWhitespaceBreak = (breakIndex: number, runStart: number): number => {
|
||||||
|
if (breakIndex <= start) {
|
||||||
|
return breakIndex;
|
||||||
|
}
|
||||||
|
if (runStart <= start) {
|
||||||
|
return breakIndex;
|
||||||
|
}
|
||||||
|
return /\s/.test(text[breakIndex] ?? "") ? runStart : breakIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastOutsideParenNewlineBreak > start) {
|
||||||
|
return lastOutsideParenNewlineBreak;
|
||||||
|
}
|
||||||
|
if (lastOutsideParenWhitespaceBreak > start) {
|
||||||
|
return resolveWhitespaceBreak(
|
||||||
|
lastOutsideParenWhitespaceBreak,
|
||||||
|
lastOutsideParenWhitespaceRunStart,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (lastAnyNewlineBreak > start) {
|
||||||
|
return lastAnyNewlineBreak;
|
||||||
|
}
|
||||||
|
if (lastAnyWhitespaceBreak > start) {
|
||||||
|
return resolveWhitespaceBreak(lastAnyWhitespaceBreak, lastAnyWhitespaceRunStart);
|
||||||
|
}
|
||||||
|
return maxEnd;
|
||||||
|
}
|
||||||
|
|
||||||
function splitMarkdownIRPreserveWhitespace(ir: MarkdownIR, limit: number): MarkdownIR[] {
|
function splitMarkdownIRPreserveWhitespace(ir: MarkdownIR, limit: number): MarkdownIR[] {
|
||||||
if (!ir.text) {
|
if (!ir.text) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -523,7 +663,7 @@ function splitMarkdownIRPreserveWhitespace(ir: MarkdownIR, limit: number): Markd
|
||||||
const chunks: MarkdownIR[] = [];
|
const chunks: MarkdownIR[] = [];
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
while (cursor < ir.text.length) {
|
while (cursor < ir.text.length) {
|
||||||
const end = Math.min(ir.text.length, cursor + normalizedLimit);
|
const end = findMarkdownIRPreservedSplitIndex(ir.text, cursor, normalizedLimit);
|
||||||
chunks.push({
|
chunks.push({
|
||||||
text: ir.text.slice(cursor, end),
|
text: ir.text.slice(cursor, end),
|
||||||
styles: sliceStyleSpans(ir.styles, cursor, end),
|
styles: sliceStyleSpans(ir.styles, cursor, end),
|
||||||
|
|
@ -534,32 +674,98 @@ function splitMarkdownIRPreserveWhitespace(ir: MarkdownIR, limit: number): Markd
|
||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coalesceWhitespaceOnlyMarkdownIRChunks(chunks: MarkdownIR[], limit: number): MarkdownIR[] {
|
||||||
|
const coalesced: MarkdownIR[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
while (index < chunks.length) {
|
||||||
|
const chunk = chunks[index];
|
||||||
|
if (!chunk) {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (chunk.text.trim().length > 0) {
|
||||||
|
coalesced.push(chunk);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = coalesced.at(-1);
|
||||||
|
const next = chunks[index + 1];
|
||||||
|
const chunkLength = chunk.text.length;
|
||||||
|
|
||||||
|
const canMergePrev = (candidate: MarkdownIR) =>
|
||||||
|
renderTelegramChunkHtml(candidate).length <= limit;
|
||||||
|
const canMergeNext = (candidate: MarkdownIR) =>
|
||||||
|
renderTelegramChunkHtml(candidate).length <= limit;
|
||||||
|
|
||||||
|
if (prev) {
|
||||||
|
const mergedPrev = mergeMarkdownIRChunks(prev, chunk);
|
||||||
|
if (canMergePrev(mergedPrev)) {
|
||||||
|
coalesced[coalesced.length - 1] = mergedPrev;
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
const mergedNext = mergeMarkdownIRChunks(chunk, next);
|
||||||
|
if (canMergeNext(mergedNext)) {
|
||||||
|
chunks[index + 1] = mergedNext;
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev && next) {
|
||||||
|
for (let prefixLength = chunkLength - 1; prefixLength >= 1; prefixLength -= 1) {
|
||||||
|
const prefix = sliceMarkdownIR(chunk, 0, prefixLength);
|
||||||
|
const suffix = sliceMarkdownIR(chunk, prefixLength, chunkLength);
|
||||||
|
const mergedPrev = mergeMarkdownIRChunks(prev, prefix);
|
||||||
|
const mergedNext = mergeMarkdownIRChunks(suffix, next);
|
||||||
|
if (canMergePrev(mergedPrev) && canMergeNext(mergedNext)) {
|
||||||
|
coalesced[coalesced.length - 1] = mergedPrev;
|
||||||
|
chunks[index + 1] = mergedNext;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return coalesced;
|
||||||
|
}
|
||||||
|
|
||||||
function renderTelegramChunksWithinHtmlLimit(
|
function renderTelegramChunksWithinHtmlLimit(
|
||||||
ir: MarkdownIR,
|
ir: MarkdownIR,
|
||||||
limit: number,
|
limit: number,
|
||||||
): TelegramFormattedChunk[] {
|
): TelegramFormattedChunk[] {
|
||||||
const normalizedLimit = Math.max(1, Math.floor(limit));
|
const normalizedLimit = Math.max(1, Math.floor(limit));
|
||||||
const pending = chunkMarkdownIR(ir, normalizedLimit);
|
const pending = chunkMarkdownIR(ir, normalizedLimit);
|
||||||
const rendered: TelegramFormattedChunk[] = [];
|
const finalized: MarkdownIR[] = [];
|
||||||
while (pending.length > 0) {
|
while (pending.length > 0) {
|
||||||
const chunk = pending.shift();
|
const chunk = pending.shift();
|
||||||
if (!chunk) {
|
if (!chunk) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const html = wrapFileReferencesInHtml(renderTelegramHtml(chunk));
|
const html = renderTelegramChunkHtml(chunk);
|
||||||
if (html.length <= normalizedLimit || chunk.text.length <= 1) {
|
if (html.length <= normalizedLimit || chunk.text.length <= 1) {
|
||||||
rendered.push({ html, text: chunk.text });
|
finalized.push(chunk);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const split = splitTelegramChunkByHtmlLimit(chunk, normalizedLimit, html.length);
|
const split = splitTelegramChunkByHtmlLimit(chunk, normalizedLimit, html.length);
|
||||||
if (split.length <= 1) {
|
if (split.length <= 1) {
|
||||||
// Worst-case safety: avoid retry loops, deliver the chunk as-is.
|
// Worst-case safety: avoid retry loops, deliver the chunk as-is.
|
||||||
rendered.push({ html, text: chunk.text });
|
finalized.push(chunk);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
pending.unshift(...split);
|
pending.unshift(...split);
|
||||||
}
|
}
|
||||||
return rendered;
|
return coalesceWhitespaceOnlyMarkdownIRChunks(finalized, normalizedLimit).map((chunk) => ({
|
||||||
|
html: renderTelegramChunkHtml(chunk),
|
||||||
|
text: chunk.text,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markdownToTelegramChunks(
|
export function markdownToTelegramChunks(
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,35 @@ describe("markdownToTelegramChunks - file reference wrapping", () => {
|
||||||
expect(chunks.map((chunk) => chunk.text).join("")).toBe(input);
|
expect(chunks.map((chunk) => chunk.text).join("")).toBe(input);
|
||||||
expect(chunks.every((chunk) => chunk.html.length <= 5)).toBe(true);
|
expect(chunks.every((chunk) => chunk.html.length <= 5)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers word boundaries when html-limit retry splits formatted prose", () => {
|
||||||
|
const input = "**Which of these**";
|
||||||
|
const chunks = markdownToTelegramChunks(input, 16);
|
||||||
|
expect(chunks.map((chunk) => chunk.text)).toEqual(["Which of ", "these"]);
|
||||||
|
expect(chunks.every((chunk) => chunk.html.length <= 16)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to in-paren word boundaries when the parenthesis is unbalanced", () => {
|
||||||
|
const input = "**foo (bar baz qux quux**";
|
||||||
|
const chunks = markdownToTelegramChunks(input, 20);
|
||||||
|
expect(chunks.map((chunk) => chunk.text)).toEqual(["foo", "(bar baz qux ", "quux"]);
|
||||||
|
expect(chunks.every((chunk) => chunk.html.length <= 20)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not emit whitespace-only chunks during html-limit retry splitting", () => {
|
||||||
|
const input = "**ab <<**";
|
||||||
|
const chunks = markdownToTelegramChunks(input, 11);
|
||||||
|
expect(chunks.map((chunk) => chunk.text).join("")).toBe("ab <<");
|
||||||
|
expect(chunks.every((chunk) => chunk.text.trim().length > 0)).toBe(true);
|
||||||
|
expect(chunks.every((chunk) => chunk.html.length <= 11)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves paragraph separators when retry chunking produces whitespace-only spans", () => {
|
||||||
|
const input = "ab\n\n<<";
|
||||||
|
const chunks = markdownToTelegramChunks(input, 6);
|
||||||
|
expect(chunks.map((chunk) => chunk.text).join("")).toBe(input);
|
||||||
|
expect(chunks.every((chunk) => chunk.html.length <= 6)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("edge cases", () => {
|
describe("edge cases", () => {
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,30 @@ describe("ws connect policy", () => {
|
||||||
expect(shouldSkipControlUiPairing(strict, "operator", true)).toBe(true);
|
expect(shouldSkipControlUiPairing(strict, "operator", true)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("auth.mode=none skips pairing for operator control-ui only", () => {
|
||||||
|
const controlUi = resolveControlUiAuthPolicy({
|
||||||
|
isControlUi: true,
|
||||||
|
controlUiConfig: undefined,
|
||||||
|
deviceRaw: null,
|
||||||
|
});
|
||||||
|
const nonControlUi = resolveControlUiAuthPolicy({
|
||||||
|
isControlUi: false,
|
||||||
|
controlUiConfig: undefined,
|
||||||
|
deviceRaw: null,
|
||||||
|
});
|
||||||
|
// Control UI + operator + auth.mode=none: skip pairing (the fix for #42931)
|
||||||
|
expect(shouldSkipControlUiPairing(controlUi, "operator", false, "none")).toBe(true);
|
||||||
|
// Control UI + node role + auth.mode=none: still require pairing
|
||||||
|
expect(shouldSkipControlUiPairing(controlUi, "node", false, "none")).toBe(false);
|
||||||
|
// Non-Control-UI + operator + auth.mode=none: still require pairing
|
||||||
|
// (prevents #43478 regression where ALL clients bypassed pairing)
|
||||||
|
expect(shouldSkipControlUiPairing(nonControlUi, "operator", false, "none")).toBe(false);
|
||||||
|
// Control UI + operator + auth.mode=shared-key: no change
|
||||||
|
expect(shouldSkipControlUiPairing(controlUi, "operator", false, "shared-key")).toBe(false);
|
||||||
|
// Control UI + operator + no authMode: no change
|
||||||
|
expect(shouldSkipControlUiPairing(controlUi, "operator", false)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
test("trusted-proxy control-ui bypass only applies to operator + trusted-proxy auth", () => {
|
test("trusted-proxy control-ui bypass only applies to operator + trusted-proxy auth", () => {
|
||||||
const cases: Array<{
|
const cases: Array<{
|
||||||
role: "operator" | "node";
|
role: "operator" | "node";
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { GatewayRole } from "../../role-policy.js";
|
||||||
import { roleCanSkipDeviceIdentity } from "../../role-policy.js";
|
import { roleCanSkipDeviceIdentity } from "../../role-policy.js";
|
||||||
|
|
||||||
export type ControlUiAuthPolicy = {
|
export type ControlUiAuthPolicy = {
|
||||||
|
isControlUi: boolean;
|
||||||
allowInsecureAuthConfigured: boolean;
|
allowInsecureAuthConfigured: boolean;
|
||||||
dangerouslyDisableDeviceAuth: boolean;
|
dangerouslyDisableDeviceAuth: boolean;
|
||||||
allowBypass: boolean;
|
allowBypass: boolean;
|
||||||
|
|
@ -24,6 +25,7 @@ export function resolveControlUiAuthPolicy(params: {
|
||||||
const dangerouslyDisableDeviceAuth =
|
const dangerouslyDisableDeviceAuth =
|
||||||
params.isControlUi && params.controlUiConfig?.dangerouslyDisableDeviceAuth === true;
|
params.isControlUi && params.controlUiConfig?.dangerouslyDisableDeviceAuth === true;
|
||||||
return {
|
return {
|
||||||
|
isControlUi: params.isControlUi,
|
||||||
allowInsecureAuthConfigured,
|
allowInsecureAuthConfigured,
|
||||||
dangerouslyDisableDeviceAuth,
|
dangerouslyDisableDeviceAuth,
|
||||||
// `allowInsecureAuth` must not bypass secure-context/device-auth requirements.
|
// `allowInsecureAuth` must not bypass secure-context/device-auth requirements.
|
||||||
|
|
@ -36,10 +38,21 @@ export function shouldSkipControlUiPairing(
|
||||||
policy: ControlUiAuthPolicy,
|
policy: ControlUiAuthPolicy,
|
||||||
role: GatewayRole,
|
role: GatewayRole,
|
||||||
trustedProxyAuthOk = false,
|
trustedProxyAuthOk = false,
|
||||||
|
authMode?: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (trustedProxyAuthOk) {
|
if (trustedProxyAuthOk) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// When auth is completely disabled (mode=none), there is no shared secret
|
||||||
|
// or token to gate pairing. Requiring pairing in this configuration adds
|
||||||
|
// friction without security value since any client can already connect
|
||||||
|
// without credentials. Guard with policy.isControlUi because this function
|
||||||
|
// is called for ALL clients (not just Control UI) at the call site.
|
||||||
|
// Scope to operator role so node-role sessions still need device identity
|
||||||
|
// (#43478 was reverted for skipping ALL clients).
|
||||||
|
if (policy.isControlUi && role === "operator" && authMode === "none") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// dangerouslyDisableDeviceAuth is the break-glass path for Control UI
|
// dangerouslyDisableDeviceAuth is the break-glass path for Control UI
|
||||||
// operators. Keep pairing aligned with the missing-device bypass, including
|
// operators. Keep pairing aligned with the missing-device bypass, including
|
||||||
// open-auth deployments where there is no shared token/password to prove.
|
// open-auth deployments where there is no shared token/password to prove.
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,13 @@ export function attachGatewayWsMessageHandler(params: {
|
||||||
hasBrowserOriginHeader,
|
hasBrowserOriginHeader,
|
||||||
sharedAuthOk,
|
sharedAuthOk,
|
||||||
authMethod,
|
authMethod,
|
||||||
}) || shouldSkipControlUiPairing(controlUiAuthPolicy, role, trustedProxyAuthOk);
|
}) ||
|
||||||
|
shouldSkipControlUiPairing(
|
||||||
|
controlUiAuthPolicy,
|
||||||
|
role,
|
||||||
|
trustedProxyAuthOk,
|
||||||
|
resolvedAuth.mode,
|
||||||
|
);
|
||||||
if (device && devicePublicKey && !skipPairing) {
|
if (device && devicePublicKey && !skipPairing) {
|
||||||
const formatAuditList = (items: string[] | undefined): string => {
|
const formatAuditList = (items: string[] | undefined): string => {
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -729,7 +729,7 @@ export async function edgeTTS(params: {
|
||||||
({ size } = statSync(outputPath));
|
({ size } = statSync(outputPath));
|
||||||
|
|
||||||
if (size === 0) {
|
if (size === 0) {
|
||||||
throw new Error("Edge TTS produced empty audio file after retry");
|
throw new Error("Edge TTS produced empty audio file after retry.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue