mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 06:31:11 +00:00
refactor: dedupe shared helpers
This commit is contained in:
parent
661f11b947
commit
69196670b7
16 changed files with 293 additions and 509 deletions
|
|
@ -37,19 +37,7 @@ export async function loginGeminiCliOAuth(
|
|||
const authUrl = buildAuthUrl(challenge, state);
|
||||
|
||||
if (needsManual) {
|
||||
ctx.progress.update("OAuth URL ready");
|
||||
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
|
||||
ctx.progress.update("Waiting for you to paste the callback URL...");
|
||||
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
|
||||
const parsed = parseCallbackInput(callbackInput);
|
||||
if ("error" in parsed) {
|
||||
throw new Error(parsed.error);
|
||||
}
|
||||
if (parsed.state !== state) {
|
||||
throw new Error("OAuth state mismatch - please try again");
|
||||
}
|
||||
ctx.progress.update("Exchanging authorization code for tokens...");
|
||||
return exchangeCodeForTokens(parsed.code, verifier);
|
||||
return manualFlow(ctx, authUrl, state, verifier);
|
||||
}
|
||||
|
||||
ctx.progress.update("Complete sign-in in browser...");
|
||||
|
|
@ -75,18 +63,30 @@ export async function loginGeminiCliOAuth(
|
|||
err.message.includes("listen"))
|
||||
) {
|
||||
ctx.progress.update("Local callback server failed. Switching to manual mode...");
|
||||
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
|
||||
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
|
||||
const parsed = parseCallbackInput(callbackInput);
|
||||
if ("error" in parsed) {
|
||||
throw new Error(parsed.error, { cause: err });
|
||||
}
|
||||
if (parsed.state !== state) {
|
||||
throw new Error("OAuth state mismatch - please try again", { cause: err });
|
||||
}
|
||||
ctx.progress.update("Exchanging authorization code for tokens...");
|
||||
return exchangeCodeForTokens(parsed.code, verifier);
|
||||
return manualFlow(ctx, authUrl, state, verifier, err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function manualFlow(
|
||||
ctx: GeminiCliOAuthContext,
|
||||
authUrl: string,
|
||||
state: string,
|
||||
verifier: string,
|
||||
cause?: Error,
|
||||
): Promise<GeminiCliOAuthCredentials> {
|
||||
ctx.progress.update("OAuth URL ready");
|
||||
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
|
||||
ctx.progress.update("Waiting for you to paste the callback URL...");
|
||||
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
|
||||
const parsed = parseCallbackInput(callbackInput);
|
||||
if ("error" in parsed) {
|
||||
throw new Error(parsed.error, cause ? { cause } : undefined);
|
||||
}
|
||||
if (parsed.state !== state) {
|
||||
throw new Error("OAuth state mismatch - please try again", cause ? { cause } : undefined);
|
||||
}
|
||||
ctx.progress.update("Exchanging authorization code for tokens...");
|
||||
return exchangeCodeForTokens(parsed.code, verifier);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@ import {
|
|||
captureHttpExchange,
|
||||
isDebugProxyGlobalFetchPatchInstalled,
|
||||
} from "openclaw/plugin-sdk/proxy-capture";
|
||||
import {
|
||||
asObject,
|
||||
readResponseTextLimited,
|
||||
trimToUndefined,
|
||||
truncateErrorDetail,
|
||||
} from "openclaw/plugin-sdk/speech";
|
||||
import { extractProviderErrorDetail, trimToUndefined } from "openclaw/plugin-sdk/speech";
|
||||
|
||||
export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
||||
|
||||
|
|
@ -69,43 +64,8 @@ export function resolveOpenAITtsInstructions(
|
|||
return next && model.includes("gpt-4o-mini-tts") ? next : undefined;
|
||||
}
|
||||
|
||||
function formatOpenAiErrorPayload(payload: unknown): string | undefined {
|
||||
const root = asObject(payload);
|
||||
const subject = asObject(root?.error) ?? root;
|
||||
if (!subject) {
|
||||
return undefined;
|
||||
}
|
||||
const message =
|
||||
trimToUndefined(subject.message) ??
|
||||
trimToUndefined(subject.detail) ??
|
||||
trimToUndefined(root?.message);
|
||||
const type = trimToUndefined(subject.type);
|
||||
const code = trimToUndefined(subject.code);
|
||||
const metadata = [type ? `type=${type}` : undefined, code ? `code=${code}` : undefined]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.join(", ");
|
||||
if (message && metadata) {
|
||||
return `${truncateErrorDetail(message)} [${metadata}]`;
|
||||
}
|
||||
if (message) {
|
||||
return truncateErrorDetail(message);
|
||||
}
|
||||
if (metadata) {
|
||||
return `[${metadata}]`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function extractOpenAiErrorDetail(response: Response): Promise<string | undefined> {
|
||||
const rawBody = trimToUndefined(await readResponseTextLimited(response));
|
||||
if (!rawBody) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return formatOpenAiErrorPayload(JSON.parse(rawBody)) ?? truncateErrorDetail(rawBody);
|
||||
} catch {
|
||||
return truncateErrorDetail(rawBody);
|
||||
}
|
||||
return await extractProviderErrorDetail(response);
|
||||
}
|
||||
|
||||
export async function openaiTTS(params: {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ import {
|
|||
adaptScopedAccountAccessor,
|
||||
createScopedChannelConfigAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
import { patchChannelConfigForAccount } from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { inspectSlackAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listSlackAccountIds,
|
||||
|
|
@ -14,118 +11,20 @@ import {
|
|||
resolveSlackAccount,
|
||||
type ResolvedSlackAccount,
|
||||
} from "./accounts.js";
|
||||
import { getChatChannelMeta, type ChannelPlugin, type OpenClawConfig } from "./channel-api.js";
|
||||
import { getChatChannelMeta, type ChannelPlugin } from "./channel-api.js";
|
||||
import { SlackChannelConfigSchema } from "./config-schema.js";
|
||||
import { slackDoctor } from "./doctor.js";
|
||||
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { slackSecurityAdapter } from "./security.js";
|
||||
import { SLACK_CHANNEL } from "./setup-shared.js";
|
||||
|
||||
export const SLACK_CHANNEL = "slack" as const;
|
||||
|
||||
function buildSlackManifest(botName: string) {
|
||||
const safeName = botName.trim() || "OpenClaw";
|
||||
const manifest = {
|
||||
display_information: {
|
||||
name: safeName,
|
||||
description: `${safeName} connector for OpenClaw`,
|
||||
},
|
||||
features: {
|
||||
bot_user: {
|
||||
display_name: safeName,
|
||||
always_online: true,
|
||||
},
|
||||
app_home: {
|
||||
messages_tab_enabled: true,
|
||||
messages_tab_read_only_enabled: false,
|
||||
},
|
||||
slash_commands: [
|
||||
{
|
||||
command: "/openclaw",
|
||||
description: "Send a message to OpenClaw",
|
||||
should_escape: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
oauth_config: {
|
||||
scopes: {
|
||||
bot: [
|
||||
"app_mentions:read",
|
||||
"assistant:write",
|
||||
"channels:history",
|
||||
"channels:read",
|
||||
"chat:write",
|
||||
"commands",
|
||||
"emoji:read",
|
||||
"files:read",
|
||||
"files:write",
|
||||
"groups:history",
|
||||
"groups:read",
|
||||
"im:history",
|
||||
"im:read",
|
||||
"im:write",
|
||||
"mpim:history",
|
||||
"mpim:read",
|
||||
"mpim:write",
|
||||
"pins:read",
|
||||
"pins:write",
|
||||
"reactions:read",
|
||||
"reactions:write",
|
||||
"users:read",
|
||||
],
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
socket_mode_enabled: true,
|
||||
event_subscriptions: {
|
||||
bot_events: [
|
||||
"app_mention",
|
||||
"channel_rename",
|
||||
"member_joined_channel",
|
||||
"member_left_channel",
|
||||
"message.channels",
|
||||
"message.groups",
|
||||
"message.im",
|
||||
"message.mpim",
|
||||
"pin_added",
|
||||
"pin_removed",
|
||||
"reaction_added",
|
||||
"reaction_removed",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
return JSON.stringify(manifest, null, 2);
|
||||
}
|
||||
|
||||
export function buildSlackSetupLines(botName = "OpenClaw"): string[] {
|
||||
return [
|
||||
"1) Slack API -> Create App -> From scratch or From manifest (with the JSON below)",
|
||||
"2) Add Socket Mode + enable it to get the app-level token (xapp-...)",
|
||||
"3) Install App to workspace to get the xoxb- bot token",
|
||||
"4) Enable Event Subscriptions (socket) for message events",
|
||||
"5) App Home -> enable the Messages tab for DMs",
|
||||
"Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.",
|
||||
`Docs: ${formatDocsLink("/slack", "slack")}`,
|
||||
"",
|
||||
"Manifest (JSON):",
|
||||
buildSlackManifest(botName),
|
||||
];
|
||||
}
|
||||
|
||||
export function setSlackChannelAllowlist(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
channelKeys: string[],
|
||||
): OpenClawConfig {
|
||||
const channels = Object.fromEntries(channelKeys.map((key) => [key, { enabled: true }]));
|
||||
return patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel: SLACK_CHANNEL,
|
||||
accountId,
|
||||
patch: { channels },
|
||||
});
|
||||
}
|
||||
export {
|
||||
buildSlackSetupLines,
|
||||
isSlackSetupAccountConfigured,
|
||||
setSlackChannelAllowlist,
|
||||
SLACK_CHANNEL,
|
||||
} from "./setup-shared.js";
|
||||
|
||||
export function isSlackPluginAccountConfigured(account: ResolvedSlackAccount): boolean {
|
||||
const mode = account.config.mode ?? "socket";
|
||||
|
|
@ -139,14 +38,6 @@ export function isSlackPluginAccountConfigured(account: ResolvedSlackAccount): b
|
|||
return Boolean(account.appToken?.trim());
|
||||
}
|
||||
|
||||
export function isSlackSetupAccountConfigured(account: ResolvedSlackAccount): boolean {
|
||||
const hasConfiguredBotToken =
|
||||
Boolean(account.botToken?.trim()) || hasConfiguredSecretInput(account.config.botToken);
|
||||
const hasConfiguredAppToken =
|
||||
Boolean(account.appToken?.trim()) || hasConfiguredSecretInput(account.config.appToken);
|
||||
return hasConfiguredBotToken && hasConfiguredAppToken;
|
||||
}
|
||||
|
||||
export const slackConfigAdapter = createScopedChannelConfigAdapter<ResolvedSlackAccount>({
|
||||
sectionKey: SLACK_CHANNEL,
|
||||
listAccountIds: listSlackAccountIds,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { postJsonRequest } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
asObject,
|
||||
readResponseTextLimited,
|
||||
trimToUndefined,
|
||||
truncateErrorDetail,
|
||||
} from "openclaw/plugin-sdk/speech";
|
||||
import { extractProviderErrorDetail, trimToUndefined } from "openclaw/plugin-sdk/speech";
|
||||
import { XAI_BASE_URL } from "./api.js";
|
||||
export { XAI_BASE_URL };
|
||||
|
||||
|
|
@ -44,43 +39,8 @@ export function normalizeXaiLanguageCode(value: unknown): string | undefined {
|
|||
);
|
||||
}
|
||||
|
||||
function formatXaiErrorPayload(payload: unknown): string | undefined {
|
||||
const root = asObject(payload);
|
||||
const subject = asObject(root?.error) ?? root;
|
||||
if (!subject) {
|
||||
return undefined;
|
||||
}
|
||||
const message =
|
||||
trimToUndefined(subject.message) ??
|
||||
trimToUndefined(subject.detail) ??
|
||||
trimToUndefined(root?.message);
|
||||
const type = trimToUndefined(subject.type);
|
||||
const code = trimToUndefined(subject.code);
|
||||
const metadata = [type ? `type=${type}` : undefined, code ? `code=${code}` : undefined]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.join(", ");
|
||||
if (message && metadata) {
|
||||
return `${truncateErrorDetail(message)} [${metadata}]`;
|
||||
}
|
||||
if (message) {
|
||||
return truncateErrorDetail(message);
|
||||
}
|
||||
if (metadata) {
|
||||
return `[${metadata}]`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function extractXaiErrorDetail(response: Response): Promise<string | undefined> {
|
||||
const rawBody = trimToUndefined(await readResponseTextLimited(response));
|
||||
if (!rawBody) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return formatXaiErrorPayload(JSON.parse(rawBody)) ?? truncateErrorDetail(rawBody);
|
||||
} catch {
|
||||
return truncateErrorDetail(rawBody);
|
||||
}
|
||||
return await extractProviderErrorDetail(response);
|
||||
}
|
||||
|
||||
export async function xaiTTS(params: {
|
||||
|
|
|
|||
|
|
@ -1336,8 +1336,8 @@
|
|||
"docs:list": "node scripts/docs-list.js",
|
||||
"docs:spellcheck": "bash scripts/docs-spellcheck.sh",
|
||||
"docs:spellcheck:fix": "bash scripts/docs-spellcheck.sh --write",
|
||||
"dup:check": "jscpd src extensions test scripts --format typescript,javascript --pattern \"**/*.{ts,tsx,js,mjs,cjs}\" --gitignore --noSymlinks --ignore \"**/node_modules/**,**/dist/**,**/.git/**,**/coverage/**,**/build/**,**/.build/**,**/.artifacts/**\" --min-lines 12 --min-tokens 80 --reporters console",
|
||||
"dup:check:json": "jscpd src extensions test scripts --format typescript,javascript --pattern \"**/*.{ts,tsx,js,mjs,cjs}\" --gitignore --noSymlinks --ignore \"**/node_modules/**,**/dist/**,**/.git/**,**/coverage/**,**/build/**,**/.build/**,**/.artifacts/**\" --min-lines 12 --min-tokens 80 --reporters json --output .artifacts/jscpd",
|
||||
"dup:check": "jscpd src extensions scripts --format typescript,javascript --pattern \"**/*.{ts,tsx,js,mjs,cjs}\" --gitignore --noSymlinks --ignore \"**/*.test.ts,**/*.test.tsx,**/*.test.js,extensions/qa-matrix/src/shared/**,extensions/qa-matrix/src/report.ts,extensions/qa-matrix/src/docker-runtime.ts,extensions/qa-matrix/src/cli-paths.ts,**/node_modules/**,**/dist/**,**/.git/**,**/coverage/**,**/build/**,**/.build/**,**/.artifacts/**\" --min-lines 50 --min-tokens 300 --reporters console",
|
||||
"dup:check:json": "jscpd src extensions scripts --format typescript,javascript --pattern \"**/*.{ts,tsx,js,mjs,cjs}\" --gitignore --noSymlinks --ignore \"**/*.test.ts,**/*.test.tsx,**/*.test.js,extensions/qa-matrix/src/shared/**,extensions/qa-matrix/src/report.ts,extensions/qa-matrix/src/docker-runtime.ts,extensions/qa-matrix/src/cli-paths.ts,**/node_modules/**,**/dist/**,**/.git/**,**/coverage/**,**/build/**,**/.build/**,**/.artifacts/**\" --min-lines 50 --min-tokens 300 --reporters json --output .artifacts/jscpd",
|
||||
"format": "oxfmt --write --threads=1",
|
||||
"format:all": "pnpm format && pnpm format:swift",
|
||||
"format:check": "oxfmt --check --threads=1",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { compile } from "@mdx-js/mdx";
|
||||
import {
|
||||
checkMintlifyAccordionIndentation,
|
||||
MINTLIFY_ACCORDION_INDENT_MESSAGE,
|
||||
} from "./lib/mintlify-accordion.mjs";
|
||||
|
||||
const MINTLIFY_LANGUAGE_CODES = new Set([
|
||||
"en",
|
||||
|
|
@ -120,59 +124,13 @@ function formatMdxError(filePath, error) {
|
|||
}
|
||||
|
||||
function checkMintlifyMdxStructure(filePath, raw) {
|
||||
const errors = [];
|
||||
const lines = stripFrontmatter(raw).split(/\r?\n/u);
|
||||
const accordionStack = [];
|
||||
let inCodeFence = false;
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (/^\s*(```|~~~)/u.test(line)) {
|
||||
inCodeFence = !inCodeFence;
|
||||
continue;
|
||||
}
|
||||
if (inCodeFence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const openAccordion = line.match(/^(\s*)<Accordion\b/u);
|
||||
if (openAccordion) {
|
||||
accordionStack.push({
|
||||
indent: openAccordion[1].length,
|
||||
line: index + 1,
|
||||
hasOutdentedListItem: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const listItem = line.match(/^(\s*)[-*+]\s+/u);
|
||||
if (listItem) {
|
||||
for (const accordion of accordionStack) {
|
||||
if (listItem[1].length < accordion.indent) {
|
||||
accordion.hasOutdentedListItem = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const closeAccordion = line.match(/^(\s*)<\/Accordion>/u);
|
||||
if (!closeAccordion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const opening = accordionStack.pop();
|
||||
if (opening && opening.hasOutdentedListItem && closeAccordion[1].length > opening.indent) {
|
||||
errors.push({
|
||||
type: "mintlify-mdx",
|
||||
file: filePath,
|
||||
line: index + 1,
|
||||
column: closeAccordion[1].length + 1,
|
||||
message:
|
||||
"Accordion closing tag is indented deeper than its opening tag; Mintlify can parse following markdown as nested content.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
return checkMintlifyAccordionIndentation(stripFrontmatter(raw)).map((error) => ({
|
||||
type: "mintlify-mdx",
|
||||
file: filePath,
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
message: MINTLIFY_ACCORDION_INDENT_MESSAGE,
|
||||
}));
|
||||
}
|
||||
|
||||
async function checkMdxFile(filePath) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { execFileSync } from "node:child_process";
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(HERE, "..");
|
||||
|
|
@ -14,6 +15,10 @@ const SYNC_SUPPORT_FILES = [
|
|||
source: path.join(ROOT, "scripts", "check-docs-mdx.mjs"),
|
||||
target: path.join(".openclaw-sync", "check-docs-mdx.mjs"),
|
||||
},
|
||||
{
|
||||
source: path.join(ROOT, "scripts", "lib", "mintlify-accordion.mjs"),
|
||||
target: path.join(".openclaw-sync", "lib", "mintlify-accordion.mjs"),
|
||||
},
|
||||
{
|
||||
source: path.join(ROOT, ".github", "codex", "prompts", "docs-mdx-repair.md"),
|
||||
target: path.join(".openclaw-sync", "docs-mdx-repair.md"),
|
||||
|
|
@ -284,55 +289,6 @@ function composeDocsConfig() {
|
|||
};
|
||||
}
|
||||
|
||||
function repairMintlifyAccordionIndentation(raw) {
|
||||
const lines = raw.split(/\r?\n/u);
|
||||
const accordionStack = [];
|
||||
let inCodeFence = false;
|
||||
let changed = false;
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (/^\s*(```|~~~)/u.test(line)) {
|
||||
inCodeFence = !inCodeFence;
|
||||
continue;
|
||||
}
|
||||
if (inCodeFence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const openAccordion = line.match(/^(\s*)<Accordion\b/u);
|
||||
if (openAccordion) {
|
||||
accordionStack.push({
|
||||
indent: openAccordion[1].length,
|
||||
hasOutdentedListItem: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const listItem = line.match(/^(\s*)[-*+]\s+/u);
|
||||
if (listItem) {
|
||||
for (const accordion of accordionStack) {
|
||||
if (listItem[1].length < accordion.indent) {
|
||||
accordion.hasOutdentedListItem = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const closeAccordion = line.match(/^(\s*)<\/Accordion>/u);
|
||||
if (!closeAccordion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const opening = accordionStack.pop();
|
||||
if (opening && opening.hasOutdentedListItem && closeAccordion[1].length > opening.indent) {
|
||||
lines[index] = `${" ".repeat(opening.indent)}${line.slice(closeAccordion[1].length)}`;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? lines.join("\n") : raw;
|
||||
}
|
||||
|
||||
function repairGeneratedLocaleDocs(targetDocsDir) {
|
||||
let repaired = 0;
|
||||
for (const locale of GENERATED_LOCALES) {
|
||||
|
|
|
|||
73
scripts/lib/mintlify-accordion.mjs
Normal file
73
scripts/lib/mintlify-accordion.mjs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
export const MINTLIFY_ACCORDION_INDENT_MESSAGE =
|
||||
"Accordion closing tag is indented deeper than its opening tag; Mintlify can parse following markdown as nested content.";
|
||||
|
||||
function visitAccordionIndentation(raw, onMisindentedClose) {
|
||||
const lines = raw.split(/\r?\n/u);
|
||||
const accordionStack = [];
|
||||
let inCodeFence = false;
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (/^\s*(```|~~~)/u.test(line)) {
|
||||
inCodeFence = !inCodeFence;
|
||||
continue;
|
||||
}
|
||||
if (inCodeFence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const openAccordion = line.match(/^(\s*)<Accordion\b/u);
|
||||
if (openAccordion) {
|
||||
accordionStack.push({
|
||||
indent: openAccordion[1].length,
|
||||
hasOutdentedListItem: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const listItem = line.match(/^(\s*)[-*+]\s+/u);
|
||||
if (listItem) {
|
||||
for (const accordion of accordionStack) {
|
||||
if (listItem[1].length < accordion.indent) {
|
||||
accordion.hasOutdentedListItem = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const closeAccordion = line.match(/^(\s*)<\/Accordion>/u);
|
||||
if (!closeAccordion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const opening = accordionStack.pop();
|
||||
if (opening && opening.hasOutdentedListItem && closeAccordion[1].length > opening.indent) {
|
||||
onMisindentedClose({ closeAccordion, index, line, lines, opening });
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function checkMintlifyAccordionIndentation(raw) {
|
||||
const errors = [];
|
||||
visitAccordionIndentation(raw, ({ closeAccordion, index }) => {
|
||||
errors.push({
|
||||
line: index + 1,
|
||||
column: closeAccordion[1].length + 1,
|
||||
message: MINTLIFY_ACCORDION_INDENT_MESSAGE,
|
||||
});
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function repairMintlifyAccordionIndentation(raw) {
|
||||
let changed = false;
|
||||
const lines = visitAccordionIndentation(
|
||||
raw,
|
||||
({ closeAccordion, index, line, lines, opening }) => {
|
||||
lines[index] = `${" ".repeat(opening.indent)}${line.slice(closeAccordion[1].length)}`;
|
||||
changed = true;
|
||||
},
|
||||
);
|
||||
return changed ? lines.join("\n") : raw;
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
rmSync,
|
||||
} from "node:fs";
|
||||
import { builtinModules } from "node:module";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join, relative } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
|
@ -25,7 +26,6 @@ import {
|
|||
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
|
||||
import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs";
|
||||
import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
type InstalledPackageJson = {
|
||||
version?: string;
|
||||
|
|
@ -123,7 +123,10 @@ export function normalizeInstalledBinaryVersion(output: string): string {
|
|||
return versionMatch?.[0] ?? trimmed;
|
||||
}
|
||||
|
||||
function listDistJavaScriptFiles(packageRoot: string): string[] {
|
||||
function listDistJavaScriptFiles(
|
||||
packageRoot: string,
|
||||
opts: { skipRelativePath?: (relativePath: string) => boolean } = {},
|
||||
): string[] {
|
||||
const distDir = join(packageRoot, "dist");
|
||||
if (!existsSync(distDir)) {
|
||||
return [];
|
||||
|
|
@ -138,6 +141,10 @@ function listDistJavaScriptFiles(packageRoot: string): string[] {
|
|||
}
|
||||
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
||||
const entryPath = join(currentDir, entry.name);
|
||||
const relativePath = relative(distDir, entryPath).replaceAll("\\", "/");
|
||||
if (opts.skipRelativePath?.(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
pending.push(entryPath);
|
||||
continue;
|
||||
|
|
@ -166,35 +173,9 @@ export function collectInstalledContextEngineRuntimeErrors(packageRoot: string):
|
|||
}
|
||||
|
||||
function listInstalledRootDistJavaScriptFiles(packageRoot: string): string[] {
|
||||
const distDir = join(packageRoot, "dist");
|
||||
if (!existsSync(distDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pending = [distDir];
|
||||
const files: string[] = [];
|
||||
while (pending.length > 0) {
|
||||
const currentDir = pending.pop();
|
||||
if (!currentDir) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
||||
const entryPath = join(currentDir, entry.name);
|
||||
const relativePath = relative(distDir, entryPath).replaceAll("\\", "/");
|
||||
if (relativePath.startsWith("extensions/")) {
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
pending.push(entryPath);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE.test(entry.name)) {
|
||||
files.push(entryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
return listDistJavaScriptFiles(packageRoot, {
|
||||
skipRelativePath: (relativePath) => relativePath.startsWith("extensions/"),
|
||||
});
|
||||
}
|
||||
|
||||
type ParsedImportSpecifiersResult =
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ import { resetConfiguredBindingTargetInPlace } from "../../channels/plugins/bind
|
|||
import { updateSessionStoreEntry } from "../../config/sessions/store.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { isAcpSessionKey } from "../../routing/session-key.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import { resolveBoundAcpThreadSessionKey } from "./commands-acp/targets.js";
|
||||
import { emitResetCommandHooks, type ResetCommandAction } from "./commands-reset-hooks.js";
|
||||
import { parseSoftResetCommand } from "./commands-reset-mode.js";
|
||||
import type { CommandHandlerResult, HandleCommandsParams } from "./commands-types.js";
|
||||
import { isResetAuthorizedForContext } from "./reset-authorization.js";
|
||||
|
||||
function applyAcpResetTailContext(ctx: HandleCommandsParams["ctx"], resetTail: string): void {
|
||||
const mutableCtx = ctx as Record<string, unknown>;
|
||||
|
|
@ -23,26 +22,11 @@ function applyAcpResetTailContext(ctx: HandleCommandsParams["ctx"], resetTail: s
|
|||
}
|
||||
|
||||
function isResetAuthorized(params: HandleCommandsParams): boolean {
|
||||
const auth = resolveCommandAuthorization({
|
||||
return isResetAuthorizedForContext({
|
||||
ctx: params.ctx,
|
||||
cfg: params.cfg,
|
||||
commandAuthorized: params.ctx.CommandAuthorized === true,
|
||||
commandAuthorized: params.command.isAuthorizedSender || params.ctx.CommandAuthorized === true,
|
||||
});
|
||||
if (!params.command.isAuthorizedSender && !auth.isAuthorizedSender) {
|
||||
return false;
|
||||
}
|
||||
const provider = params.ctx.Provider;
|
||||
const internalGatewayCaller = provider
|
||||
? isInternalMessageChannel(provider)
|
||||
: isInternalMessageChannel(params.ctx.Surface);
|
||||
if (!internalGatewayCaller) {
|
||||
return true;
|
||||
}
|
||||
const scopes = params.ctx.GatewayClientScopes;
|
||||
if (!Array.isArray(scopes) || scopes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return scopes.includes("operator.admin");
|
||||
}
|
||||
|
||||
export async function maybeHandleResetCommand(
|
||||
|
|
|
|||
|
|
@ -213,6 +213,30 @@ export async function applyInlineDirectiveOverrides(params: {
|
|||
};
|
||||
}
|
||||
|
||||
const directivePersistenceContext = {
|
||||
directives,
|
||||
effectiveModelDirective,
|
||||
cfg,
|
||||
agentDir,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
storePath,
|
||||
elevatedEnabled,
|
||||
elevatedAllowed,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
aliasIndex,
|
||||
allowedModelKeys: modelState.allowedModelKeys,
|
||||
initialModelLabel,
|
||||
formatModelSwitchEvent,
|
||||
agentCfg,
|
||||
messageProvider: ctx.Provider,
|
||||
surface: ctx.Surface,
|
||||
gatewayClientScopes: ctx.GatewayClientScopes,
|
||||
senderIsOwner: command.senderIsOwner,
|
||||
};
|
||||
|
||||
if (
|
||||
isDirectiveOnly({
|
||||
directives,
|
||||
|
|
@ -251,29 +275,9 @@ export async function applyInlineDirectiveOverrides(params: {
|
|||
const persisted = await (
|
||||
await loadDirectivePersist()
|
||||
).persistInlineDirectives({
|
||||
directives,
|
||||
effectiveModelDirective,
|
||||
cfg,
|
||||
agentDir,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
storePath,
|
||||
elevatedEnabled,
|
||||
elevatedAllowed,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
aliasIndex,
|
||||
allowedModelKeys: modelState.allowedModelKeys,
|
||||
...directivePersistenceContext,
|
||||
provider,
|
||||
model,
|
||||
initialModelLabel,
|
||||
formatModelSwitchEvent,
|
||||
agentCfg,
|
||||
messageProvider: ctx.Provider,
|
||||
surface: ctx.Surface,
|
||||
gatewayClientScopes: ctx.GatewayClientScopes,
|
||||
senderIsOwner: command.senderIsOwner,
|
||||
markLiveSwitchPending: true,
|
||||
});
|
||||
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
||||
|
|
@ -399,29 +403,9 @@ export async function applyInlineDirectiveOverrides(params: {
|
|||
const persisted = await (
|
||||
await loadDirectivePersist()
|
||||
).persistInlineDirectives({
|
||||
directives,
|
||||
effectiveModelDirective,
|
||||
cfg,
|
||||
agentDir,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
storePath,
|
||||
elevatedEnabled,
|
||||
elevatedAllowed,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
aliasIndex,
|
||||
allowedModelKeys: modelState.allowedModelKeys,
|
||||
...directivePersistenceContext,
|
||||
provider,
|
||||
model,
|
||||
initialModelLabel,
|
||||
formatModelSwitchEvent,
|
||||
agentCfg,
|
||||
messageProvider: ctx.Provider,
|
||||
surface: ctx.Surface,
|
||||
gatewayClientScopes: ctx.GatewayClientScopes,
|
||||
senderIsOwner: command.senderIsOwner,
|
||||
});
|
||||
provider = persisted.provider;
|
||||
model = persisted.model;
|
||||
|
|
|
|||
27
src/auto-reply/reply/reset-authorization.ts
Normal file
27
src/auto-reply/reply/reset-authorization.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
|
||||
export function isResetAuthorizedForContext(params: {
|
||||
ctx: MsgContext;
|
||||
cfg: OpenClawConfig;
|
||||
commandAuthorized: boolean;
|
||||
}): boolean {
|
||||
const auth = resolveCommandAuthorization(params);
|
||||
if (!params.commandAuthorized && !auth.isAuthorizedSender) {
|
||||
return false;
|
||||
}
|
||||
const provider = params.ctx.Provider;
|
||||
const internalGatewayCaller = provider
|
||||
? isInternalMessageChannel(provider)
|
||||
: isInternalMessageChannel(params.ctx.Surface);
|
||||
if (!internalGatewayCaller) {
|
||||
return true;
|
||||
}
|
||||
const scopes = params.ctx.GatewayClientScopes;
|
||||
if (!Array.isArray(scopes) || scopes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return scopes.includes("operator.admin");
|
||||
}
|
||||
|
|
@ -45,8 +45,6 @@ import {
|
|||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.shared.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import { normalizeCommandBody } from "../commands-registry.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import { resolveEffectiveResetTargetSessionKey } from "./acp-reset-target.js";
|
||||
|
|
@ -54,6 +52,7 @@ import { parseSoftResetCommand } from "./commands-reset-mode.js";
|
|||
import { resolveConversationBindingContextFromMessage } from "./conversation-binding-input.js";
|
||||
import { normalizeInboundTextNewlines } from "./inbound-text.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import { isResetAuthorizedForContext } from "./reset-authorization.js";
|
||||
import {
|
||||
maybeRetireLegacyMainDeliveryRoute,
|
||||
resolveLastChannelRaw,
|
||||
|
|
@ -164,29 +163,6 @@ export type SessionInitResult = {
|
|||
triggerBodyNormalized: string;
|
||||
};
|
||||
|
||||
function isResetAuthorizedForContext(params: {
|
||||
ctx: MsgContext;
|
||||
cfg: OpenClawConfig;
|
||||
commandAuthorized: boolean;
|
||||
}): boolean {
|
||||
const auth = resolveCommandAuthorization(params);
|
||||
if (!params.commandAuthorized && !auth.isAuthorizedSender) {
|
||||
return false;
|
||||
}
|
||||
const provider = params.ctx.Provider;
|
||||
const internalGatewayCaller = provider
|
||||
? isInternalMessageChannel(provider)
|
||||
: isInternalMessageChannel(params.ctx.Surface);
|
||||
if (!internalGatewayCaller) {
|
||||
return true;
|
||||
}
|
||||
const scopes = params.ctx.GatewayClientScopes;
|
||||
if (!Array.isArray(scopes) || scopes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return scopes.includes("operator.admin");
|
||||
}
|
||||
|
||||
function resolveSessionConversationBindingContext(
|
||||
cfg: OpenClawConfig,
|
||||
ctx: MsgContext,
|
||||
|
|
|
|||
|
|
@ -78,6 +78,35 @@ function shouldSuppressListModel(params: {
|
|||
});
|
||||
}
|
||||
|
||||
function appendVisibleRow(params: {
|
||||
rows: ModelRow[];
|
||||
model: ListRowModel;
|
||||
key: string;
|
||||
context: RowBuilderContext;
|
||||
seenKeys?: Set<string>;
|
||||
allowProviderAvailabilityFallback?: boolean;
|
||||
}): boolean {
|
||||
if (params.seenKeys?.has(params.key)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesRowFilter(params.context.filter, params.model)) {
|
||||
return false;
|
||||
}
|
||||
if (shouldSuppressListModel({ model: params.model, context: params.context })) {
|
||||
return false;
|
||||
}
|
||||
params.rows.push(
|
||||
buildRow({
|
||||
model: params.model,
|
||||
key: params.key,
|
||||
context: params.context,
|
||||
allowProviderAvailabilityFallback: params.allowProviderAvailabilityFallback,
|
||||
}),
|
||||
);
|
||||
params.seenKeys?.add(params.key);
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveConfiguredModelInput(params: {
|
||||
model: Partial<ModelDefinitionConfig>;
|
||||
}): Array<"text" | "image"> {
|
||||
|
|
@ -126,21 +155,8 @@ export function appendDiscoveredRows(params: {
|
|||
});
|
||||
|
||||
for (const model of sorted) {
|
||||
if (shouldSuppressListModel({ model, context: params.context })) {
|
||||
continue;
|
||||
}
|
||||
if (!matchesRowFilter(params.context.filter, model)) {
|
||||
continue;
|
||||
}
|
||||
const key = modelKey(model.provider, model.id);
|
||||
params.rows.push(
|
||||
buildRow({
|
||||
model,
|
||||
key,
|
||||
context: params.context,
|
||||
}),
|
||||
);
|
||||
seenKeys.add(key);
|
||||
appendVisibleRow({ rows: params.rows, model, key, context: params.context, seenKeys });
|
||||
}
|
||||
|
||||
return seenKeys;
|
||||
|
|
@ -159,29 +175,19 @@ export function appendConfiguredProviderRows(params: {
|
|||
continue;
|
||||
}
|
||||
const key = modelKey(provider, configuredModel.id);
|
||||
if (params.seenKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const model = toConfiguredProviderListModel({
|
||||
provider,
|
||||
providerConfig,
|
||||
model: configuredModel,
|
||||
});
|
||||
if (!matchesRowFilter(params.context.filter, model)) {
|
||||
continue;
|
||||
}
|
||||
if (shouldSuppressListModel({ model, context: params.context })) {
|
||||
continue;
|
||||
}
|
||||
params.rows.push(
|
||||
buildRow({
|
||||
model,
|
||||
key,
|
||||
context: params.context,
|
||||
allowProviderAvailabilityFallback: !params.context.discoveredKeys.has(key),
|
||||
}),
|
||||
);
|
||||
params.seenKeys.add(key);
|
||||
appendVisibleRow({
|
||||
rows: params.rows,
|
||||
model,
|
||||
key,
|
||||
context: params.context,
|
||||
seenKeys: params.seenKeys,
|
||||
allowProviderAvailabilityFallback: !params.context.discoveredKeys.has(key),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -201,30 +207,23 @@ export async function appendCatalogSupplementRows(params: {
|
|||
continue;
|
||||
}
|
||||
const key = modelKey(entry.provider, entry.id);
|
||||
if (params.seenKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const model = resolveModelWithRegistry({
|
||||
provider: entry.provider,
|
||||
modelId: entry.id,
|
||||
modelRegistry: params.modelRegistry,
|
||||
cfg: params.context.cfg,
|
||||
});
|
||||
if (!model || !matchesRowFilter(params.context.filter, model)) {
|
||||
if (!model) {
|
||||
continue;
|
||||
}
|
||||
if (shouldSuppressListModel({ model, context: params.context })) {
|
||||
continue;
|
||||
}
|
||||
params.rows.push(
|
||||
buildRow({
|
||||
model,
|
||||
key,
|
||||
context: params.context,
|
||||
allowProviderAvailabilityFallback: !params.context.discoveredKeys.has(key),
|
||||
}),
|
||||
);
|
||||
params.seenKeys.add(key);
|
||||
appendVisibleRow({
|
||||
rows: params.rows,
|
||||
model,
|
||||
key,
|
||||
context: params.context,
|
||||
seenKeys: params.seenKeys,
|
||||
allowProviderAvailabilityFallback: !params.context.discoveredKeys.has(key),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.context.filter.local) {
|
||||
|
|
@ -251,26 +250,19 @@ export async function appendProviderCatalogRows(params: {
|
|||
providerFilter: params.context.filter.provider,
|
||||
staticOnly: params.staticOnly,
|
||||
})) {
|
||||
if (!matchesRowFilter(params.context.filter, model)) {
|
||||
continue;
|
||||
}
|
||||
if (shouldSuppressListModel({ model, context: params.context })) {
|
||||
continue;
|
||||
}
|
||||
const key = modelKey(model.provider, model.id);
|
||||
if (params.seenKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
params.rows.push(
|
||||
buildRow({
|
||||
if (
|
||||
appendVisibleRow({
|
||||
rows: params.rows,
|
||||
model,
|
||||
key,
|
||||
context: params.context,
|
||||
seenKeys: params.seenKeys,
|
||||
allowProviderAvailabilityFallback: !params.context.discoveredKeys.has(key),
|
||||
}),
|
||||
);
|
||||
params.seenKeys.add(key);
|
||||
appended += 1;
|
||||
})
|
||||
) {
|
||||
appended += 1;
|
||||
}
|
||||
}
|
||||
return appended;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ export {
|
|||
asBoolean,
|
||||
asFiniteNumber,
|
||||
asObject,
|
||||
extractProviderErrorDetail,
|
||||
formatProviderErrorPayload,
|
||||
readResponseTextLimited,
|
||||
trimToUndefined,
|
||||
truncateErrorDetail,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export { asFiniteNumber } from "../shared/number-coercion.js";
|
||||
import { normalizeOptionalString as trimToUndefined } from "../shared/string-coerce.js";
|
||||
export { normalizeOptionalString as trimToUndefined } from "../shared/string-coerce.js";
|
||||
|
||||
export function asBoolean(value: unknown): boolean | undefined {
|
||||
|
|
@ -63,3 +64,42 @@ export async function readResponseTextLimited(
|
|||
|
||||
return text;
|
||||
}
|
||||
|
||||
export function formatProviderErrorPayload(payload: unknown): string | undefined {
|
||||
const root = asObject(payload);
|
||||
const subject = asObject(root?.error) ?? root;
|
||||
if (!subject) {
|
||||
return undefined;
|
||||
}
|
||||
const message =
|
||||
trimToUndefined(subject.message) ??
|
||||
trimToUndefined(subject.detail) ??
|
||||
trimToUndefined(root?.message);
|
||||
const type = trimToUndefined(subject.type);
|
||||
const code = trimToUndefined(subject.code);
|
||||
const metadata = [type ? `type=${type}` : undefined, code ? `code=${code}` : undefined]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.join(", ");
|
||||
if (message && metadata) {
|
||||
return `${truncateErrorDetail(message)} [${metadata}]`;
|
||||
}
|
||||
if (message) {
|
||||
return truncateErrorDetail(message);
|
||||
}
|
||||
if (metadata) {
|
||||
return `[${metadata}]`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function extractProviderErrorDetail(response: Response): Promise<string | undefined> {
|
||||
const rawBody = trimToUndefined(await readResponseTextLimited(response));
|
||||
if (!rawBody) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return formatProviderErrorPayload(JSON.parse(rawBody)) ?? truncateErrorDetail(rawBody);
|
||||
} catch {
|
||||
return truncateErrorDetail(rawBody);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue