mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
feat(tooling): enforce noUncheckedIndexedAccess in the extensions lane (NUIA phase 4) (#105132)
* 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.
This commit is contained in:
committed by
GitHub
parent
43e138cc66
commit
218dcd815a
@@ -1,2 +1,2 @@
|
||||
de87f2acba61514406fe343f0ee2896a101435203c4ef35af8db707b32d109f6 plugin-sdk-api-baseline.json
|
||||
eb72aa8eb79d0729f12ea7715b0ff098677e06e677050009ba1add89cc357f27 plugin-sdk-api-baseline.jsonl
|
||||
e9eede54e4d95e441f0d8dace534f3c94cbb4cc68523a5cae72914bca1683c42 plugin-sdk-api-baseline.json
|
||||
4eff68c554e5c4740c528ea69be16e1f7079865bb363a4c5bc7a436d3c4d94c8 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -324,6 +324,7 @@ SDK.
|
||||
| Transport readiness waits | `openclaw/plugin-sdk/transport-ready-runtime` |
|
||||
| Secure token helpers | `openclaw/plugin-sdk/secure-random-runtime` |
|
||||
| Bounded async task concurrency | `openclaw/plugin-sdk/concurrency-runtime` |
|
||||
| Required-value assertions for provable invariants | `openclaw/plugin-sdk/expect-runtime` |
|
||||
| Numeric coercion | `openclaw/plugin-sdk/number-runtime` |
|
||||
| Process-local async lock | `openclaw/plugin-sdk/async-lock-runtime` |
|
||||
| File locks | `openclaw/plugin-sdk/file-lock` |
|
||||
|
||||
@@ -319,6 +319,7 @@ usage endpoint failed or returned no usable usage data.
|
||||
| `plugin-sdk/delivery-queue-runtime` | Outbound pending-delivery drain helper |
|
||||
| `plugin-sdk/file-access-runtime` | Safe local-file and media-source path helpers |
|
||||
| `plugin-sdk/heartbeat-runtime` | Heartbeat wake, event, and visibility helpers |
|
||||
| `plugin-sdk/expect-runtime` | Required-value assertion helper for provable runtime invariants |
|
||||
| `plugin-sdk/number-runtime` | Numeric coercion helper |
|
||||
| `plugin-sdk/secure-random-runtime` | Secure token/UUID helpers |
|
||||
| `plugin-sdk/system-event-runtime` | System event queue helpers |
|
||||
|
||||
@@ -142,12 +142,14 @@ export function extractTrustedCodexProjectPaths(configToml: string): string[] {
|
||||
|
||||
const assignment =
|
||||
/^(?<key>"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*(?<value>.+)$/.exec(line);
|
||||
if (!assignment?.groups) {
|
||||
const rawKey = assignment?.groups?.key;
|
||||
const rawValue = assignment?.groups?.value;
|
||||
if (!rawKey || rawValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = parseTomlString(assignment.groups.key) ?? assignment.groups.key;
|
||||
const value = assignment.groups.value.trim();
|
||||
const key = parseTomlString(rawKey) ?? rawKey;
|
||||
const value = rawValue.trim();
|
||||
if (inProjectsTable && /^\{.*\}$/.test(value)) {
|
||||
if (/\btrust_level\s*=\s*["']trusted["']/.test(value) && key) {
|
||||
trusted.add(key);
|
||||
|
||||
@@ -198,13 +198,16 @@ function parseProcessList(stdout: string): AcpxProcessInfo[] {
|
||||
const processes: AcpxProcessInfo[] = [];
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const match = /^\s*(?<pid>\d+)\s+(?<ppid>\d+)\s+(?<command>.+?)\s*$/.exec(line);
|
||||
if (!match?.groups) {
|
||||
const pid = match?.groups?.pid;
|
||||
const ppid = match?.groups?.ppid;
|
||||
const command = match?.groups?.command;
|
||||
if (!pid || !ppid || !command) {
|
||||
continue;
|
||||
}
|
||||
processes.push({
|
||||
pid: Number.parseInt(match.groups.pid, 10),
|
||||
ppid: Number.parseInt(match.groups.ppid, 10),
|
||||
command: match.groups.command,
|
||||
pid: Number.parseInt(pid, 10),
|
||||
ppid: Number.parseInt(ppid, 10),
|
||||
command,
|
||||
});
|
||||
}
|
||||
return processes;
|
||||
|
||||
@@ -399,11 +399,16 @@ function isEnvAssignment(value: string): boolean {
|
||||
}
|
||||
|
||||
function unwrapEnvCommand(parts: string[]): string[] {
|
||||
if (!parts.length || basename(parts[0]) !== "env") {
|
||||
const command = parts.at(0);
|
||||
if (!command || basename(command) !== "env") {
|
||||
return parts;
|
||||
}
|
||||
let index = 1;
|
||||
while (index < parts.length && isEnvAssignment(parts[index])) {
|
||||
while (true) {
|
||||
const part = parts.at(index);
|
||||
if (!part || !isEnvAssignment(part)) {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
return parts.slice(index);
|
||||
|
||||
@@ -337,8 +337,7 @@ function injectBedrockCachePoints(
|
||||
// Bedrock Converse uses lowercase roles ("user" / "assistant").
|
||||
const messages = payload.messages as BedrockMessage[] | undefined;
|
||||
if (Array.isArray(messages) && messages.length > 0) {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
for (const msg of messages.toReversed()) {
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
if (!hasCachePoint(msg.content)) {
|
||||
msg.content.push(point);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "@aws-sdk/client-bedrock-runtime";
|
||||
import { NodeHttpHandler } from "@smithy/node-http-handler";
|
||||
import type { DocumentType } from "@smithy/types";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
adjustMaxTokensForThinking,
|
||||
AssistantMessageEventStream,
|
||||
@@ -50,7 +51,6 @@ import {
|
||||
type ThinkingLevel,
|
||||
type Tool,
|
||||
type ToolCall,
|
||||
type ToolResultMessage,
|
||||
} from "openclaw/plugin-sdk/llm";
|
||||
import {
|
||||
resolveClaudeFable5ModelIdentity,
|
||||
@@ -504,7 +504,7 @@ function handleContentBlockDelta(
|
||||
const newBlock: Block = { type: "text", text: "", index: contentBlockIndex };
|
||||
output.content.push(newBlock);
|
||||
index = blocks.length - 1;
|
||||
block = blocks[index];
|
||||
block = newBlock;
|
||||
stream.push({ type: "text_start", contentIndex: index, partial: output });
|
||||
}
|
||||
if (block.type === "text") {
|
||||
@@ -794,7 +794,7 @@ function convertMessages(
|
||||
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
|
||||
|
||||
for (let i = 0; i < transformedMessages.length; i++) {
|
||||
const m = transformedMessages[i];
|
||||
const m = expectDefined(transformedMessages[i], "message conversion index is in bounds");
|
||||
|
||||
switch (m.role) {
|
||||
case "user": {
|
||||
@@ -917,8 +917,11 @@ function convertMessages(
|
||||
|
||||
// Look ahead for consecutive toolResult messages
|
||||
let j = i + 1;
|
||||
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
|
||||
const nextMsg = transformedMessages[j] as ToolResultMessage;
|
||||
while (true) {
|
||||
const nextMsg = transformedMessages.at(j);
|
||||
if (nextMsg?.role !== "toolResult") {
|
||||
break;
|
||||
}
|
||||
toolResults.push({
|
||||
toolResult: {
|
||||
toolUseId: nextMsg.toolCallId,
|
||||
@@ -949,7 +952,7 @@ function convertMessages(
|
||||
|
||||
// Add cache point to the last user message for supported Claude models when caching is enabled
|
||||
if (cacheRetention !== "none" && supportsPromptCaching(model) && result.length > 0) {
|
||||
const lastMessage = result[result.length - 1];
|
||||
const lastMessage = expectDefined(result.at(-1), "non-empty converted message list");
|
||||
if (lastMessage.role === ConversationRole.USER && lastMessage.content) {
|
||||
lastMessage.content.push({
|
||||
cachePoint: {
|
||||
|
||||
@@ -143,13 +143,17 @@ export function normalizeClaudePermissionArgs(
|
||||
}
|
||||
const normalized: string[] = [];
|
||||
let hasPermissionMode = false;
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
let skipNext = false;
|
||||
for (const [index, arg] of args.entries()) {
|
||||
if (skipNext) {
|
||||
skipNext = false;
|
||||
continue;
|
||||
}
|
||||
if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) {
|
||||
continue;
|
||||
}
|
||||
if (arg === CLAUDE_PERMISSION_MODE_ARG) {
|
||||
const maybeValue = args[i + 1];
|
||||
const maybeValue = args.at(index + 1);
|
||||
if (
|
||||
typeof maybeValue === "string" &&
|
||||
maybeValue.trim().length > 0 &&
|
||||
@@ -160,7 +164,7 @@ export function normalizeClaudePermissionArgs(
|
||||
normalized.push(arg);
|
||||
normalized.push(maybeValue);
|
||||
}
|
||||
i += 1;
|
||||
skipNext = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -189,10 +193,14 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u
|
||||
}
|
||||
const normalized: string[] = [];
|
||||
let hasSettingSources = false;
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
let skipNext = false;
|
||||
for (const [index, arg] of args.entries()) {
|
||||
if (skipNext) {
|
||||
skipNext = false;
|
||||
continue;
|
||||
}
|
||||
if (arg === CLAUDE_SETTING_SOURCES_ARG) {
|
||||
const maybeValue = args[i + 1];
|
||||
const maybeValue = args.at(index + 1);
|
||||
if (
|
||||
typeof maybeValue === "string" &&
|
||||
maybeValue.trim().length > 0 &&
|
||||
@@ -200,7 +208,7 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u
|
||||
) {
|
||||
hasSettingSources = true;
|
||||
normalized.push(arg, CLAUDE_SAFE_SETTING_SOURCES);
|
||||
i += 1;
|
||||
skipNext = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -431,13 +431,11 @@ export async function startGatewayBonjourAdvertiser(
|
||||
|
||||
const hostnameRaw =
|
||||
process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || resolveSystemMdnsHostname() || "openclaw";
|
||||
const hostname = truncateToDnsLabel(
|
||||
hostnameRaw
|
||||
.replace(/\.local$/i, "")
|
||||
.split(".")[0]
|
||||
.trim() || "openclaw",
|
||||
"openclaw",
|
||||
);
|
||||
const hostnameWithoutLocal = hostnameRaw.replace(/\.local$/i, "");
|
||||
const dotIndex = hostnameWithoutLocal.indexOf(".");
|
||||
const labelEnd = dotIndex === -1 ? hostnameWithoutLocal.length : dotIndex;
|
||||
const hostnameLabel = hostnameWithoutLocal.slice(0, labelEnd).trim() || "openclaw";
|
||||
const hostname = truncateToDnsLabel(hostnameLabel, "openclaw");
|
||||
const instanceName =
|
||||
typeof opts.instanceName === "string" && opts.instanceName.trim()
|
||||
? opts.instanceName.trim()
|
||||
|
||||
@@ -167,6 +167,9 @@ function normalizeBraveUiLang(value: string | undefined): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
const [, language, region] = match;
|
||||
if (!language || !region) {
|
||||
return undefined;
|
||||
}
|
||||
return `${normalizeLowercaseStringOrEmpty(language)}-${region.toUpperCase()}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Provides screenshots, target creation, JavaScript evaluation, ARIA/role
|
||||
* snapshots, DOM text, and selector lookup on top of the CDP socket helpers.
|
||||
*/
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
@@ -517,7 +518,7 @@ function buildRoleTree(nodes: RawAXNode[]): { tree: RoleTreeNode[]; roots: numbe
|
||||
continue;
|
||||
}
|
||||
tree[index]?.children.push(childIndex);
|
||||
tree[childIndex].parent = index;
|
||||
expectDefined(tree[childIndex], "CDP child node index").parent = index;
|
||||
childIndexes.add(childIndex);
|
||||
}
|
||||
}
|
||||
@@ -529,7 +530,7 @@ function buildRoleTree(nodes: RawAXNode[]): { tree: RoleTreeNode[]; roots: numbe
|
||||
if (!current) {
|
||||
break;
|
||||
}
|
||||
tree[current.index].depth = current.depth;
|
||||
expectDefined(tree[current.index], "CDP traversal node index").depth = current.depth;
|
||||
for (const child of (tree[current.index]?.children ?? []).toReversed()) {
|
||||
stack.push({ index: child, depth: current.depth + 1 });
|
||||
}
|
||||
@@ -857,7 +858,7 @@ async function buildCdpRoleSnapshot(params: {
|
||||
if (node.backendDOMNodeId && iframeFrameIds.has(node.backendDOMNodeId)) {
|
||||
node.frameId = iframeFrameIds.get(node.backendDOMNodeId);
|
||||
if (node.ref && refs[node.ref]) {
|
||||
refs[node.ref].frameId = node.frameId;
|
||||
expectDefined(refs[node.ref], "owned CDP role reference").frameId = node.frameId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,10 @@ function setDeep(obj: Record<string, unknown>, keys: string[], value: unknown) {
|
||||
}
|
||||
node = node[key] as Record<string, unknown>;
|
||||
}
|
||||
node[keys[keys.length - 1]] = value;
|
||||
const lastKey = keys.at(-1);
|
||||
if (lastKey !== undefined) {
|
||||
node[lastKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function parseHexRgbToSignedArgbInt(hex: string): number | null {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config";
|
||||
/**
|
||||
* Browser profile allocation helpers.
|
||||
*
|
||||
* Validates profile names and allocates CDP ports/colors for newly persisted
|
||||
* browser profiles.
|
||||
*/
|
||||
import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
|
||||
/**
|
||||
* CDP port allocation for browser profiles.
|
||||
@@ -111,7 +112,7 @@ export function allocateColor(usedColors: Set<string>): string {
|
||||
}
|
||||
// All colors used, cycle based on count
|
||||
const index = usedColors.size % PROFILE_COLORS.length;
|
||||
return PROFILE_COLORS[index] ?? PROFILE_COLORS[0];
|
||||
return expectDefined(PROFILE_COLORS[index], "cycled browser color palette index");
|
||||
}
|
||||
|
||||
/** Extract currently used profile colors from profile config. */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Converts ARIA or AI snapshots into compact role/name text with stable refs
|
||||
* and duplicate disambiguation for agent actions.
|
||||
*/
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { CONTENT_ROLES, INTERACTIVE_ROLES, STRUCTURAL_ROLES } from "./snapshot-roles.js";
|
||||
|
||||
@@ -107,7 +108,8 @@ export function finalizeRoleSnapshot<T extends { role: string }>(params: {
|
||||
|
||||
function getIndentLevel(line: string): number {
|
||||
const match = line.match(/^(\s*)/);
|
||||
return match ? Math.floor(match[1].length / 2) : 0;
|
||||
const indent = match?.[1];
|
||||
return indent === undefined ? 0 : Math.floor(indent.length / 2);
|
||||
}
|
||||
|
||||
function matchInteractiveSnapshotLine(
|
||||
@@ -125,6 +127,9 @@ function matchInteractiveSnapshotLine(
|
||||
const roleRaw = match[2];
|
||||
const name = match[3];
|
||||
const suffix = match[4];
|
||||
if (roleRaw === undefined || suffix === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (roleRaw.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
@@ -201,13 +206,20 @@ function compactTree(tree: string) {
|
||||
}
|
||||
current.entry.keep ||= current.entry.hasRef;
|
||||
if (current.entry.hasRef && stack.length > 0) {
|
||||
stack[stack.length - 1].entry.hasRef = true;
|
||||
const parent = stack.at(-1);
|
||||
if (parent !== undefined) {
|
||||
parent.entry.hasRef = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const indent = getIndentLevel(line);
|
||||
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
||||
while (stack.length > 0) {
|
||||
const lastEntry = expectDefined(stack.at(-1), "non-empty role snapshot stack");
|
||||
if (lastEntry.indent < indent) {
|
||||
break;
|
||||
}
|
||||
finishEntry();
|
||||
}
|
||||
const entry = {
|
||||
@@ -246,7 +258,13 @@ function processLine(
|
||||
return options.interactive ? null : line;
|
||||
}
|
||||
|
||||
const [, prefix, roleRaw, name, suffix] = match;
|
||||
const prefix = match[1];
|
||||
const roleRaw = match[2];
|
||||
const name = match[3];
|
||||
const suffix = match[4];
|
||||
if (prefix === undefined || roleRaw === undefined || suffix === undefined) {
|
||||
return options.interactive ? null : line;
|
||||
}
|
||||
if (roleRaw.startsWith("/")) {
|
||||
return options.interactive ? null : line;
|
||||
}
|
||||
@@ -414,10 +432,10 @@ export function buildRoleSnapshotFromAriaSnapshot(
|
||||
function parseAiSnapshotRef(suffix: string): string | null {
|
||||
const eMatch = suffix.match(/\[ref=(e\d+)\]/i);
|
||||
if (eMatch) {
|
||||
return eMatch[1];
|
||||
return eMatch[1] ?? null;
|
||||
}
|
||||
const numMatch = suffix.match(/\[ref=(\d{1,9})\]/);
|
||||
return numMatch ? numMatch[1] : null;
|
||||
return numMatch?.[1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,6 +484,10 @@ export function buildRoleSnapshotFromAiSnapshot(
|
||||
const roleRaw = match[2];
|
||||
const name = match[3];
|
||||
const suffix = match[4];
|
||||
if (roleRaw === undefined || suffix === undefined) {
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
if (roleRaw.startsWith("/")) {
|
||||
out.push(line);
|
||||
continue;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Manages CDP-backed Playwright connections, page lookup, observed dialogs,
|
||||
* console/network/page state, role refs, and safe navigation handling.
|
||||
*/
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
isFutureDateTimestampMs,
|
||||
parseFiniteNumber,
|
||||
@@ -1152,7 +1153,7 @@ function resolvePendingDialogForResponse(params: {
|
||||
throw new Error(`Dialog "${dialogId}" is not pending.`);
|
||||
}
|
||||
if (params.state.pendingDialogs.length === 1) {
|
||||
return params.state.pendingDialogs[0];
|
||||
return expectDefined(params.state.pendingDialogs.at(0), "single pending browser dialog");
|
||||
}
|
||||
if (params.state.pendingDialogs.length > 1) {
|
||||
throw new Error("Multiple dialogs are pending; pass dialogId.");
|
||||
@@ -1488,7 +1489,7 @@ async function getPageForTargetIdOnce(opts: {
|
||||
}
|
||||
throw new Error("No pages available in the connected browser.");
|
||||
}
|
||||
const first = accessible[0];
|
||||
const first = expectDefined(accessible.at(0), "non-empty accessible browser pages");
|
||||
if (!opts.targetId) {
|
||||
bindRoleRefsTarget(first.page, opts.cdpUrl, first.targetId);
|
||||
return first.page;
|
||||
|
||||
@@ -1483,6 +1483,10 @@ export async function screenshotWithLabelsViaPlaywright(opts: {
|
||||
const inputs: RawAnnotationInput[] = [];
|
||||
let bboxFailures = 0;
|
||||
for (const ref of refKeys) {
|
||||
const refInfo = opts.refs[ref];
|
||||
if (refInfo === undefined) {
|
||||
continue;
|
||||
}
|
||||
const box = await refLocator(page, ref)
|
||||
.boundingBox()
|
||||
.catch(() => null);
|
||||
@@ -1492,8 +1496,8 @@ export async function screenshotWithLabelsViaPlaywright(opts: {
|
||||
}
|
||||
inputs.push({
|
||||
ref,
|
||||
role: opts.refs[ref].role,
|
||||
name: opts.refs[ref].name,
|
||||
role: refInfo.role,
|
||||
name: refInfo.name,
|
||||
doc: {
|
||||
x: box.x + scroll.x,
|
||||
y: box.y + scroll.y,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
/**
|
||||
* Browser agent action route registration and existing-session execution.
|
||||
*
|
||||
* Dispatches normalized actions to either Playwright-backed OpenClaw browser
|
||||
* control or Chrome MCP existing-session operations with navigation guards.
|
||||
*/
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import {
|
||||
clickChromeMcpElement,
|
||||
@@ -216,7 +217,9 @@ function buildExistingSessionWaitPredicate(params: {
|
||||
if (checks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return checks.length === 1 ? checks[0] : checks.map((check) => `(${check})`).join(" && ");
|
||||
return checks.length === 1
|
||||
? expectDefined(checks.at(0), "single existing-session condition")
|
||||
: checks.map((check) => `(${check})`).join(" && ");
|
||||
}
|
||||
|
||||
async function waitForExistingSessionCondition(
|
||||
|
||||
@@ -73,15 +73,15 @@ function resolveProfileForTest(
|
||||
state: BrowserServerState,
|
||||
profileName: string,
|
||||
): ResolvedBrowserProfile {
|
||||
const rawProfile = state.resolved.profiles[profileName] ?? {};
|
||||
const rawProfile = state.resolved.profiles[profileName];
|
||||
const cdpPort =
|
||||
typeof rawProfile.cdpPort === "number"
|
||||
typeof rawProfile?.cdpPort === "number"
|
||||
? rawProfile.cdpPort
|
||||
: profileName === "remote"
|
||||
? 9222
|
||||
: state.resolved.cdpPortRangeStart;
|
||||
const cdpUrl =
|
||||
typeof rawProfile.cdpUrl === "string"
|
||||
typeof rawProfile?.cdpUrl === "string"
|
||||
? rawProfile.cdpUrl
|
||||
: `${state.resolved.cdpProtocol}://${state.resolved.cdpHost}:${cdpPort}`;
|
||||
const parsed = new URL(cdpUrl.replace(/^ws/i, "http"));
|
||||
@@ -93,13 +93,13 @@ function resolveProfileForTest(
|
||||
cdpUrl,
|
||||
cdpHost,
|
||||
cdpIsLoopback,
|
||||
color: rawProfile.color ?? state.resolved.color,
|
||||
driver: rawProfile.driver === "existing-session" ? "existing-session" : "openclaw",
|
||||
headless: rawProfile.headless ?? state.resolved.headless,
|
||||
color: rawProfile?.color ?? state.resolved.color,
|
||||
driver: rawProfile?.driver === "existing-session" ? "existing-session" : "openclaw",
|
||||
headless: rawProfile?.headless ?? state.resolved.headless,
|
||||
headlessSource:
|
||||
typeof rawProfile.headless === "boolean" ? "profile" : state.resolved.headlessSource,
|
||||
attachOnly: rawProfile.attachOnly ?? state.resolved.attachOnly,
|
||||
userDataDir: rawProfile.userDataDir,
|
||||
typeof rawProfile?.headless === "boolean" ? "profile" : state.resolved.headlessSource,
|
||||
attachOnly: rawProfile?.attachOnly ?? state.resolved.attachOnly,
|
||||
userDataDir: rawProfile?.userDataDir,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -97,10 +97,10 @@ export async function readKeychainSecret(
|
||||
const raw = Buffer.from(stdout);
|
||||
let start = 0;
|
||||
let end = raw.length;
|
||||
while (start < end && isAsciiWhitespace(raw[start])) {
|
||||
while (start < end && isAsciiWhitespace(raw.readUInt8(start))) {
|
||||
start += 1;
|
||||
}
|
||||
while (end > start && isAsciiWhitespace(raw[end - 1])) {
|
||||
while (end > start && isAsciiWhitespace(raw.readUInt8(end - 1))) {
|
||||
end -= 1;
|
||||
}
|
||||
const secret = Buffer.from(raw.subarray(start, end));
|
||||
|
||||
@@ -118,8 +118,7 @@ export function neutralizeMediaDirectives(text: string): string {
|
||||
}
|
||||
const lines = text.split("\n");
|
||||
let changed = false;
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
for (const [i, line] of lines.entries()) {
|
||||
const leading = line.length - line.trimStart().length;
|
||||
const rest = line.slice(leading);
|
||||
if (/^MEDIA:/i.test(rest)) {
|
||||
|
||||
@@ -225,6 +225,9 @@ export function resolveCanvasHttpPathToLocalPath(
|
||||
return null;
|
||||
}
|
||||
const [rawDocumentId, ...entrySegments] = segments;
|
||||
if (!rawDocumentId) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const documentId = normalizeCanvasDocumentId(rawDocumentId);
|
||||
const normalizedEntrypoint = normalizeLogicalPath(entrySegments.join("/"));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Canvas host server and static-file/live-reload handler implementation.
|
||||
*/
|
||||
import * as fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import http, { type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||
import { createRequire } from "node:module";
|
||||
@@ -246,15 +245,7 @@ async function resolveDocumentCspSandbox(
|
||||
}
|
||||
|
||||
function resolveDefaultCanvasRoot(): string {
|
||||
const candidates = [path.join(resolveStateDir(), "canvas")];
|
||||
const existing = candidates.find((dir) => {
|
||||
try {
|
||||
return fsSync.statSync(dir).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return existing ?? candidates[0];
|
||||
return path.join(resolveStateDir(), "canvas");
|
||||
}
|
||||
|
||||
function resolveDefaultWatchFactory(): ChokidarWatch {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Cerebras model catalog helpers derived from the plugin manifest.
|
||||
*/
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import manifest from "./openclaw.plugin.json" with { type: "json" };
|
||||
@@ -28,5 +29,5 @@ export function buildCerebrasModelDefinition(
|
||||
providerId: "cerebras",
|
||||
catalog: { ...CEREBRAS_MANIFEST_CATALOG, models: [model] },
|
||||
});
|
||||
return providerConfig.models[0];
|
||||
return expectDefined(providerConfig.models.at(0), "normalized Cerebras manifest model");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Codex provider plugin and live app-server model catalog discovery.
|
||||
*/
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/core";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
@@ -33,7 +34,9 @@ const DEFAULT_DISCOVERY_TIMEOUT_MS = 2500;
|
||||
const LIVE_DISCOVERY_ENV = "OPENCLAW_CODEX_DISCOVERY_LIVE";
|
||||
const MODEL_DISCOVERY_PAGE_LIMIT = 100;
|
||||
const CODEX_APP_SERVER_SETUP_METHOD_ID = "app-server";
|
||||
const CODEX_DEFAULT_MODEL_REF = `${CODEX_PROVIDER_ID}/${FALLBACK_CODEX_MODELS[0].id}`;
|
||||
const CODEX_DEFAULT_MODEL_REF = `${CODEX_PROVIDER_ID}/${
|
||||
expectDefined(FALLBACK_CODEX_MODELS[0], "Codex fallback model catalog must not be empty").id
|
||||
}`;
|
||||
const codexCatalogLog = createSubsystemLogger("codex/catalog");
|
||||
const CODEX_REASONING_EFFORTS = [
|
||||
"minimal",
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
wrapToolWithBeforeToolCallHook,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import type { ImageContent, TextContent } from "openclaw/plugin-sdk/llm";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
@@ -387,7 +388,7 @@ function computerFrameImageIdentity(
|
||||
if (images.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const image = images[0];
|
||||
const image = expectDefined(images[0], "single Codex computer frame image");
|
||||
return createHash("sha256")
|
||||
.update(JSON.stringify([image.mimeType, image.data]))
|
||||
.digest("hex");
|
||||
@@ -1139,7 +1140,7 @@ function composeAbortSignals(...signals: Array<AbortSignal | undefined>): AbortS
|
||||
return new AbortController().signal;
|
||||
}
|
||||
if (activeSignals.length === 1) {
|
||||
return activeSignals[0];
|
||||
return expectDefined(activeSignals[0], "single active Codex abort signal");
|
||||
}
|
||||
return AbortSignal.any(activeSignals);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { access } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import type { CodexAppServerStartOptions, CodexManagedCommandOrder } from "./config.js";
|
||||
import { MANAGED_CODEX_APP_SERVER_PACKAGE } from "./version.js";
|
||||
|
||||
@@ -57,7 +58,7 @@ export async function resolveManagedCodexAppServerStartOptions(
|
||||
pathExists,
|
||||
platform,
|
||||
});
|
||||
const commandPath = commandPaths[0];
|
||||
const commandPath = expectDefined(commandPaths[0], "resolved managed Codex command path");
|
||||
const managedFallbackCommandPaths = commandPaths.slice(1);
|
||||
|
||||
return {
|
||||
|
||||
@@ -409,8 +409,10 @@ function sortJsonValue(value: JsonValue): JsonValue {
|
||||
return value.map(sortJsonValue);
|
||||
}
|
||||
const sorted: JsonObject = {};
|
||||
for (const key of Object.keys(value).toSorted()) {
|
||||
sorted[key] = sortJsonValue(value[key]);
|
||||
for (const [key, entry] of Object.entries(value).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
sorted[key] = sortJsonValue(entry);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
@@ -1482,8 +1482,7 @@ function lastChildAssistantMessage(childState: ChildState, turnId: string): stri
|
||||
if (!messages) {
|
||||
return undefined;
|
||||
}
|
||||
for (let index = messages.order.length - 1; index >= 0; index -= 1) {
|
||||
const itemId = messages.order[index];
|
||||
for (const itemId of messages.order.toReversed()) {
|
||||
if (messages.finalMessageIds.has(itemId) && !messages.commentaryIds.has(itemId)) {
|
||||
const text = normalizeOptionalString(messages.texts.get(itemId));
|
||||
if (text) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Parses Codex account rate-limit payloads into user-facing usage summaries,
|
||||
* reset hints, and enriched usage-limit error messages.
|
||||
*/
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
MAX_DATE_TIMESTAMP_MS,
|
||||
resolveExpiresAtMsFromEpochSeconds,
|
||||
@@ -160,7 +161,9 @@ export function summarizeCodexAccountUsage(
|
||||
if (snapshots.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const usageSnapshot = snapshots.find(isCodexLimitSnapshot) ?? snapshots[0];
|
||||
const usageSnapshot =
|
||||
snapshots.find(isCodexLimitSnapshot) ??
|
||||
expectDefined(snapshots[0], "displayable Codex rate-limit snapshot");
|
||||
const blockedSnapshots = snapshots.filter(snapshotHasLimitBlock);
|
||||
const blockingSnapshot =
|
||||
blockedSnapshots.find(isCodexLimitSnapshot) ?? blockedSnapshots[0] ?? undefined;
|
||||
|
||||
@@ -767,8 +767,7 @@ async function startInitializedCodexAppServerClient(params: {
|
||||
const acquireStartedAt = Date.now();
|
||||
const timeoutMs = params.timeoutMs ?? 0;
|
||||
const startOptionsCandidates = resolveManagedFallbackStartOptions(params.startOptions);
|
||||
for (let index = 0; index < startOptionsCandidates.length; index += 1) {
|
||||
const startOptions = startOptionsCandidates[index];
|
||||
for (const [index, startOptions] of startOptionsCandidates.entries()) {
|
||||
const runtimeArtifactModule = params.runtimeArtifactMode
|
||||
? await import("./runtime-artifact.js")
|
||||
: undefined;
|
||||
@@ -905,8 +904,7 @@ function resolveManagedFallbackStartOptions(
|
||||
): CodexAppServerStartOptions[] {
|
||||
const commands = [startOptions.command, ...(startOptions.managedFallbackCommandPaths ?? [])];
|
||||
const candidates: CodexAppServerStartOptions[] = [];
|
||||
for (let index = 0; index < commands.length; index += 1) {
|
||||
const command = commands[index];
|
||||
for (const [index, command] of commands.entries()) {
|
||||
const managedFallbackCommandPaths = commands.slice(index + 1);
|
||||
const candidate = {
|
||||
...startOptions,
|
||||
|
||||
@@ -290,7 +290,7 @@ function resolveLiveAccountProfileId(params: {
|
||||
return (
|
||||
params.order.find((profileId) => {
|
||||
const credential = params.store.profiles[profileId];
|
||||
if (!isChatGptSubscriptionProfile(credential)) {
|
||||
if (!credential || !isChatGptSubscriptionProfile(credential)) {
|
||||
return false;
|
||||
}
|
||||
const profileEmail =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Codex plugin module implements command handlers behavior.
|
||||
import crypto from "node:crypto";
|
||||
import { resolveAgentDir, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
isModelSelectionLocked,
|
||||
MODEL_SELECTION_LOCKED_MESSAGE,
|
||||
@@ -2337,7 +2338,7 @@ function splitArgs(value: string | undefined): string[] {
|
||||
function parseBindArgs(args: string[]): ParsedBindArgs {
|
||||
const parsed: ParsedBindArgs = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
const arg = expectDefined(args[index], "current Codex bind argument");
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
@@ -2389,7 +2390,7 @@ function parseCodexCliSessionsArgs(args: string[]): ParsedCodexCliSessionsArgs {
|
||||
const parsed: ParsedCodexCliSessionsArgs = { filter: "" };
|
||||
const filter: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
const arg = expectDefined(args[index], "current Codex sessions argument");
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
@@ -2429,7 +2430,7 @@ function parseCodexCliSessionsArgs(args: string[]): ParsedCodexCliSessionsArgs {
|
||||
function parseResumeArgs(args: string[]): ParsedResumeArgs {
|
||||
const parsed: ParsedResumeArgs = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
const arg = expectDefined(args[index], "current Codex resume argument");
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import type {
|
||||
OpenClawPluginNodeHostCommand,
|
||||
@@ -594,7 +595,7 @@ async function resolveCodexCliNode(params: {
|
||||
if (usable.length > 1) {
|
||||
throw new Error("Multiple Codex CLI-capable nodes connected. Pass --host <node-id>.");
|
||||
}
|
||||
return usable[0];
|
||||
return expectDefined(usable[0], "single usable Codex CLI node");
|
||||
}
|
||||
|
||||
function parseCodexCliSessionsListResult(raw: unknown): CodexCliSessionsListResult {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { resolveDefaultAgentDir } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core";
|
||||
/**
|
||||
* Compatibility tools for the retired Codex Supervisor plugin.
|
||||
*
|
||||
@@ -6,9 +9,7 @@
|
||||
* continuation belongs to the Codex harness, which installs approval and tool
|
||||
* handlers before it starts or resumes the harness-owned Codex thread.
|
||||
*/
|
||||
import { resolveDefaultAgentDir } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { Type } from "typebox";
|
||||
import {
|
||||
@@ -753,7 +754,7 @@ async function resolveEndpointForThread(params: {
|
||||
}
|
||||
}
|
||||
if (matches.length === 1) {
|
||||
return matches[0];
|
||||
return expectDefined(matches[0], "single matching Codex supervision endpoint");
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
throw new Error(`Codex thread id is ambiguous across endpoints: ${params.threadId}`);
|
||||
@@ -763,8 +764,7 @@ async function resolveEndpointForThread(params: {
|
||||
|
||||
function findInProgressTurnId(thread: Record<string, unknown>): string | undefined {
|
||||
const turns = asRecordArray(thread.turns);
|
||||
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
||||
const turn = turns[index];
|
||||
for (const turn of turns.toReversed()) {
|
||||
if (turn.status === "inProgress" && typeof turn.id === "string") {
|
||||
return turn.id;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Cohere model catalog helpers derived from the plugin manifest.
|
||||
*/
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import manifest from "./openclaw.plugin.json" with { type: "json" };
|
||||
@@ -35,8 +36,9 @@ export function buildCohereCatalogModels(): ModelDefinitionConfig[] {
|
||||
export function buildCohereModelDefinition(
|
||||
model: (typeof COHERE_MODEL_CATALOG)[number],
|
||||
): ModelDefinitionConfig {
|
||||
return buildManifestModelProviderConfig({
|
||||
const providerConfig = buildManifestModelProviderConfig({
|
||||
providerId: "cohere",
|
||||
catalog: { ...COHERE_MANIFEST_CATALOG, models: [model] },
|
||||
}).models[0];
|
||||
});
|
||||
return expectDefined(providerConfig.models.at(0), "normalized Cohere manifest model");
|
||||
}
|
||||
|
||||
@@ -26,20 +26,20 @@ export function selectPendingApprovalRequest(params: {
|
||||
pending: PendingPairingEntry[];
|
||||
requested?: string;
|
||||
}): { pending?: PendingPairingEntry; reply?: { text: string } } {
|
||||
if (params.pending.length === 0) {
|
||||
const [firstPending, ...remainingPending] = params.pending;
|
||||
if (!firstPending) {
|
||||
return { reply: { text: "No pending device pairing requests." } };
|
||||
}
|
||||
|
||||
if (!params.requested) {
|
||||
return params.pending.length === 1
|
||||
? { pending: params.pending[0] }
|
||||
return remainingPending.length === 0
|
||||
? { pending: firstPending }
|
||||
: { reply: buildMultiplePendingApprovalReply(params.pending) };
|
||||
}
|
||||
|
||||
if (normalizeLowercaseStringOrEmpty(params.requested) === "latest") {
|
||||
let latest = params.pending[0];
|
||||
for (let index = 1; index < params.pending.length; index += 1) {
|
||||
const pending = params.pending[index];
|
||||
let latest = firstPending;
|
||||
for (const pending of remainingPending) {
|
||||
if ((pending.ts ?? 0) > (latest.ts ?? 0)) {
|
||||
latest = pending;
|
||||
}
|
||||
|
||||
@@ -1327,13 +1327,10 @@ function assignOtelLogEventAttributes(
|
||||
if (!eventAttributes) {
|
||||
return;
|
||||
}
|
||||
for (const rawKey in eventAttributes) {
|
||||
for (const [rawKey, value] of Object.entries(eventAttributes)) {
|
||||
if (Object.keys(attributes).length >= MAX_OTEL_LOG_ATTRIBUTE_COUNT) {
|
||||
break;
|
||||
}
|
||||
if (!Object.hasOwn(eventAttributes, rawKey)) {
|
||||
continue;
|
||||
}
|
||||
const key = rawKey.trim();
|
||||
if (BLOCKED_OTEL_LOG_ATTRIBUTE_KEYS.has(key)) {
|
||||
continue;
|
||||
@@ -1344,7 +1341,7 @@ function assignOtelLogEventAttributes(
|
||||
if (!OTEL_LOG_RAW_ATTRIBUTE_KEY_RE.test(key)) {
|
||||
continue;
|
||||
}
|
||||
assignOtelLogAttribute(attributes, `openclaw.${key}`, eventAttributes[rawKey]);
|
||||
assignOtelLogAttribute(attributes, `openclaw.${key}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1355,13 +1352,10 @@ function assignOtelSecurityEventAttributes(
|
||||
if (!eventAttributes) {
|
||||
return;
|
||||
}
|
||||
for (const rawKey in eventAttributes) {
|
||||
for (const [rawKey, value] of Object.entries(eventAttributes)) {
|
||||
if (Object.keys(attributes).length >= MAX_OTEL_LOG_ATTRIBUTE_COUNT) {
|
||||
break;
|
||||
}
|
||||
if (!Object.hasOwn(eventAttributes, rawKey)) {
|
||||
continue;
|
||||
}
|
||||
const key = rawKey.trim();
|
||||
if (BLOCKED_OTEL_LOG_ATTRIBUTE_KEYS.has(key)) {
|
||||
continue;
|
||||
@@ -1372,7 +1366,6 @@ function assignOtelSecurityEventAttributes(
|
||||
if (!OTEL_LOG_RAW_ATTRIBUTE_KEY_RE.test(key)) {
|
||||
continue;
|
||||
}
|
||||
const value = eventAttributes[rawKey];
|
||||
assignOtelLogAttribute(
|
||||
attributes,
|
||||
`openclaw.security.attribute.${key}`,
|
||||
|
||||
@@ -30,7 +30,7 @@ export function extractDiscordChannelId(sessionKey?: string | null): string | nu
|
||||
return null;
|
||||
}
|
||||
const match = sessionKey.match(/discord:(?:channel|group):(\d+)/);
|
||||
return match ? match[1] : null;
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function extractDiscordSessionKind(sessionKey?: string | null): "channel" | "group" | "dm" | null {
|
||||
@@ -48,7 +48,7 @@ function extractDiscordSessionKind(sessionKey?: string | null): "channel" | "gro
|
||||
if (raw === "direct") {
|
||||
return "dm";
|
||||
}
|
||||
return raw as "channel" | "group" | "dm";
|
||||
return raw === "channel" || raw === "group" || raw === "dm" ? raw : null;
|
||||
}
|
||||
|
||||
function normalizeDiscordOriginChannelId(value?: string | null): string | null {
|
||||
@@ -61,7 +61,7 @@ function normalizeDiscordOriginChannelId(value?: string | null): string | null {
|
||||
}
|
||||
const prefixed = trimmed.match(/^(?:channel|group):(\d+)$/i);
|
||||
if (prefixed) {
|
||||
return prefixed[1];
|
||||
return prefixed[1] ?? null;
|
||||
}
|
||||
return /^\d+$/.test(trimmed) ? trimmed : null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Discord plugin module implements chunk behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { chunkMarkdownTextWithMode, type ChunkMode } from "openclaw/plugin-sdk/reply-chunking";
|
||||
|
||||
@@ -93,7 +94,7 @@ function clampToCodePointBoundary(text: string, index: number) {
|
||||
|
||||
function findWhitespaceBreak(window: string) {
|
||||
for (let i = window.length - 1; i >= 0; i--) {
|
||||
if (/\s/.test(window[i])) {
|
||||
if (/\s/.test(window.charAt(i))) {
|
||||
// Return the separator index so whitespace stays with the next segment.
|
||||
return i;
|
||||
}
|
||||
@@ -244,7 +245,7 @@ export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}):
|
||||
currentLines += 1;
|
||||
}
|
||||
} else {
|
||||
current = segment;
|
||||
current = expectDefined(segment, "current Discord chunk segment");
|
||||
currentLines = 1;
|
||||
}
|
||||
}
|
||||
@@ -305,7 +306,7 @@ function rebalanceReasoningItalics(source: string, chunks: string[]): string[] {
|
||||
const adjusted = [...chunks];
|
||||
for (let i = 0; i < adjusted.length; i++) {
|
||||
const isLast = i === adjusted.length - 1;
|
||||
const current = adjusted[i];
|
||||
const current = expectDefined(adjusted[i], "Discord chunk adjustment index");
|
||||
|
||||
// Ensure current chunk closes italics so Discord renders it italicized.
|
||||
const needsClosing = !current.trimEnd().endsWith("_");
|
||||
@@ -318,7 +319,7 @@ function rebalanceReasoningItalics(source: string, chunks: string[]): string[] {
|
||||
}
|
||||
|
||||
// Re-open italics on the next chunk if needed.
|
||||
const next = adjusted[i + 1];
|
||||
const next = expectDefined(adjusted[i + 1], "non-final Discord chunk successor");
|
||||
const leadingWhitespaceLen = next.length - next.trimStart().length;
|
||||
const leadingWhitespace = next.slice(0, leadingWhitespaceLen);
|
||||
const nextBody = next.slice(leadingWhitespaceLen);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Discord plugin module implements doctor behavior.
|
||||
import type { ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Discord plugin module implements doctor behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { collectProviderDangerousNameMatchingScopes } from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
@@ -161,14 +162,14 @@ export function collectDiscordNumericIdWarnings(params: {
|
||||
|
||||
const lines: string[] = [];
|
||||
if (repairableHits.length > 0) {
|
||||
const sample = repairableHits[0];
|
||||
const sample = expectDefined(repairableHits.at(0), "non-empty repairable Discord ID hits");
|
||||
lines.push(
|
||||
`- Discord allowlists contain ${repairableHits.length} numeric ${repairableHits.length === 1 ? "entry" : "entries"} (e.g. ${sanitizeForLog(sample.path)}=${sanitizeForLog(String(sample.entry))}).`,
|
||||
`- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`,
|
||||
);
|
||||
}
|
||||
if (blockedHits.length > 0) {
|
||||
const sample = blockedHits[0];
|
||||
const sample = expectDefined(blockedHits.at(0), "non-empty blocked Discord ID hits");
|
||||
lines.push(
|
||||
`- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that cannot be auto-repaired (e.g. ${sanitizeForLog(sample.path)}).`,
|
||||
`- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Discord plugin module implements components.base behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import type { BaseComponentInteraction } from "./interactions.js";
|
||||
|
||||
export type ComponentParserResult = {
|
||||
@@ -13,9 +14,13 @@ export type ComponentData<
|
||||
export type ConditionalComponentOption = (interaction: BaseComponentInteraction) => boolean;
|
||||
|
||||
export function parseCustomId(id: string): ComponentParserResult {
|
||||
const [rawKey, ...parts] = id.split(";");
|
||||
const [rawKeyValue, ...parts] = id.split(";");
|
||||
const rawKey = expectDefined(rawKeyValue, "custom id split first segment");
|
||||
const [keyPart, firstValue] = rawKey.split("=");
|
||||
const key = keyPart.includes(":") ? keyPart.split(":")[0] : keyPart;
|
||||
const definedKeyPart = expectDefined(keyPart, "custom id key segment");
|
||||
const key = definedKeyPart.includes(":")
|
||||
? expectDefined(definedKeyPart.split(":").at(0), "namespaced custom id key")
|
||||
: definedKeyPart;
|
||||
const data: ComponentParserResult["data"] = {};
|
||||
const entries = firstValue === undefined ? parts : [rawKey.slice(key.length + 1), ...parts];
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Discord plugin module implements mentions behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
@@ -38,7 +39,7 @@ export function formatMention(params: {
|
||||
if (values.length !== 1) {
|
||||
throw new Error("formatMention requires exactly one of userId, roleId, or channelId");
|
||||
}
|
||||
const target = values[0];
|
||||
const target = expectDefined(values.at(0), "single Discord mention target");
|
||||
if (target.kind === "user") {
|
||||
return `<@${target.id}>`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Discord plugin module implements message handlerraft preview behavior.
|
||||
import { EmbeddedBlockChunker } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import {
|
||||
type ChannelProgressDraftLine,
|
||||
@@ -10,6 +9,8 @@ import {
|
||||
resolveChannelStreamingSuppressDefaultToolProgressMessages,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Discord plugin module implements message handlerraft preview behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
convertMarkdownTables,
|
||||
stripInlineDirectiveTagsForDelivery,
|
||||
@@ -213,7 +214,7 @@ export function createDiscordDraftPreviewController(params: {
|
||||
if (chunks.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = chunks[0].trim();
|
||||
const trimmed = expectDefined(chunks.at(0), "single Discord preview chunk").trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Discord plugin module implements model picker.state behavior.
|
||||
import { createHash } from "node:crypto";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import type { ModelsProviderData } from "openclaw/plugin-sdk/models-provider-runtime";
|
||||
import { parseStrictInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
@@ -398,9 +399,8 @@ export function computeAlphaBuckets(sortedItems: string[]): DiscordModelPickerBu
|
||||
}
|
||||
|
||||
const firstLetter = (value: string): string => value.charAt(0).toLowerCase();
|
||||
const allSamePrefix = sortedItems.every(
|
||||
(item) => firstLetter(item) === firstLetter(sortedItems[0]),
|
||||
);
|
||||
const firstItem = expectDefined(sortedItems.at(0), "non-empty sorted model picker items");
|
||||
const allSamePrefix = sortedItems.every((item) => firstLetter(item) === firstLetter(firstItem));
|
||||
if (allSamePrefix) {
|
||||
return chunkBucketsByCount(sortedItems);
|
||||
}
|
||||
@@ -418,13 +418,16 @@ export function computeAlphaBuckets(sortedItems: string[]): DiscordModelPickerBu
|
||||
let end = Math.min(sortedItems.length, start + target);
|
||||
// Extend `end` so we don't split a letter group across two buckets.
|
||||
if (end < sortedItems.length) {
|
||||
const last = firstLetter(sortedItems[end - 1]);
|
||||
while (end < sortedItems.length && firstLetter(sortedItems[end]) === last) {
|
||||
const last = firstLetter(expectDefined(sortedItems[end - 1], "bucket end predecessor"));
|
||||
while (
|
||||
end < sortedItems.length &&
|
||||
firstLetter(expectDefined(sortedItems[end], "bucket extension index")) === last
|
||||
) {
|
||||
end += 1;
|
||||
}
|
||||
}
|
||||
const startLetter = firstLetter(sortedItems[start]);
|
||||
const endLetter = firstLetter(sortedItems[end - 1]);
|
||||
const startLetter = firstLetter(expectDefined(sortedItems[start], "bucket start index"));
|
||||
const endLetter = firstLetter(expectDefined(sortedItems[end - 1], "bucket end predecessor"));
|
||||
const id = startLetter === endLetter ? startLetter : `${startLetter}-${endLetter}`;
|
||||
const label =
|
||||
startLetter === endLetter
|
||||
@@ -477,9 +480,12 @@ function resolveBucket(
|
||||
return null;
|
||||
}
|
||||
if (!id) {
|
||||
return buckets[0];
|
||||
return expectDefined(buckets.at(0), "non-empty model picker buckets");
|
||||
}
|
||||
return buckets.find((bucket) => bucket.id === id) ?? buckets[0];
|
||||
return (
|
||||
buckets.find((bucket) => bucket.id === id) ??
|
||||
expectDefined(buckets.at(0), "non-empty model picker buckets")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,10 +103,14 @@ function parseCurrentModelRef(raw?: string): DiscordModelPickerCurrentModelRef |
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const provider = normalizeProviderId(match[1]);
|
||||
const providerText = match[1];
|
||||
const model = match[2];
|
||||
if (providerText === undefined || model === undefined) {
|
||||
return null;
|
||||
}
|
||||
const provider = normalizeProviderId(providerText);
|
||||
// Preserve the model suffix exactly as entered after "/" so select defaults
|
||||
// continue to mirror the stored ref for Discord interactions.
|
||||
const model = match[2];
|
||||
if (!provider || !model) {
|
||||
return null;
|
||||
}
|
||||
@@ -938,8 +942,7 @@ export function renderDiscordModelPickerRecentsView(
|
||||
);
|
||||
|
||||
// Recent model buttons — slot 2+.
|
||||
for (let i = 0; i < dedupedQuickModels.length; i++) {
|
||||
const modelRef = dedupedQuickModels[i];
|
||||
for (const [i, modelRef] of dedupedQuickModels.entries()) {
|
||||
rows.push(
|
||||
new Row([
|
||||
createModelPickerButton({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Discord plugin module implements thread bindings.lifecycle behavior.
|
||||
import { readAcpSessionEntry, type AcpSessionStoreEntry } from "openclaw/plugin-sdk/acp-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Discord plugin module implements thread bindings.lifecycle behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -71,13 +72,16 @@ async function mapWithConcurrency<TItem, TResult>(params: {
|
||||
if (index >= params.items.length) {
|
||||
return;
|
||||
}
|
||||
resultsByIndex.set(index, await params.worker(params.items[index], index));
|
||||
const item = expectDefined(params.items[index], "bounded worker item index");
|
||||
resultsByIndex.set(index, await params.worker(item, index));
|
||||
}
|
||||
};
|
||||
|
||||
const workers = Array.from({ length: Math.min(limit, params.items.length) }, () => runWorker());
|
||||
await Promise.all(workers);
|
||||
return params.items.map((_item, index) => resultsByIndex.get(index)!);
|
||||
return params.items.map((_item, index) =>
|
||||
expectDefined(resultsByIndex.get(index), "completed bounded worker result"),
|
||||
);
|
||||
}
|
||||
|
||||
export function listThreadBindingsForAccount(accountId?: string): ThreadBindingRecord[] {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Discord plugin module implements outbound payload behavior.
|
||||
import {
|
||||
attachChannelToResult,
|
||||
type ChannelOutboundAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-send-result";
|
||||
// Discord plugin module implements outbound payload behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
getReplyPayloadTtsSupplement,
|
||||
resolvePayloadMediaUrls,
|
||||
@@ -101,7 +102,8 @@ export async function sendDiscordOutboundPayload(params: {
|
||||
let deliveredVoice = false;
|
||||
let lastResult: Awaited<ReturnType<DiscordPayloadSendContext["send"]>>;
|
||||
try {
|
||||
lastResult = await sendContext.sendVoice(sendContext.target, mediaUrls[0], {
|
||||
const voiceUrl = expectDefined(mediaUrls.at(0), "non-empty Discord voice media URLs");
|
||||
lastResult = await sendContext.sendVoice(sendContext.target, voiceUrl, {
|
||||
...resolveDiscordDeliveryOptions(ctx, sendContext, voiceReply),
|
||||
});
|
||||
deliveredVoice = true;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Discord plugin module implements send.shared behavior.
|
||||
import { PollLayoutType } from "discord-api-types/payloads/v10";
|
||||
import type { RESTAPIPoll } from "discord-api-types/rest/v10";
|
||||
import type { APIChannel } from "discord-api-types/v10";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Discord plugin module implements send.shared behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { buildOutboundMediaLoadOptions } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { extensionForMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
@@ -378,7 +379,8 @@ async function sendDiscordText(params: DiscordTextSendParams) {
|
||||
return { result, replyToId: chunkReplyTo };
|
||||
};
|
||||
if (chunks.length === 1) {
|
||||
const { result, replyToId } = await sendChunk(chunks[0], true);
|
||||
const chunk = expectDefined(chunks.at(0), "single Discord text chunk");
|
||||
const { result, replyToId } = await sendChunk(chunk, true);
|
||||
await onResult?.(result, "text", replyToId);
|
||||
return { ...result, platformMessageIds: result.id ? [result.id] : [] };
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
parseFfprobeCodecAndSampleRate,
|
||||
runFfmpeg,
|
||||
@@ -168,7 +169,7 @@ async function generateWaveformFromPcm(filePath: string): Promise<string> {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (let j = 0; j < step && i * step + j < samples.length; j++) {
|
||||
sum += Math.abs(samples[i * step + j]);
|
||||
sum += Math.abs(expectDefined(samples.at(i * step + j), "bounded PCM waveform sample"));
|
||||
count++;
|
||||
}
|
||||
const avg = count > 0 ? sum / count : 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Discord plugin module implements manager behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Discord plugin module implements manager behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -1225,7 +1226,10 @@ export class DiscordVoiceManager {
|
||||
if (this.botUserId && remainingLookups === 1) {
|
||||
break;
|
||||
}
|
||||
const guildId = guildIds[(start + offset) % guildIds.length];
|
||||
const guildId = expectDefined(
|
||||
guildIds[(start + offset) % guildIds.length],
|
||||
"voice reconciliation guild index",
|
||||
);
|
||||
const userLimit = this.resolveFollowUserReconcileUserLookupLimit(
|
||||
followedUserIds.length,
|
||||
remainingLookups,
|
||||
@@ -1269,7 +1273,10 @@ export class DiscordVoiceManager {
|
||||
let scanned = 0;
|
||||
let assigned = 0;
|
||||
for (; scanned < guildIds.length && assigned < remainingLookups; scanned += 1) {
|
||||
const guildId = guildIds[(start + scanned) % guildIds.length];
|
||||
const guildId = expectDefined(
|
||||
guildIds[(start + scanned) % guildIds.length],
|
||||
"bot voice reconciliation guild index",
|
||||
);
|
||||
const plan = plansByGuild.get(guildId);
|
||||
if (!plan?.checkedAllUsers) {
|
||||
continue;
|
||||
@@ -1301,10 +1308,15 @@ export class DiscordVoiceManager {
|
||||
return { userIds: followedUserIds, completedCycle: true };
|
||||
}
|
||||
const start = this.followUsersReconcileUserCursors.get(guildId) ?? 0;
|
||||
const selected = Array.from(
|
||||
{ length: limit },
|
||||
(_, offset) => followedUserIds[(start + offset) % followedUserIds.length],
|
||||
);
|
||||
const selected: string[] = [];
|
||||
for (let offset = 0; offset < limit; offset += 1) {
|
||||
selected.push(
|
||||
expectDefined(
|
||||
followedUserIds[(start + offset) % followedUserIds.length],
|
||||
"followed user selection index",
|
||||
),
|
||||
);
|
||||
}
|
||||
const completedCycle = start + selected.length >= followedUserIds.length;
|
||||
this.followUsersReconcileUserCursors.set(
|
||||
guildId,
|
||||
@@ -1464,7 +1476,9 @@ export class DiscordVoiceManager {
|
||||
return null;
|
||||
}
|
||||
const guildAllowed = this.allowedChannels.filter((entry) => entry.guildId === guildId);
|
||||
return guildAllowed.length === 1 ? guildAllowed[0] : null;
|
||||
return guildAllowed.length === 1
|
||||
? expectDefined(guildAllowed.at(0), "single allowed guild voice channel")
|
||||
: null;
|
||||
}
|
||||
|
||||
private enqueueProcessing(entry: VoiceSessionEntry, task: () => Promise<void>) {
|
||||
|
||||
@@ -93,13 +93,12 @@ async function extractPdfContent(
|
||||
try {
|
||||
const images: DocumentExtractedImage[] = [];
|
||||
let remainingPixels = request.maxPixels;
|
||||
for (let index = 0; index < imagePages.length; index += 1) {
|
||||
for (const [index, pageNumber] of imagePages.entries()) {
|
||||
if (remainingPixels <= 0) {
|
||||
break;
|
||||
}
|
||||
const pagesRemaining = imagePages.length - index;
|
||||
const maxPixelsPerPage = Math.max(1, Math.ceil(remainingPixels / pagesRemaining));
|
||||
const pageNumber = imagePages[index];
|
||||
const imageResult = await pdf.extract({
|
||||
mode: "images",
|
||||
pages: [pageNumber],
|
||||
|
||||
@@ -82,13 +82,19 @@ function parseBitableUrl(url: string): { token: string; tableId?: string; isWiki
|
||||
// Wiki format: /wiki/XXXXX?table=YYY
|
||||
const wikiMatch = u.pathname.match(/\/wiki\/([A-Za-z0-9]+)/);
|
||||
if (wikiMatch) {
|
||||
return { token: wikiMatch[1], tableId, isWiki: true };
|
||||
const wikiPathSegment = wikiMatch[1];
|
||||
return wikiPathSegment === undefined
|
||||
? null
|
||||
: { token: wikiPathSegment, tableId, isWiki: true };
|
||||
}
|
||||
|
||||
// Base format: /base/XXXXX?table=YYY
|
||||
const baseMatch = u.pathname.match(/\/base\/([A-Za-z0-9]+)/);
|
||||
if (baseMatch) {
|
||||
return { token: baseMatch[1], tableId, isWiki: false };
|
||||
const basePathSegment = baseMatch[1];
|
||||
return basePathSegment === undefined
|
||||
? null
|
||||
: { token: basePathSegment, tableId, isWiki: false };
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -407,7 +413,7 @@ async function createApp(
|
||||
path: { app_token: appToken },
|
||||
});
|
||||
if (tablesRes.code === 0 && tablesRes.data?.items && tablesRes.data.items.length > 0) {
|
||||
tableId = tablesRes.data.items[0].table_id ?? undefined;
|
||||
tableId = tablesRes.data.items.at(0)?.table_id;
|
||||
if (tableId) {
|
||||
const cleanup = await cleanupNewBitable(client, appToken, tableId, name, log);
|
||||
cleanedRows = cleanup.cleanedRows;
|
||||
|
||||
@@ -1736,10 +1736,14 @@ export async function handleFeishuMessage(params: {
|
||||
}
|
||||
} else {
|
||||
const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
if (results[i].status === "rejected") {
|
||||
for (const [i, result] of results.entries()) {
|
||||
if (result.status === "rejected") {
|
||||
const agentId = broadcastAgents.at(i);
|
||||
if (agentId === undefined) {
|
||||
continue;
|
||||
}
|
||||
log(
|
||||
`feishu[${account.accountId}]: broadcast dispatch failed for agent=${broadcastAgents[i]}: ${String((results[i] as PromiseRejectedResult).reason)}`,
|
||||
`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,9 @@ export function parseFeishuConversationId(params: {
|
||||
const topicSenderMatch = conversationId.match(/^(.+):topic:([^:]+):sender:([^:]+)$/i);
|
||||
if (topicSenderMatch) {
|
||||
const [, chatId, topicId, senderOpenId] = topicSenderMatch;
|
||||
if (chatId === undefined || topicId === undefined || senderOpenId === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
canonicalConversationId: buildFeishuConversationId({
|
||||
chatId,
|
||||
@@ -141,6 +144,9 @@ export function parseFeishuConversationId(params: {
|
||||
const topicMatch = conversationId.match(/^(.+):topic:([^:]+)$/i);
|
||||
if (topicMatch) {
|
||||
const [, chatId, topicId] = topicMatch;
|
||||
if (chatId === undefined || topicId === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
canonicalConversationId: buildFeishuConversationId({
|
||||
chatId,
|
||||
@@ -156,6 +162,9 @@ export function parseFeishuConversationId(params: {
|
||||
const senderMatch = conversationId.match(/^(.+):sender:([^:]+)$/i);
|
||||
if (senderMatch) {
|
||||
const [, chatId, senderOpenId] = senderMatch;
|
||||
if (chatId === undefined || senderOpenId === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
canonicalConversationId: buildFeishuConversationId({
|
||||
chatId,
|
||||
|
||||
@@ -197,8 +197,7 @@ export async function insertBlocksInBatches(
|
||||
// When startIndex == -1 (append to end), each batch appends after the previous.
|
||||
// When startIndex >= 0, each batch starts at startIndex + count of first-level IDs already inserted.
|
||||
let currentIndex = startIndex;
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i];
|
||||
for (const [i, batch] of batches.entries()) {
|
||||
logger?.info?.(
|
||||
`feishu_doc: Inserting batch ${i + 1}/${batches.length} (${batch.blocks.length} blocks)...`,
|
||||
);
|
||||
|
||||
@@ -89,6 +89,9 @@ function parseColorMarkup(content: string): Segment[] {
|
||||
// Tagged segment
|
||||
const tagStr = normalizeLowercaseStringOrEmpty(match[1]);
|
||||
const text = match[2];
|
||||
if (text === undefined) {
|
||||
continue;
|
||||
}
|
||||
const tags = tagStr.split(/\s+/);
|
||||
|
||||
const segment: Segment = { text };
|
||||
|
||||
@@ -126,7 +126,7 @@ function calculateAdaptiveColumnWidths(blocks: FeishuDocxBlock[], tableBlockId:
|
||||
if (cellId) {
|
||||
const content = getCellText(cellId);
|
||||
const length = getWeightedLength(content);
|
||||
maxLengths[col] = Math.max(maxLengths[col], length);
|
||||
maxLengths[col] = Math.max(maxLengths[col] ?? 0, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,8 +168,12 @@ function calculateAdaptiveColumnWidths(blocks: FeishuDocxBlock[], tableBlockId:
|
||||
}
|
||||
|
||||
for (const i of growable) {
|
||||
const add = Math.min(perColumn, MAX_COLUMN_WIDTH - widths[i]);
|
||||
widths[i] += add;
|
||||
const width = widths[i];
|
||||
if (width === undefined) {
|
||||
continue;
|
||||
}
|
||||
const add = Math.min(perColumn, MAX_COLUMN_WIDTH - width);
|
||||
widths[i] = width + add;
|
||||
remaining -= add;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,11 @@ function extractImageUrls(markdown: string): string[] {
|
||||
const urls: string[] = [];
|
||||
let match;
|
||||
while ((match = regex.exec(markdown)) !== null) {
|
||||
const url = match[1].trim();
|
||||
const capturedUrl = match[1];
|
||||
if (capturedUrl === undefined) {
|
||||
continue;
|
||||
}
|
||||
const url = capturedUrl.trim();
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
urls.push(url);
|
||||
}
|
||||
@@ -669,7 +673,7 @@ async function processImages(
|
||||
for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
|
||||
const url = imageUrls[i];
|
||||
const blockId = imageBlocks[i]?.block_id;
|
||||
if (!blockId) {
|
||||
if (!url || !blockId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,13 @@ async function retryBotIdentityProbe(
|
||||
const log = runtime?.log ?? console.log;
|
||||
const error = runtime?.error ?? console.error;
|
||||
|
||||
for (let i = 0; i < BOT_IDENTITY_RETRY_DELAYS_MS.length; i += 1) {
|
||||
const nextDelays = BOT_IDENTITY_RETRY_DELAYS_MS.slice(1)[Symbol.iterator]();
|
||||
for (const [i, delayMs] of BOT_IDENTITY_RETRY_DELAYS_MS.entries()) {
|
||||
if (abortSignal?.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delayElapsed = await waitForAbortableDelay(BOT_IDENTITY_RETRY_DELAYS_MS[i], abortSignal);
|
||||
const delayElapsed = await waitForAbortableDelay(delayMs, abortSignal);
|
||||
if (!delayElapsed) {
|
||||
return;
|
||||
}
|
||||
@@ -50,7 +51,8 @@ async function retryBotIdentityProbe(
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDelay = BOT_IDENTITY_RETRY_DELAYS_MS[i + 1];
|
||||
const nextDelayResult = nextDelays.next();
|
||||
const nextDelay = nextDelayResult.done ? undefined : nextDelayResult.value;
|
||||
error(
|
||||
`feishu[${accountId}]: bot identity background retry ${i + 1}/${BOT_IDENTITY_RETRY_DELAYS_MS.length} failed` +
|
||||
(nextDelay ? `; next attempt in ${nextDelay / 1000}s` : ""),
|
||||
|
||||
@@ -138,8 +138,7 @@ function resolveFeishuDebounceMentions(params: {
|
||||
if (entries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
||||
const entry = entries[index];
|
||||
for (const entry of entries.toReversed()) {
|
||||
if (isMentionForwardRequest(entry, botOpenId)) {
|
||||
return mergeFeishuDebounceMentions([entry]);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ function resolveFeishuRequesterConversation(params: {
|
||||
if (requesterSessionKey) {
|
||||
const existingBindings = manager.listBySessionKey(requesterSessionKey);
|
||||
if (existingBindings.length === 1) {
|
||||
const existing = existingBindings[0];
|
||||
const existing = existingBindings.at(0);
|
||||
if (existing === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: existing.accountId,
|
||||
conversationId: existing.conversationId,
|
||||
@@ -72,7 +75,10 @@ function resolveFeishuRequesterConversation(params: {
|
||||
!entry.parentConversationId,
|
||||
);
|
||||
if (directMatches.length === 1) {
|
||||
const existing = directMatches[0];
|
||||
const existing = directMatches.at(0);
|
||||
if (existing === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: existing.accountId,
|
||||
conversationId: existing.conversationId,
|
||||
@@ -93,7 +99,10 @@ function resolveFeishuRequesterConversation(params: {
|
||||
);
|
||||
});
|
||||
if (matchingTopicBindings.length === 1) {
|
||||
const existing = matchingTopicBindings[0];
|
||||
const existing = matchingTopicBindings.at(0);
|
||||
if (existing === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: existing.accountId,
|
||||
conversationId: existing.conversationId,
|
||||
@@ -111,7 +120,10 @@ function resolveFeishuRequesterConversation(params: {
|
||||
senderScopedTopicBindings.length === 1 &&
|
||||
matchingTopicBindings.length === senderScopedTopicBindings.length
|
||||
) {
|
||||
const existing = senderScopedTopicBindings[0];
|
||||
const existing = senderScopedTopicBindings.at(0);
|
||||
if (existing === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: existing.accountId,
|
||||
conversationId: existing.conversationId,
|
||||
|
||||
@@ -113,7 +113,7 @@ async function preflightDu(dirPath: string, maxBytes: number): Promise<boolean>
|
||||
finish(true);
|
||||
return;
|
||||
}
|
||||
const sizeKb = Number.parseInt(match[1], 10);
|
||||
const sizeKb = Number.parseInt(match[0], 10);
|
||||
finish(sizeKb <= heuristicKb);
|
||||
});
|
||||
du.on("error", () => {
|
||||
|
||||
@@ -367,12 +367,18 @@ export async function persistAllowAlways(input: {
|
||||
);
|
||||
// Use hasOwnProperty so a node with displayName "constructor" doesn't
|
||||
// accidentally hit Object.prototype.constructor and pretend to match.
|
||||
let key = candidates.find((c) => Object.hasOwn(fileTransfer, c));
|
||||
if (!key) {
|
||||
key = assertSafeConfigKey(input.nodeDisplayName ?? input.nodeId);
|
||||
fileTransfer[key] = {};
|
||||
let entry: NodeFilePolicyConfig | undefined;
|
||||
for (const candidate of candidates) {
|
||||
entry = Object.entries(fileTransfer).find(([key]) => key === candidate)?.[1];
|
||||
if (entry) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!entry) {
|
||||
const key = assertSafeConfigKey(input.nodeDisplayName ?? input.nodeId);
|
||||
entry = {};
|
||||
fileTransfer[key] = entry;
|
||||
}
|
||||
const entry = fileTransfer[key];
|
||||
const list = Array.isArray(entry[field]) ? entry[field] : [];
|
||||
if (!list.includes(input.path)) {
|
||||
list.push(input.path);
|
||||
|
||||
@@ -257,9 +257,14 @@ async function preValidateTarball(
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const entryPath = paths[i];
|
||||
const t = typeChars[i];
|
||||
for (const [index, entryPath] of paths.entries()) {
|
||||
const t = typeChars.at(index);
|
||||
if (t === undefined) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `tar -tzf and tar -tzvf disagree on entry count (${paths.length} vs ${typeChars.length}); refusing`,
|
||||
};
|
||||
}
|
||||
if (t === "l" || t === "h") {
|
||||
return { ok: false, reason: `archive contains link entry: ${entryPath}` };
|
||||
}
|
||||
|
||||
@@ -154,8 +154,9 @@ function pickBestModel(available: string[], userModel?: string): string {
|
||||
return preferred;
|
||||
}
|
||||
}
|
||||
if (available.length > 0) {
|
||||
return available[0];
|
||||
const [firstAvailable] = available;
|
||||
if (firstAvailable) {
|
||||
return firstAvailable;
|
||||
}
|
||||
throw new Error("No embedding models available from GitHub Copilot");
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ import {
|
||||
} from "./replay-policy.js";
|
||||
import { wrapCopilotProviderStream } from "./stream.js";
|
||||
|
||||
const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
||||
const COPILOT_ENV_VARS: [string, string, string] = [
|
||||
"COPILOT_GITHUB_TOKEN",
|
||||
"GH_TOKEN",
|
||||
"GITHUB_TOKEN",
|
||||
];
|
||||
const DEFAULT_COPILOT_MODEL = "github-copilot/claude-opus-4.7";
|
||||
const DEFAULT_COPILOT_PROFILE_ID = "github-copilot:github";
|
||||
|
||||
@@ -165,25 +169,30 @@ function applyGithubCopilotDomainToConfig(
|
||||
|
||||
const models = config.models ?? {};
|
||||
const providers = models.providers ?? {};
|
||||
const provider = providers[PROVIDER_ID] ?? {};
|
||||
const params = { ...provider.params } as Record<string, unknown>;
|
||||
const provider = providers[PROVIDER_ID];
|
||||
const params: Record<string, unknown> = {};
|
||||
if (provider?.params) {
|
||||
Object.assign(params, provider.params);
|
||||
}
|
||||
if (isEnterprise) {
|
||||
params.githubDomain = domain;
|
||||
} else {
|
||||
delete params.githubDomain;
|
||||
}
|
||||
const nextProviders = { ...providers };
|
||||
if (provider) {
|
||||
nextProviders[PROVIDER_ID] = { ...provider, params };
|
||||
} else {
|
||||
// Source config accepts partial provider inputs; catalog materialization
|
||||
// supplies baseUrl/models before runtime consumption.
|
||||
Object.assign(nextProviders, { [PROVIDER_ID]: { params } });
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
models: {
|
||||
...models,
|
||||
providers: {
|
||||
...providers,
|
||||
[PROVIDER_ID]: {
|
||||
...provider,
|
||||
params,
|
||||
},
|
||||
},
|
||||
providers: nextProviders,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -368,8 +368,8 @@ export function withGithubCopilotDomainConfig(cfg: OpenClawConfig, domain: strin
|
||||
// `T | undefined`, which exactOptionalPropertyTypes rejects.
|
||||
const models: NonNullable<OpenClawConfig["models"]> = cfg.models ?? {};
|
||||
const providers: NonNullable<typeof models.providers> = models.providers ?? {};
|
||||
const provider: NonNullable<(typeof providers)[string]> = providers["github-copilot"] ?? {};
|
||||
const params = provider.params;
|
||||
const provider = providers["github-copilot"];
|
||||
const params = provider?.params;
|
||||
const isDefault = domain === PUBLIC_GITHUB_COPILOT_DOMAIN;
|
||||
if (isDefault && !(params && "githubDomain" in params)) {
|
||||
return cfg;
|
||||
@@ -380,14 +380,19 @@ export function withGithubCopilotDomainConfig(cfg: OpenClawConfig, domain: strin
|
||||
} else {
|
||||
nextParams.githubDomain = domain;
|
||||
}
|
||||
const nextProviders = { ...providers };
|
||||
if (provider) {
|
||||
nextProviders["github-copilot"] = { ...provider, params: nextParams };
|
||||
} else {
|
||||
// Source config accepts partial provider inputs; catalog materialization
|
||||
// supplies baseUrl/models before runtime consumption.
|
||||
Object.assign(nextProviders, { "github-copilot": { params: nextParams } });
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
models: {
|
||||
...models,
|
||||
providers: {
|
||||
...providers,
|
||||
"github-copilot": { ...provider, params: nextParams },
|
||||
},
|
||||
providers: nextProviders,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createInterface } from "node:readline/promises";
|
||||
import { format } from "node:util";
|
||||
import type { Command } from "commander";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
|
||||
import {
|
||||
clampTimerTimeoutMs,
|
||||
@@ -1317,7 +1318,11 @@ const CRC32_TABLE = new Uint32Array(
|
||||
function crc32(buffer: Buffer): number {
|
||||
let value = 0xffffffff;
|
||||
for (const byte of buffer) {
|
||||
value = CRC32_TABLE[(value ^ byte) & 0xff] ^ (value >>> 8);
|
||||
const tableValue = expectDefined(
|
||||
CRC32_TABLE.at((value ^ byte) & 0xff),
|
||||
"CRC32 lookup table entry",
|
||||
);
|
||||
value = tableValue ^ (value >>> 8);
|
||||
}
|
||||
return (value ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
@@ -131,34 +131,35 @@ export async function resolveChromeNodeInfo(params: {
|
||||
if (requested) {
|
||||
const list = await listGoogleMeetNodes(params.runtime);
|
||||
const matches = list.nodes.filter((node) => matchesRequestedNode(node, requested));
|
||||
if (matches.length === 1) {
|
||||
const [node] = matches;
|
||||
if (isGoogleMeetNode(node)) {
|
||||
return node;
|
||||
}
|
||||
throw new Error(
|
||||
`Configured Google Meet node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node).join("; ")}. Start or reinstall \`openclaw node run\` on that Chrome host, approve pairing, and allow googlemeet.chrome plus browser.proxy.`,
|
||||
);
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
throw new Error(
|
||||
`Configured Google Meet node ${requested} is ambiguous (${matches.length} matches). Pin chromeNode.node to a unique node id, display name, or remote IP.`,
|
||||
);
|
||||
}
|
||||
const [node] = matches;
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`Configured Google Meet node ${requested} was not found. Run \`openclaw nodes status\` and start or approve the Chrome node.`,
|
||||
);
|
||||
}
|
||||
if (isGoogleMeetNode(node)) {
|
||||
return node;
|
||||
}
|
||||
throw new Error(
|
||||
`Configured Google Meet node ${requested} was not found. Run \`openclaw nodes status\` and start or approve the Chrome node.`,
|
||||
`Configured Google Meet node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node).join("; ")}. Start or reinstall \`openclaw node run\` on that Chrome host, approve pairing, and allow googlemeet.chrome plus browser.proxy.`,
|
||||
);
|
||||
}
|
||||
|
||||
const list = await listGoogleMeetNodes(params.runtime, { connected: true });
|
||||
const nodes = list.nodes.filter(isGoogleMeetNode);
|
||||
if (nodes.length === 0) {
|
||||
const [node] = nodes;
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
"No connected Google Meet-capable node with browser proxy. Run `openclaw node run` on the Chrome host with browser proxy enabled, approve pairing, and allow googlemeet.chrome plus browser.proxy.",
|
||||
);
|
||||
}
|
||||
if (nodes.length === 1) {
|
||||
return nodes[0];
|
||||
return node;
|
||||
}
|
||||
throw new Error(
|
||||
"Multiple Google Meet-capable nodes connected. Set plugins.entries.google-meet.config.chromeNode.node.",
|
||||
|
||||
@@ -13,8 +13,7 @@ export const GOOGLE_MAX_INPUT_IMAGES = 10;
|
||||
export const DEFAULT_GOOGLE_VIDEO_MODEL = "veo-3.1-fast-generate-preview";
|
||||
export const GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS = [4, 6, 8] as const;
|
||||
export const GOOGLE_VIDEO_MIN_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[0];
|
||||
export const GOOGLE_VIDEO_MAX_DURATION_SECONDS =
|
||||
GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1];
|
||||
export const GOOGLE_VIDEO_MAX_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[2];
|
||||
|
||||
function isGoogleProviderConfigured(
|
||||
ctx: { agentDir?: string } | VideoGenerationProviderConfiguredContext,
|
||||
|
||||
@@ -266,8 +266,8 @@ async function processMessageWithPipeline(params: {
|
||||
|
||||
let mediaPath: string | undefined;
|
||||
let mediaType: string | undefined;
|
||||
if (attachments.length > 0) {
|
||||
const first = attachments[0];
|
||||
const first = attachments.at(0);
|
||||
if (first) {
|
||||
const attachmentData = await downloadAttachment(first, account, mediaMaxMb, core);
|
||||
if (attachmentData) {
|
||||
mediaPath = attachmentData.path;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Imessage plugin module implements accounts behavior.
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
createAccountListHelpers,
|
||||
@@ -6,6 +5,8 @@ import {
|
||||
resolveMergedAccountConfig,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/account-resolution";
|
||||
// Imessage plugin module implements accounts behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { IMessageAccountConfig } from "./account-types.js";
|
||||
@@ -219,11 +220,12 @@ export function collectIMessageDuplicateAccountSourceWarnings(params: {
|
||||
if (collisions.length < 2) {
|
||||
continue;
|
||||
}
|
||||
const firstCollision = expectDefined(collisions[0], "duplicate iMessage account source");
|
||||
const ownerId = resolveIMessageAccountSourceOwner({
|
||||
cfg: params.cfg,
|
||||
signature: resolveIMessageAccountSourceSignature(collisions[0]),
|
||||
signature: resolveIMessageAccountSourceSignature(firstCollision),
|
||||
});
|
||||
const owner = collisions.find((a) => a.accountId === ownerId) ?? collisions[0];
|
||||
const owner = collisions.find((a) => a.accountId === ownerId) ?? firstCollision;
|
||||
const duplicates = collisions.filter((a) => a.accountId !== owner.accountId);
|
||||
const dupIds = duplicates.map((a) => `"${a.accountId}"`).join(", ");
|
||||
const cliPath = normalizeIMessageCliPath(owner.config.cliPath);
|
||||
|
||||
@@ -414,10 +414,16 @@ function visibleApprovalBindingMatches(
|
||||
const visibleDecisions: ExecApprovalReplyDecision[] = [];
|
||||
for (const line of lines) {
|
||||
const match = line.match(APPROVE_COMMAND_LINE_RE);
|
||||
if (!match || (match[1] !== binding.approvalId && match[1] !== binding.approvalSlug)) {
|
||||
const approvalId = match?.[1];
|
||||
const decisionsText = match?.[2];
|
||||
if (
|
||||
!approvalId ||
|
||||
!decisionsText ||
|
||||
(approvalId !== binding.approvalId && approvalId !== binding.approvalSlug)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const token of match[2].split(/[\s|,]+/)) {
|
||||
for (const token of decisionsText.split(/[\s|,]+/)) {
|
||||
const decision = normalizeApprovalDecision(token);
|
||||
if (decision && !visibleDecisions.includes(decision)) {
|
||||
visibleDecisions.push(decision);
|
||||
@@ -539,13 +545,17 @@ export function extractIMessageApprovalPromptBinding(text: string): {
|
||||
return null;
|
||||
}
|
||||
const approvalId = idHeaderMatch[1];
|
||||
if (!approvalId) {
|
||||
return null;
|
||||
}
|
||||
const allowedDecisions: ExecApprovalReplyDecision[] = [];
|
||||
for (const line of lines) {
|
||||
const match = line.match(APPROVE_COMMAND_LINE_RE);
|
||||
if (!match || match[1] !== approvalId) {
|
||||
const decisionsText = match?.[2];
|
||||
if (!match || match[1] !== approvalId || !decisionsText) {
|
||||
continue;
|
||||
}
|
||||
const decisions = match[2].split(/[\s|,]+/);
|
||||
const decisions = decisionsText.split(/[\s|,]+/);
|
||||
for (const decisionText of decisions) {
|
||||
const decision = normalizeApprovalDecision(decisionText);
|
||||
if (decision && !allowedDecisions.includes(decision)) {
|
||||
|
||||
@@ -233,8 +233,7 @@ export function capFailureRetriesMap(
|
||||
// debugging).
|
||||
entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
||||
const capped: Record<string, number> = {};
|
||||
for (let i = 0; i < entries.length && i < maxSize; i++) {
|
||||
const [guid, count] = entries[i];
|
||||
for (const [guid, count] of entries.slice(0, maxSize)) {
|
||||
capped[guid] = count;
|
||||
if (textEncoder.encode(JSON.stringify(capped)).byteLength > maxBytes) {
|
||||
delete capped[guid];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Imessage plugin module implements the same-sender inbound debounce merge.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type { IMessagePayload } from "./types.js";
|
||||
|
||||
@@ -104,12 +105,12 @@ export function combineIMessagePayloads(payloads: IMessagePayload[]): CoalescedI
|
||||
if (payloads.length === 0) {
|
||||
throw new Error("combineIMessagePayloads: cannot combine empty payloads");
|
||||
}
|
||||
const first = expectDefined(payloads[0], "first iMessage payload to coalesce");
|
||||
if (payloads.length === 1) {
|
||||
return payloads[0];
|
||||
return first;
|
||||
}
|
||||
|
||||
const first = payloads[0];
|
||||
const last = payloads[payloads.length - 1];
|
||||
const last = expectDefined(payloads.at(-1), "last iMessage payload to coalesce");
|
||||
|
||||
// Cap entries: keep first (preserves command/context) + most recent
|
||||
// (preserves latest payload) when a flood exceeds the cap.
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
upsertChannelPairingRequest,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { normalizeScpRemoteHost } from "openclaw/plugin-sdk/host-runtime";
|
||||
import { isInboundPathAllowed, kindFromMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||
@@ -772,7 +773,10 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
};
|
||||
|
||||
if (entries.length === 1) {
|
||||
await dispatchUnit(entries, entries[0].message);
|
||||
await dispatchUnit(
|
||||
entries,
|
||||
expectDefined(entries[0], "single iMessage dispatch entry").message,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Imessage plugin module implements self chat cache behavior.
|
||||
import { createHash } from "node:crypto";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { formatIMessageChatTarget } from "../targets.js";
|
||||
|
||||
type SelfChatCacheKeyParts = {
|
||||
@@ -134,7 +135,10 @@ class DefaultSelfChatCache implements SelfChatCache {
|
||||
this.entryCount > MAX_SELF_CHAT_CACHE_ENTRIES &&
|
||||
this.insertionOrderOffset < this.insertionOrder.length
|
||||
) {
|
||||
const oldest = this.insertionOrder[this.insertionOrderOffset];
|
||||
const oldest = expectDefined(
|
||||
this.insertionOrder[this.insertionOrderOffset],
|
||||
"oldest iMessage self-chat cache entry",
|
||||
);
|
||||
this.insertionOrderOffset += 1;
|
||||
const entries = this.cache.get(oldest.key);
|
||||
if (!entries) {
|
||||
|
||||
@@ -13,6 +13,9 @@ function readVarint(buf: Uint8Array, start: number): Varint | null {
|
||||
|
||||
while (offset < buf.length && shift <= 28) {
|
||||
const byte = buf[offset];
|
||||
if (byte === undefined) {
|
||||
return null;
|
||||
}
|
||||
offset += 1;
|
||||
value |= (byte & 0x7f) << shift;
|
||||
if ((byte & 0x80) === 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Line plugin module implements card command behavior.
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
// Line plugin module implements card command behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
@@ -144,13 +145,14 @@ function parseCardArgs(argsStrInput: string): {
|
||||
const quotedRegex = /"([^"]*?)"/g;
|
||||
let match;
|
||||
while ((match = quotedRegex.exec(argsStr)) !== null) {
|
||||
result.args.push(match[1]);
|
||||
result.args.push(expectDefined(match[1], "quoted card argument capture"));
|
||||
}
|
||||
|
||||
// Extract flags (--key value or --key "value")
|
||||
const flagRegex = /--(\w+)\s+(?:"([^"]*?)"|(\S+))/g;
|
||||
while ((match = flagRegex.exec(argsStr)) !== null) {
|
||||
result.flags[match[1]] = match[2] ?? match[3];
|
||||
const key = expectDefined(match[1], "card flag name capture");
|
||||
result.flags[key] = expectDefined(match[2] ?? match[3], "card flag value capture");
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -511,8 +511,7 @@ export function createDeviceControlCard(params: {
|
||||
for (let i = 0; i < limitedControls.length; i += 2) {
|
||||
const rowButtons: FlexComponent[] = [];
|
||||
|
||||
for (let j = i; j < Math.min(i + 2, limitedControls.length); j++) {
|
||||
const ctrl = limitedControls[j];
|
||||
for (const [offset, ctrl] of limitedControls.slice(i, i + 2).entries()) {
|
||||
const buttonLabel = ctrl.icon ? `${ctrl.icon} ${ctrl.label}` : ctrl.label;
|
||||
|
||||
rowButtons.push({
|
||||
@@ -525,7 +524,7 @@ export function createDeviceControlCard(params: {
|
||||
style: ctrl.style ?? "secondary",
|
||||
flex: 1,
|
||||
height: "sm",
|
||||
margin: j > i ? "md" : undefined,
|
||||
margin: offset > 0 ? "md" : undefined,
|
||||
} as FlexButton);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Line plugin module implements markdown to line behavior.
|
||||
import type { messagingApi } from "@line/bot-sdk";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { uriAction } from "./actions.js";
|
||||
@@ -42,9 +43,9 @@ export function extractMarkdownTables(text: string): {
|
||||
const matches: { fullMatch: string; table: MarkdownTable }[] = [];
|
||||
|
||||
while ((match = MARKDOWN_TABLE_REGEX.exec(text)) !== null) {
|
||||
const fullMatch = match[0];
|
||||
const headerLine = match[1];
|
||||
const bodyLines = match[2];
|
||||
const fullMatch = expectDefined(match[0], "Markdown table match");
|
||||
const headerLine = expectDefined(match[1], "Markdown table header capture");
|
||||
const bodyLines = expectDefined(match[2], "Markdown table body capture");
|
||||
|
||||
const headers = parseTableRow(headerLine);
|
||||
const rows = bodyLines
|
||||
@@ -62,8 +63,7 @@ export function extractMarkdownTables(text: string): {
|
||||
}
|
||||
|
||||
// Remove tables from text in reverse order to preserve indices
|
||||
for (let i = matches.length - 1; i >= 0; i--) {
|
||||
const { fullMatch, table } = matches[i];
|
||||
for (const { fullMatch, table } of matches.toReversed()) {
|
||||
tables.unshift(table);
|
||||
textWithoutTables = textWithoutTables.replace(fullMatch, "");
|
||||
}
|
||||
@@ -203,9 +203,9 @@ export function extractCodeBlocks(text: string): {
|
||||
const matches: { fullMatch: string; block: CodeBlock }[] = [];
|
||||
|
||||
while ((match = MARKDOWN_CODE_BLOCK_REGEX.exec(text)) !== null) {
|
||||
const fullMatch = match[0];
|
||||
const fullMatch = expectDefined(match[0], "Markdown code block match");
|
||||
const language = match[1] || undefined;
|
||||
const code = match[2];
|
||||
const code = expectDefined(match[2], "Markdown code body capture");
|
||||
|
||||
matches.push({
|
||||
fullMatch,
|
||||
@@ -214,8 +214,7 @@ export function extractCodeBlocks(text: string): {
|
||||
}
|
||||
|
||||
// Remove code blocks in reverse order
|
||||
for (let i = matches.length - 1; i >= 0; i--) {
|
||||
const { fullMatch, block } = matches[i];
|
||||
for (const { fullMatch, block } of matches.toReversed()) {
|
||||
codeBlocks.unshift(block);
|
||||
textWithoutCode = textWithoutCode.replace(fullMatch, "");
|
||||
}
|
||||
@@ -286,8 +285,8 @@ export function extractLinks(text: string): { links: MarkdownLink[]; textWithLin
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = MARKDOWN_LINK_REGEX.exec(text)) !== null) {
|
||||
links.push({
|
||||
text: match[1],
|
||||
url: match[2],
|
||||
text: expectDefined(match[1], "Markdown link text capture"),
|
||||
url: expectDefined(match[2], "Markdown link URL capture"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -237,11 +237,11 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
|
||||
}
|
||||
|
||||
if (chunks.length > 0) {
|
||||
for (let i = 0; i < chunks.length; i += 1) {
|
||||
for (const [i, chunk] of chunks.entries()) {
|
||||
const isLast = i === chunks.length - 1;
|
||||
if (isLast && hasQuickReplies) {
|
||||
await recordResult(
|
||||
sendQuickReplies(to, chunks[i], quickReplies, {
|
||||
sendQuickReplies(to, chunk, quickReplies, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
@@ -249,7 +249,7 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
|
||||
);
|
||||
} else {
|
||||
await recordResult(
|
||||
sendText(to, chunks[i], {
|
||||
sendText(to, chunk, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Line plugin module implements reply chunks behavior.
|
||||
import type { messagingApi } from "@line/bot-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
|
||||
type LineReplyMessage = messagingApi.TextMessage;
|
||||
|
||||
@@ -35,7 +36,7 @@ export type SendLineReplyChunksParams = {
|
||||
export async function sendLineReplyChunks(
|
||||
params: SendLineReplyChunksParams,
|
||||
): Promise<{ replyTokenUsed: boolean }> {
|
||||
const hasQuickReplies = Boolean(params.quickReplies?.length);
|
||||
const quickReplies = params.quickReplies?.length ? params.quickReplies : undefined;
|
||||
let replyTokenUsed = Boolean(params.replyTokenUsed);
|
||||
|
||||
if (params.chunks.length === 0) {
|
||||
@@ -52,11 +53,11 @@ export async function sendLineReplyChunks(
|
||||
text: chunk,
|
||||
}));
|
||||
|
||||
if (hasQuickReplies && remaining.length === 0 && replyMessages.length > 0) {
|
||||
if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
|
||||
const lastIndex = replyMessages.length - 1;
|
||||
replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(
|
||||
replyBatch[lastIndex],
|
||||
params.quickReplies!,
|
||||
expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"),
|
||||
quickReplies,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,17 +67,15 @@ export async function sendLineReplyChunks(
|
||||
});
|
||||
replyTokenUsed = true;
|
||||
|
||||
for (let i = 0; i < remaining.length; i += 1) {
|
||||
for (const [i, chunk] of remaining.entries()) {
|
||||
const isLastChunk = i === remaining.length - 1;
|
||||
if (isLastChunk && hasQuickReplies) {
|
||||
await params.pushTextMessageWithQuickReplies(
|
||||
params.to,
|
||||
remaining[i],
|
||||
params.quickReplies!,
|
||||
{ cfg: params.cfg, accountId: params.accountId },
|
||||
);
|
||||
if (isLastChunk && quickReplies) {
|
||||
await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
} else {
|
||||
await params.pushMessageLine(params.to, remaining[i], {
|
||||
await params.pushMessageLine(params.to, chunk, {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
@@ -90,17 +89,15 @@ export async function sendLineReplyChunks(
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < params.chunks.length; i += 1) {
|
||||
for (const [i, chunk] of params.chunks.entries()) {
|
||||
const isLastChunk = i === params.chunks.length - 1;
|
||||
if (isLastChunk && hasQuickReplies) {
|
||||
await params.pushTextMessageWithQuickReplies(
|
||||
params.to,
|
||||
params.chunks[i],
|
||||
params.quickReplies!,
|
||||
{ cfg: params.cfg, accountId: params.accountId },
|
||||
);
|
||||
if (isLastChunk && quickReplies) {
|
||||
await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
} else {
|
||||
await params.pushMessageLine(params.to, params.chunks[i], {
|
||||
await params.pushMessageLine(params.to, chunk, {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Line plugin module implements reply payload transform behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import {
|
||||
@@ -51,10 +52,21 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
}
|
||||
return base.join("&");
|
||||
};
|
||||
const parseConfirmAction = (part: string): { label: string; data: string } => {
|
||||
const colonIndex = part.indexOf(":");
|
||||
if (colonIndex === -1) {
|
||||
return { label: part, data: normalizeLowercaseStringOrEmpty(part) };
|
||||
}
|
||||
return {
|
||||
label: part.slice(0, colonIndex).trim(),
|
||||
data: part.slice(colonIndex + 1).trim(),
|
||||
};
|
||||
};
|
||||
|
||||
const quickRepliesMatch = text.match(/\[\[quick_replies:\s*([^\]]+)\]\]/i);
|
||||
if (quickRepliesMatch) {
|
||||
const options = normalizeStringEntries(quickRepliesMatch[1].split(","));
|
||||
const body = expectDefined(quickRepliesMatch[1], "quick replies directive body");
|
||||
const options = normalizeStringEntries(body.split(","));
|
||||
if (options.length > 0) {
|
||||
lineData.quickReplies = [...(lineData.quickReplies || []), ...options];
|
||||
}
|
||||
@@ -63,9 +75,13 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
|
||||
const locationMatch = text.match(/\[\[location:\s*([^\]]+)\]\]/i);
|
||||
if (locationMatch && !lineData.location) {
|
||||
const parts = locationMatch[1].split("|").map((s) => s.trim());
|
||||
const body = expectDefined(locationMatch[1], "location directive body");
|
||||
const parts = body.split("|").map((s) => s.trim());
|
||||
if (parts.length >= 4) {
|
||||
const [title, address, latStr, lonStr] = parts;
|
||||
const title = expectDefined(parts[0], "location title field");
|
||||
const address = expectDefined(parts[1], "location address field");
|
||||
const latStr = expectDefined(parts[2], "location latitude field");
|
||||
const lonStr = expectDefined(parts[3], "location longitude field");
|
||||
const latitude = parseStrictFiniteNumber(latStr);
|
||||
const longitude = parseStrictFiniteNumber(lonStr);
|
||||
if (latitude !== undefined && longitude !== undefined) {
|
||||
@@ -82,23 +98,22 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
|
||||
const confirmMatch = text.match(/\[\[confirm:\s*([^\]]+)\]\]/i);
|
||||
if (confirmMatch && !lineData.templateMessage) {
|
||||
const parts = confirmMatch[1].split("|").map((s) => s.trim());
|
||||
const body = expectDefined(confirmMatch[1], "confirm directive body");
|
||||
const parts = body.split("|").map((s) => s.trim());
|
||||
if (parts.length >= 3) {
|
||||
const [question, yesPart, noPart] = parts;
|
||||
const [yesLabel, yesData] = yesPart.includes(":")
|
||||
? yesPart.split(":").map((s) => s.trim())
|
||||
: [yesPart, normalizeLowercaseStringOrEmpty(yesPart)];
|
||||
const [noLabel, noData] = noPart.includes(":")
|
||||
? noPart.split(":").map((s) => s.trim())
|
||||
: [noPart, normalizeLowercaseStringOrEmpty(noPart)];
|
||||
const question = expectDefined(parts[0], "confirm question field");
|
||||
const yesPart = expectDefined(parts[1], "confirm yes field");
|
||||
const noPart = expectDefined(parts[2], "confirm no field");
|
||||
const yesAction = parseConfirmAction(yesPart);
|
||||
const noAction = parseConfirmAction(noPart);
|
||||
|
||||
lineData.templateMessage = {
|
||||
type: "confirm",
|
||||
text: question,
|
||||
confirmLabel: yesLabel,
|
||||
confirmData: yesData,
|
||||
cancelLabel: noLabel,
|
||||
cancelData: noData,
|
||||
confirmLabel: yesAction.label,
|
||||
confirmData: yesAction.data,
|
||||
cancelLabel: noAction.label,
|
||||
cancelData: noAction.data,
|
||||
altText: question,
|
||||
};
|
||||
}
|
||||
@@ -107,9 +122,12 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
|
||||
const buttonsMatch = text.match(/\[\[buttons:\s*([^\]]+)\]\]/i);
|
||||
if (buttonsMatch && !lineData.templateMessage) {
|
||||
const parts = buttonsMatch[1].split("|").map((s) => s.trim());
|
||||
const body = expectDefined(buttonsMatch[1], "buttons directive body");
|
||||
const parts = body.split("|").map((s) => s.trim());
|
||||
if (parts.length >= 3) {
|
||||
const [title, bodyText, actionsStr] = parts;
|
||||
const title = expectDefined(parts[0], "buttons title field");
|
||||
const bodyText = expectDefined(parts[1], "buttons text field");
|
||||
const actionsStr = expectDefined(parts[2], "buttons actions field");
|
||||
|
||||
const actions = actionsStr.split(",").map((actionStr) => {
|
||||
const trimmed = actionStr.trim();
|
||||
@@ -160,9 +178,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
|
||||
const mediaPlayerMatch = text.match(/\[\[media_player:\s*([^\]]+)\]\]/i);
|
||||
if (mediaPlayerMatch && !lineData.flexMessage) {
|
||||
const parts = mediaPlayerMatch[1].split("|").map((s) => s.trim());
|
||||
const body = expectDefined(mediaPlayerMatch[1], "media player directive body");
|
||||
const parts = body.split("|").map((s) => s.trim());
|
||||
if (parts.length >= 1) {
|
||||
const [title, artist, source, imageUrl, statusStr] = parts;
|
||||
const title = expectDefined(parts[0], "media player title field");
|
||||
const [, artist, source, imageUrl, statusStr] = parts;
|
||||
const isPlaying = normalizeLowercaseStringOrEmpty(statusStr) === "playing";
|
||||
const validImageUrl = imageUrl?.startsWith("https://") ? imageUrl : undefined;
|
||||
const deviceKey = toSlug(source || title || "media");
|
||||
@@ -190,9 +210,14 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
|
||||
const eventMatch = text.match(/\[\[event:\s*([^\]]+)\]\]/i);
|
||||
if (eventMatch && !lineData.flexMessage) {
|
||||
const parts = eventMatch[1].split("|").map((s) => s.trim());
|
||||
const body = expectDefined(eventMatch[1], "event directive body");
|
||||
const parts = body.split("|").map((s) => s.trim());
|
||||
if (parts.length >= 2) {
|
||||
const [title, date, time, location, description] = parts;
|
||||
const title = expectDefined(parts[0], "event title field");
|
||||
const date = expectDefined(parts[1], "event date field");
|
||||
const time = parts[2];
|
||||
const location = parts[3];
|
||||
const description = parts[4];
|
||||
|
||||
const card = createEventCard({
|
||||
title: title || "Event",
|
||||
@@ -212,9 +237,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
|
||||
const appleTvMatch = text.match(/\[\[appletv_remote:\s*([^\]]+)\]\]/i);
|
||||
if (appleTvMatch && !lineData.flexMessage) {
|
||||
const parts = appleTvMatch[1].split("|").map((s) => s.trim());
|
||||
const body = expectDefined(appleTvMatch[1], "Apple TV directive body");
|
||||
const parts = body.split("|").map((s) => s.trim());
|
||||
if (parts.length >= 1) {
|
||||
const [deviceName, status] = parts;
|
||||
const deviceName = expectDefined(parts[0], "Apple TV device name field");
|
||||
const [, status] = parts;
|
||||
const deviceKey = toSlug(deviceName || "apple_tv");
|
||||
|
||||
const card = createAppleTvRemoteCard({
|
||||
@@ -246,9 +273,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
|
||||
const agendaMatch = text.match(/\[\[agenda:\s*([^\]]+)\]\]/i);
|
||||
if (agendaMatch && !lineData.flexMessage) {
|
||||
const parts = agendaMatch[1].split("|").map((s) => s.trim());
|
||||
const body = expectDefined(agendaMatch[1], "agenda directive body");
|
||||
const parts = body.split("|").map((s) => s.trim());
|
||||
if (parts.length >= 2) {
|
||||
const [title, eventsStr] = parts;
|
||||
const title = expectDefined(parts[0], "agenda title field");
|
||||
const eventsStr = expectDefined(parts[1], "agenda events field");
|
||||
const events = eventsStr.split(",").map((eventStr) => {
|
||||
const trimmed = eventStr.trim();
|
||||
const colonIdx = trimmed.lastIndexOf(":");
|
||||
@@ -276,13 +305,17 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
|
||||
const deviceMatch = text.match(/\[\[device:\s*([^\]]+)\]\]/i);
|
||||
if (deviceMatch && !lineData.flexMessage) {
|
||||
const parts = deviceMatch[1].split("|").map((s) => s.trim());
|
||||
const body = expectDefined(deviceMatch[1], "device directive body");
|
||||
const parts = body.split("|").map((s) => s.trim());
|
||||
if (parts.length >= 1) {
|
||||
const [deviceName, deviceType, status, controlsStr] = parts;
|
||||
const deviceName = expectDefined(parts[0], "device name field");
|
||||
const [, deviceType, status, controlsStr] = parts;
|
||||
const deviceKey = toSlug(deviceName || "device");
|
||||
const controls = controlsStr
|
||||
? controlsStr.split(",").map((ctrlStr) => {
|
||||
const [label, data] = ctrlStr.split(":").map((s) => s.trim());
|
||||
const controlParts = ctrlStr.split(":").map((s) => s.trim());
|
||||
const label = expectDefined(controlParts[0], "device control label");
|
||||
const data = controlParts[1];
|
||||
const action = data || normalizeLowercaseStringOrEmpty(label).replace(/\s+/g, "_");
|
||||
return { label, data: lineActionData(action, { "line.device": deviceKey }) };
|
||||
})
|
||||
|
||||
@@ -303,11 +303,15 @@ function normalizeConfiguredReasoningEffortMap(value: unknown): Record<string, s
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.map(([key, mapped]) => [key.trim(), typeof mapped === "string" ? mapped.trim() : ""])
|
||||
.filter(([key, mapped]) => key.length > 0 && mapped.length > 0),
|
||||
);
|
||||
const entries: Array<[string, string]> = [];
|
||||
for (const [key, mapped] of Object.entries(value)) {
|
||||
const normalizedKey = key.trim();
|
||||
const normalizedValue = typeof mapped === "string" ? mapped.trim() : "";
|
||||
if (normalizedKey && normalizedValue) {
|
||||
entries.push([normalizedKey, normalizedValue]);
|
||||
}
|
||||
}
|
||||
const normalized = Object.fromEntries(entries);
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -235,6 +235,8 @@ describe("selectBatchFrames", () => {
|
||||
describe("sampleFrames", () => {
|
||||
it("keeps small sets and evenly samples large ones", () => {
|
||||
expect(sampleFrames([1, 2, 3], 16)).toEqual([1, 2, 3]);
|
||||
expect(sampleFrames([1, 2, 3], 1)).toEqual([1]);
|
||||
expect(sampleFrames([1, 2, 3], 0)).toEqual([]);
|
||||
const sampled = sampleFrames(
|
||||
Array.from({ length: 100 }, (_, i) => i),
|
||||
16,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Logbook analysis pipeline: frames -> observations -> revised timeline cards.
|
||||
// Pure parsing/validation lives here so tests can cover it without the SDK.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { CARD_CATEGORIES } from "./prompts.js";
|
||||
import { dayKeyFor } from "./store.js";
|
||||
import type { LogbookCard, LogbookCardDraft, LogbookDistraction } from "./types.js";
|
||||
@@ -224,20 +225,28 @@ export function parseCardsJson(params: {
|
||||
return { ok: false, error: "Output contained no valid cards." };
|
||||
}
|
||||
const sorted = drafts.toSorted((a, b) => a.startMs - b.startMs);
|
||||
for (let i = 1; i < sorted.length; i += 1) {
|
||||
const overlapMs = sorted[i - 1].endMs - sorted[i].startMs;
|
||||
const normalized: LogbookCardDraft[] = [];
|
||||
for (const current of sorted) {
|
||||
const previous = normalized.at(-1);
|
||||
if (!previous) {
|
||||
normalized.push(current);
|
||||
continue;
|
||||
}
|
||||
const overlapMs = previous.endMs - current.startMs;
|
||||
if (overlapMs > 60 * 1000) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Cards ${i - 1} and ${i} overlap by ${Math.round(overlapMs / 60000)} minutes; adjacent cards must meet cleanly.`,
|
||||
error: `Cards ${normalized.length - 1} and ${normalized.length} overlap by ${Math.round(overlapMs / 60000)} minutes; adjacent cards must meet cleanly.`,
|
||||
};
|
||||
}
|
||||
if (overlapMs > 0) {
|
||||
// Trim sub-minute overlaps instead of round-tripping to the model again.
|
||||
sorted[i] = { ...sorted[i], startMs: sorted[i - 1].endMs };
|
||||
normalized.push({ ...current, startMs: previous.endMs });
|
||||
} else {
|
||||
normalized.push(current);
|
||||
}
|
||||
}
|
||||
return { ok: true, drafts: sorted };
|
||||
return { ok: true, drafts: normalized };
|
||||
}
|
||||
|
||||
/** Sub-minute slack so minute-rounded model times do not fail coverage checks. */
|
||||
@@ -329,7 +338,7 @@ export function selectBatchFrames(params: {
|
||||
if (params.frames.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const first = params.frames[0];
|
||||
const first = expectDefined(params.frames[0], "first pending Logbook frame");
|
||||
const firstDay = dayKeyFor(first.capturedAtMs);
|
||||
const nextDayStart = new Date(first.capturedAtMs);
|
||||
nextDayStart.setHours(24, 0, 0, 0);
|
||||
@@ -358,7 +367,7 @@ export function selectBatchFrames(params: {
|
||||
if (selected.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const last = selected[selected.length - 1];
|
||||
const last = expectDefined(selected.at(-1), "last selected Logbook frame");
|
||||
// Only close a batch once its window has elapsed (or a gap/midnight ended
|
||||
// it), so a window in progress keeps accumulating frames; `force` closes an
|
||||
// in-progress window immediately (analyze now).
|
||||
@@ -380,13 +389,19 @@ export function selectBatchFrames(params: {
|
||||
|
||||
/** Evenly samples frames so a batch stays within the per-call image budget. */
|
||||
export function sampleFrames<T>(frames: T[], max: number): T[] {
|
||||
if (max <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (frames.length <= max) {
|
||||
return frames;
|
||||
}
|
||||
if (max === 1) {
|
||||
return [expectDefined(frames[0], "first Logbook frame sample")];
|
||||
}
|
||||
const sampled: T[] = [];
|
||||
const step = (frames.length - 1) / (max - 1);
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
sampled.push(frames[Math.round(i * step)]);
|
||||
sampled.push(expectDefined(frames[Math.round(i * step)], "sampled Logbook frame"));
|
||||
}
|
||||
return [...new Set(sampled)];
|
||||
}
|
||||
@@ -400,7 +415,7 @@ export function pickKeyframeId(
|
||||
return undefined;
|
||||
}
|
||||
const midpoint = card.startMs + (card.endMs - card.startMs) / 2;
|
||||
let best = frames[0];
|
||||
let best = expectDefined(frames[0], "first Logbook keyframe candidate");
|
||||
for (const frame of frames) {
|
||||
if (Math.abs(frame.capturedAtMs - midpoint) < Math.abs(best.capturedAtMs - midpoint)) {
|
||||
best = frame;
|
||||
|
||||
@@ -84,7 +84,11 @@ export function listMatrixEnvAccountIds(env: NodeJS.ProcessEnv = process.env): s
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const accountId = decodeMatrixEnvAccountToken(match[1]);
|
||||
const encodedAccountId = match[1];
|
||||
if (!encodedAccountId) {
|
||||
continue;
|
||||
}
|
||||
const accountId = decodeMatrixEnvAccountToken(encodedAccountId);
|
||||
if (accountId) {
|
||||
ids.add(accountId);
|
||||
}
|
||||
|
||||
@@ -349,8 +349,14 @@ function compactLooseListTokens(tokens: MarkdownToken[]): void {
|
||||
item.immediateParagraphOpenIndexes.length === 1 &&
|
||||
item.immediateParagraphCloseIndexes.length === 1
|
||||
) {
|
||||
tokens[item.immediateParagraphOpenIndexes[0]].hidden = true;
|
||||
tokens[item.immediateParagraphCloseIndexes[0]].hidden = true;
|
||||
const openIndex = item.immediateParagraphOpenIndexes[0];
|
||||
const closeIndex = item.immediateParagraphCloseIndexes[0];
|
||||
const openToken = openIndex === undefined ? undefined : tokens[openIndex];
|
||||
const closeToken = closeIndex === undefined ? undefined : tokens[closeIndex];
|
||||
if (openToken && closeToken) {
|
||||
openToken.hidden = true;
|
||||
closeToken.hidden = true;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ function parseGeoUri(value: string): GeoUriParams | null {
|
||||
}
|
||||
const payload = trimmed.slice(4);
|
||||
const [coordsPart, ...paramParts] = payload.split(";");
|
||||
if (!coordsPart) {
|
||||
return null;
|
||||
}
|
||||
const coords = coordsPart.split(",");
|
||||
if (coords.length < 2) {
|
||||
return null;
|
||||
|
||||
@@ -74,9 +74,14 @@ export function parseMxc(url: string): { server: string; mediaId: string } | nul
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const server = match[1];
|
||||
const mediaId = match[2];
|
||||
if (!server || !mediaId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
server: match[1],
|
||||
mediaId: match[2],
|
||||
server,
|
||||
mediaId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Matrix plugin module implements verification manager behavior.
|
||||
import {
|
||||
VerificationPhase,
|
||||
VerificationRequestEvent,
|
||||
VerifierEvent,
|
||||
} from "matrix-js-sdk/lib/crypto-api/verification.js";
|
||||
import { VerificationMethod } from "matrix-js-sdk/lib/types.js";
|
||||
// Matrix plugin module implements verification manager behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import {
|
||||
resolveDateTimestampMs,
|
||||
resolveTimestampMsToIsoString,
|
||||
@@ -341,7 +342,7 @@ export class MatrixVerificationManager {
|
||||
return txId === id;
|
||||
});
|
||||
if (transactionMatches.length === 1) {
|
||||
return transactionMatches[0];
|
||||
return expectDefined(transactionMatches[0], "single Matrix verification session");
|
||||
}
|
||||
if (transactionMatches.length > 1) {
|
||||
throw new Error(
|
||||
|
||||
@@ -78,7 +78,10 @@ export function resolveSingleAccountPromotionTarget(params: {
|
||||
([accountId, value]) => accountId && typeof value === "object" && value,
|
||||
);
|
||||
if (namedAccounts.length === 1) {
|
||||
return namedAccounts[0][0];
|
||||
const onlyAccount = namedAccounts[0];
|
||||
if (onlyAccount) {
|
||||
return onlyAccount[0];
|
||||
}
|
||||
}
|
||||
if (
|
||||
namedAccounts.length > 1 &&
|
||||
|
||||
@@ -507,7 +507,11 @@ function isRetryableError(error: Error): boolean {
|
||||
if (!clientErrorMatch) {
|
||||
continue;
|
||||
}
|
||||
const statusCode = Number.parseInt(clientErrorMatch[1], 10);
|
||||
const statusCodeText = clientErrorMatch[1];
|
||||
if (!statusCodeText) {
|
||||
continue;
|
||||
}
|
||||
const statusCode = Number.parseInt(statusCodeText, 10);
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -134,6 +134,9 @@ export async function listMattermostDirectoryPeers(
|
||||
// All bots see the same user list, so one client suffices (unlike channels
|
||||
// where private channel membership varies per bot).
|
||||
const client = clients[0];
|
||||
if (!client) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const me = await fetchMattermostMe(client);
|
||||
const teams = await client.request<{ id: string }[]>("/users/me/teams");
|
||||
@@ -141,7 +144,11 @@ export async function listMattermostDirectoryPeers(
|
||||
return [];
|
||||
}
|
||||
// Uses first team — multi-team setups may need iteration in the future
|
||||
const teamId = teams[0].id;
|
||||
const team = teams[0];
|
||||
if (!team) {
|
||||
return [];
|
||||
}
|
||||
const teamId = team.id;
|
||||
const q = normalizeLowercaseStringOrEmpty(params.query);
|
||||
|
||||
let users: MattermostUser[];
|
||||
|
||||
@@ -45,7 +45,11 @@ function splitModelRef(modelRef?: string | null): { provider: string; model: str
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const provider = normalizeProviderId(match[1]);
|
||||
const rawProvider = match[1];
|
||||
if (!rawProvider) {
|
||||
return null;
|
||||
}
|
||||
const provider = normalizeProviderId(rawProvider);
|
||||
// Mattermost copy should normalize accidental whitespace around the model.
|
||||
const model = normalizeOptionalString(match[2]);
|
||||
if (!provider || !model) {
|
||||
|
||||
@@ -522,7 +522,11 @@ function buildMattermostAttachmentPlaceholder(mediaList: MattermostMediaInfo[]):
|
||||
return "";
|
||||
}
|
||||
if (mediaList.length === 1) {
|
||||
const kind = mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind;
|
||||
const media = mediaList[0];
|
||||
if (!media) {
|
||||
return "";
|
||||
}
|
||||
const kind = media.kind === "unknown" ? "document" : media.kind;
|
||||
return `<media:${kind}>`;
|
||||
}
|
||||
const allImages = mediaList.every((media) => media.kind === "image");
|
||||
|
||||
@@ -102,11 +102,15 @@ export function resolveSlashHandlerForToken(token: string): SlashHandlerMatch {
|
||||
return { kind: "none" };
|
||||
}
|
||||
if (matches.length === 1) {
|
||||
const match = matches[0];
|
||||
if (!match) {
|
||||
return { kind: "none" };
|
||||
}
|
||||
return {
|
||||
kind: "single",
|
||||
source: "token",
|
||||
handler: matches[0].handler,
|
||||
accountIds: [matches[0].accountId],
|
||||
handler: match.handler,
|
||||
accountIds: [match.accountId],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,11 +150,15 @@ export function resolveSlashHandlerForCommand(params: {
|
||||
return { kind: "none" };
|
||||
}
|
||||
if (matches.length === 1) {
|
||||
const match = matches[0];
|
||||
if (!match) {
|
||||
return { kind: "none" };
|
||||
}
|
||||
return {
|
||||
kind: "single",
|
||||
source: "command",
|
||||
handler: matches[0].handler,
|
||||
accountIds: [matches[0].accountId],
|
||||
handler: match.handler,
|
||||
accountIds: [match.accountId],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -301,8 +309,8 @@ export function registerSlashCommandRoute(api: OpenClawPluginApi) {
|
||||
|
||||
// If there's only one active account (common case), route directly.
|
||||
if (accountStates.size === 1) {
|
||||
const [, state] = [...accountStates.entries()][0];
|
||||
if (!state.handler) {
|
||||
const state = accountStates.values().next().value;
|
||||
if (!state?.handler) {
|
||||
res.statusCode = 503;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import type { Dirent } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
||||
import {
|
||||
buildSessionEntry,
|
||||
@@ -954,13 +955,13 @@ async function collectSessionIngestionBatches(params: {
|
||||
};
|
||||
const cursorAtEnd = previous !== undefined && previous.lastContentLine >= previous.lineCount;
|
||||
const unchanged =
|
||||
Boolean(previous) &&
|
||||
previous !== undefined &&
|
||||
previous.mtimeMs === fingerprint.mtimeMs &&
|
||||
previous.size === fingerprint.size &&
|
||||
previous.contentHash.length > 0 &&
|
||||
cursorAtEnd;
|
||||
if (unchanged) {
|
||||
nextFiles[stateKey] = previous!;
|
||||
nextFiles[stateKey] = expectDefined(previous, "unchanged dreaming file state");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user