mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 01:06:06 +00:00
* fix(extensions): make indexed access explicit across channel plugins Transport-payload-safe burn-down: malformed Telegram/Discord/QQ/LINE and sibling channel input keeps existing skip paths; no synthesized fields, no new throws in delivery loops. Zalo escape sentinels preserve literal matches instead of undefined replacements. * fix(extensions): make indexed access explicit across provider and memory plugins Stream and model iteration, tool-block guards, capture guards, and sparse accumulators; singleton model reads carry named invariants. * fix(extensions): make indexed access explicit across tooling plugins, flip the extensions lane Remaining plugins (oc-path, qa-lab, browser, logbook, and siblings) plus the tsconfig.extensions.json flag flip. Cleanup: logbook sampleFrames NaN index at max=1, QA retry clamp at non-positive attempts, dead Canvas probe and OpenShell no-op slice removed, twitch test setup leak excluded from the prod lane. * refactor(plugin-sdk): expose expectDefined via a focused SDK subpath Extensions imported @openclaw/normalization-core directly, crossing the external-plugin packaging boundary (it only worked because the runtime builder bundles undeclared workspace helpers). expect-runtime joins the canonical entrypoints JSON, generated exports, API baseline, docs, and subpath contract test; all 78 extension imports now use the SDK seam. Two scanner-shaped locals renamed for review-bundle hygiene. * chore(plugin-sdk): raise surface budgets for the expect-runtime subpath One new entrypoint with one callable export, added intentionally as the packaging-honest seam for extension invariant helpers.
85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
// Device Pair plugin module implements pair command approve behavior.
|
|
import {
|
|
normalizeLowercaseStringOrEmpty,
|
|
normalizeOptionalString,
|
|
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { approveDevicePairing, listDevicePairing } from "./api.js";
|
|
import { formatPendingRequests } from "./notify.js";
|
|
|
|
type PendingPairingEntry = Awaited<ReturnType<typeof listDevicePairing>>["pending"][number];
|
|
type ApprovePairingResult = Awaited<ReturnType<typeof approveDevicePairing>>;
|
|
type ApprovedPairingEntry = Exclude<ApprovePairingResult, null | { status: "forbidden" }>;
|
|
type ForbiddenPairingEntry = Extract<ApprovePairingResult, { status: "forbidden" }>;
|
|
|
|
function buildMultiplePendingApprovalReply(pending: PendingPairingEntry[]): { text: string } {
|
|
return {
|
|
text:
|
|
`${formatPendingRequests(pending)}\n\n` +
|
|
"Multiple pending requests found. Approve one explicitly:\n" +
|
|
"/pair approve <requestId>\n" +
|
|
"Or approve the most recent:\n" +
|
|
"/pair approve latest",
|
|
};
|
|
}
|
|
|
|
export function selectPendingApprovalRequest(params: {
|
|
pending: PendingPairingEntry[];
|
|
requested?: string;
|
|
}): { pending?: PendingPairingEntry; reply?: { text: string } } {
|
|
const [firstPending, ...remainingPending] = params.pending;
|
|
if (!firstPending) {
|
|
return { reply: { text: "No pending device pairing requests." } };
|
|
}
|
|
|
|
if (!params.requested) {
|
|
return remainingPending.length === 0
|
|
? { pending: firstPending }
|
|
: { reply: buildMultiplePendingApprovalReply(params.pending) };
|
|
}
|
|
|
|
if (normalizeLowercaseStringOrEmpty(params.requested) === "latest") {
|
|
let latest = firstPending;
|
|
for (const pending of remainingPending) {
|
|
if ((pending.ts ?? 0) > (latest.ts ?? 0)) {
|
|
latest = pending;
|
|
}
|
|
}
|
|
return { pending: latest };
|
|
}
|
|
|
|
return {
|
|
pending: params.pending.find((entry) => entry.requestId === params.requested),
|
|
reply: undefined,
|
|
};
|
|
}
|
|
|
|
function formatApprovedPairingReply(approved: ApprovedPairingEntry): { text: string } {
|
|
const label = normalizeOptionalString(approved.device.displayName) || approved.device.deviceId;
|
|
const platform = normalizeOptionalString(approved.device.platform);
|
|
const platformLabel = platform ? ` (${platform})` : "";
|
|
return { text: `✅ Paired ${label}${platformLabel}.` };
|
|
}
|
|
|
|
function formatForbiddenPairingRequirement(approved: ForbiddenPairingEntry): string {
|
|
return approved.scope ?? approved.role ?? "additional approval";
|
|
}
|
|
|
|
export async function approvePendingPairingRequest(params: {
|
|
requestId: string;
|
|
callerScopes?: readonly string[];
|
|
}): Promise<{ text: string }> {
|
|
const approved =
|
|
params.callerScopes === undefined
|
|
? await approveDevicePairing(params.requestId)
|
|
: await approveDevicePairing(params.requestId, { callerScopes: params.callerScopes });
|
|
if (!approved) {
|
|
return { text: "Pairing request not found." };
|
|
}
|
|
if (approved.status === "forbidden") {
|
|
return {
|
|
text: `⚠️ This command requires ${formatForbiddenPairingRequirement(approved)} to approve this pairing request.`,
|
|
};
|
|
}
|
|
return formatApprovedPairingReply(approved);
|
|
}
|