refactor: move bundled extension deps to plugin packages
This commit is contained in:
parent
07d9f725b6
commit
d7018aaf19
@ -181,13 +181,20 @@ OpenClaw scans, in order:
|
||||
|
||||
4. Bundled extensions (shipped with OpenClaw; mixed default-on/default-off)
|
||||
|
||||
- `<openclaw>/extensions/*`
|
||||
- `<openclaw>/dist/extensions/*` in packaged installs
|
||||
- `<workspace>/dist-runtime/extensions/*` in local built checkouts
|
||||
- `<workspace>/extensions/*` in source/Vitest workflows
|
||||
|
||||
Many bundled provider plugins are enabled by default so model catalogs/runtime
|
||||
hooks stay available without extra setup. Others still require explicit
|
||||
enablement via `plugins.entries.<id>.enabled` or
|
||||
`openclaw plugins enable <id>`.
|
||||
|
||||
Bundled plugin runtime dependencies are owned by each plugin package. Packaged
|
||||
builds stage opted-in bundled dependencies under
|
||||
`dist/extensions/<id>/node_modules` instead of requiring mirrored copies in the
|
||||
root package.
|
||||
|
||||
Installed plugins are enabled by default, but can be disabled the same way.
|
||||
|
||||
Workspace plugins are **disabled by default** unless you explicitly enable them
|
||||
|
||||
@ -3,6 +3,12 @@
|
||||
"version": "2026.3.14",
|
||||
"description": "OpenClaw Discord channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@buape/carbon": "0.0.0-beta-20260216184201",
|
||||
"@discordjs/voice": "^0.19.2",
|
||||
"discord-api-types": "^0.38.42",
|
||||
"opusscript": "^0.1.1"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
@ -18,6 +24,14 @@
|
||||
"blurb": "very well supported right now.",
|
||||
"systemImage": "bubble.left.and.bubble.right"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/discord",
|
||||
"localPath": "extensions/discord",
|
||||
"defaultChoice": "npm"
|
||||
},
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"release": {
|
||||
"publishToNpm": true
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { RequestClient } from "@buape/carbon";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { createDiscordRetryRunner, type RetryRunner } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import type { RetryConfig } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import type { RetryRunner } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
mergeDiscordAccountConfig,
|
||||
resolveDiscordAccount,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { createDiscordRetryRunner } from "./retry.js";
|
||||
import { normalizeDiscordToken } from "./token.js";
|
||||
|
||||
export type DiscordClientOpts = {
|
||||
|
||||
@ -33,7 +33,10 @@ import {
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { dispatchPluginInteractiveHandler } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import {
|
||||
dispatchPluginInteractiveHandler,
|
||||
type PluginInteractiveDiscordHandlerContext,
|
||||
} from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import {
|
||||
formatInboundEnvelope,
|
||||
@ -117,7 +120,7 @@ async function dispatchPluginDiscordInteractiveEvent(params: {
|
||||
? `channel:${params.interactionCtx.channelId}`
|
||||
: `user:${params.interactionCtx.userId}`;
|
||||
let responded = false;
|
||||
const respond = {
|
||||
const respond: PluginInteractiveDiscordHandlerContext["respond"] = {
|
||||
acknowledge: async () => {
|
||||
responded = true;
|
||||
await params.interaction.acknowledge();
|
||||
@ -136,20 +139,15 @@ async function dispatchPluginDiscordInteractiveEvent(params: {
|
||||
ephemeral,
|
||||
});
|
||||
},
|
||||
editMessage: async ({
|
||||
text,
|
||||
components,
|
||||
}: {
|
||||
text?: string;
|
||||
components?: TopLevelComponents[];
|
||||
}) => {
|
||||
editMessage: async (input) => {
|
||||
if (!("update" in params.interaction) || typeof params.interaction.update !== "function") {
|
||||
throw new Error("Discord interaction cannot update the source message");
|
||||
}
|
||||
const { text, components } = input;
|
||||
responded = true;
|
||||
await params.interaction.update({
|
||||
...(text !== undefined ? { content: text } : {}),
|
||||
...(components !== undefined ? { components } : {}),
|
||||
...(components !== undefined ? { components: components as TopLevelComponents[] } : {}),
|
||||
});
|
||||
},
|
||||
clearComponents: async (input?: { text?: string }) => {
|
||||
|
||||
@ -2,11 +2,11 @@ import type { RequestClient } from "@buape/carbon";
|
||||
import { resolveAgentAvatar } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { MarkdownTableMode, ReplyToMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { createDiscordRetryRunner, type RetryRunner } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
resolveRetryConfig,
|
||||
retryAsync,
|
||||
type RetryConfig,
|
||||
type RetryRunner,
|
||||
} from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
resolveSendableOutboundReplyParts,
|
||||
@ -19,6 +19,7 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveDiscordAccount } from "../accounts.js";
|
||||
import { chunkDiscordTextWithMode } from "../chunk.js";
|
||||
import { createDiscordRetryRunner } from "../retry.js";
|
||||
import { sendMessageDiscord, sendVoiceMessageDiscord, sendWebhookMessageDiscord } from "../send.js";
|
||||
import { sendDiscordText } from "../send.shared.js";
|
||||
|
||||
|
||||
27
extensions/discord/src/retry.ts
Normal file
27
extensions/discord/src/retry.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { RateLimitError } from "@buape/carbon";
|
||||
import {
|
||||
createRateLimitRetryRunner,
|
||||
type RetryConfig,
|
||||
type RetryRunner,
|
||||
} from "openclaw/plugin-sdk/infra-runtime";
|
||||
|
||||
export const DISCORD_RETRY_DEFAULTS = {
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
maxDelayMs: 30_000,
|
||||
jitter: 0.1,
|
||||
} satisfies RetryConfig;
|
||||
|
||||
export function createDiscordRetryRunner(params: {
|
||||
retry?: RetryConfig;
|
||||
configRetry?: RetryConfig;
|
||||
verbose?: boolean;
|
||||
}): RetryRunner {
|
||||
return createRateLimitRetryRunner({
|
||||
...params,
|
||||
defaults: DISCORD_RETRY_DEFAULTS,
|
||||
logLabel: "discord",
|
||||
shouldRetry: (err) => err instanceof RateLimitError,
|
||||
retryAfterMs: (err) => (err instanceof RateLimitError ? err.retryAfter * 1000 : undefined),
|
||||
});
|
||||
}
|
||||
@ -5,17 +5,6 @@ import path from "node:path";
|
||||
import type { Readable } from "node:stream";
|
||||
import { ChannelType, type Client, ReadyListener } from "@buape/carbon";
|
||||
import type { VoicePlugin } from "@buape/carbon/voice";
|
||||
import {
|
||||
AudioPlayerStatus,
|
||||
EndBehaviorType,
|
||||
VoiceConnectionStatus,
|
||||
createAudioPlayer,
|
||||
createAudioResource,
|
||||
entersState,
|
||||
joinVoiceChannel,
|
||||
type AudioPlayer,
|
||||
type VoiceConnection,
|
||||
} from "@discordjs/voice";
|
||||
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { agentCommandFromIngress } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { resolveTtsConfig, type ResolvedTtsConfig } from "openclaw/plugin-sdk/agent-runtime";
|
||||
@ -34,6 +23,7 @@ import { textToSpeech } from "openclaw/plugin-sdk/speech-runtime";
|
||||
import { formatMention } from "../mentions.js";
|
||||
import { resolveDiscordOwnerAccess } from "../monitor/allow-list.js";
|
||||
import { formatDiscordUserTag } from "../monitor/format.js";
|
||||
import { loadDiscordVoiceSdk } from "./sdk-runtime.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
@ -67,8 +57,8 @@ type VoiceSessionEntry = {
|
||||
channelId: string;
|
||||
sessionChannelId: string;
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
connection: VoiceConnection;
|
||||
player: AudioPlayer;
|
||||
connection: import("@discordjs/voice").VoiceConnection;
|
||||
player: import("@discordjs/voice").AudioPlayer;
|
||||
playbackQueue: Promise<void>;
|
||||
processingQueue: Promise<void>;
|
||||
activeSpeakers: Set<string>;
|
||||
@ -378,7 +368,8 @@ export class DiscordVoiceManager {
|
||||
decryptionFailureTolerance ?? "default"
|
||||
}`,
|
||||
);
|
||||
const connection = joinVoiceChannel({
|
||||
const voiceSdk = loadDiscordVoiceSdk();
|
||||
const connection = voiceSdk.joinVoiceChannel({
|
||||
channelId,
|
||||
guildId,
|
||||
adapterCreator,
|
||||
@ -389,7 +380,11 @@ export class DiscordVoiceManager {
|
||||
});
|
||||
|
||||
try {
|
||||
await entersState(connection, VoiceConnectionStatus.Ready, PLAYBACK_READY_TIMEOUT_MS);
|
||||
await voiceSdk.entersState(
|
||||
connection,
|
||||
voiceSdk.VoiceConnectionStatus.Ready,
|
||||
PLAYBACK_READY_TIMEOUT_MS,
|
||||
);
|
||||
logVoiceVerbose(`join: connected to guild ${guildId} channel ${channelId}`);
|
||||
} catch (err) {
|
||||
connection.destroy();
|
||||
@ -412,7 +407,7 @@ export class DiscordVoiceManager {
|
||||
peer: { kind: "channel", id: sessionChannelId },
|
||||
});
|
||||
|
||||
const player = createAudioPlayer();
|
||||
const player = voiceSdk.createAudioPlayer();
|
||||
connection.subscribe(player);
|
||||
|
||||
let speakingHandler: ((userId: string) => void) | undefined;
|
||||
@ -444,10 +439,10 @@ export class DiscordVoiceManager {
|
||||
connection.receiver.speaking.off("start", speakingHandler);
|
||||
}
|
||||
if (disconnectedHandler) {
|
||||
connection.off(VoiceConnectionStatus.Disconnected, disconnectedHandler);
|
||||
connection.off(voiceSdk.VoiceConnectionStatus.Disconnected, disconnectedHandler);
|
||||
}
|
||||
if (destroyedHandler) {
|
||||
connection.off(VoiceConnectionStatus.Destroyed, destroyedHandler);
|
||||
connection.off(voiceSdk.VoiceConnectionStatus.Destroyed, destroyedHandler);
|
||||
}
|
||||
if (playerErrorHandler) {
|
||||
player.off("error", playerErrorHandler);
|
||||
@ -466,8 +461,8 @@ export class DiscordVoiceManager {
|
||||
disconnectedHandler = async () => {
|
||||
try {
|
||||
await Promise.race([
|
||||
entersState(connection, VoiceConnectionStatus.Signalling, 5_000),
|
||||
entersState(connection, VoiceConnectionStatus.Connecting, 5_000),
|
||||
voiceSdk.entersState(connection, voiceSdk.VoiceConnectionStatus.Signalling, 5_000),
|
||||
voiceSdk.entersState(connection, voiceSdk.VoiceConnectionStatus.Connecting, 5_000),
|
||||
]);
|
||||
} catch {
|
||||
clearSessionIfCurrent();
|
||||
@ -482,8 +477,8 @@ export class DiscordVoiceManager {
|
||||
};
|
||||
|
||||
connection.receiver.speaking.on("start", speakingHandler);
|
||||
connection.on(VoiceConnectionStatus.Disconnected, disconnectedHandler);
|
||||
connection.on(VoiceConnectionStatus.Destroyed, destroyedHandler);
|
||||
connection.on(voiceSdk.VoiceConnectionStatus.Disconnected, disconnectedHandler);
|
||||
connection.on(voiceSdk.VoiceConnectionStatus.Destroyed, destroyedHandler);
|
||||
player.on("error", playerErrorHandler);
|
||||
|
||||
this.sessions.set(guildId, entry);
|
||||
@ -547,13 +542,14 @@ export class DiscordVoiceManager {
|
||||
logVoiceVerbose(
|
||||
`capture start: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`,
|
||||
);
|
||||
if (entry.player.state.status === AudioPlayerStatus.Playing) {
|
||||
const voiceSdk = loadDiscordVoiceSdk();
|
||||
if (entry.player.state.status === voiceSdk.AudioPlayerStatus.Playing) {
|
||||
entry.player.stop(true);
|
||||
}
|
||||
|
||||
const stream = entry.connection.receiver.subscribe(userId, {
|
||||
end: {
|
||||
behavior: EndBehaviorType.AfterSilence,
|
||||
behavior: voiceSdk.EndBehaviorType.AfterSilence,
|
||||
duration: SILENCE_DURATION_MS,
|
||||
},
|
||||
});
|
||||
@ -681,14 +677,15 @@ export class DiscordVoiceManager {
|
||||
logVoiceVerbose(
|
||||
`playback start: guild ${entry.guildId} channel ${entry.channelId} file ${path.basename(audioPath)}`,
|
||||
);
|
||||
const resource = createAudioResource(audioPath);
|
||||
const voiceSdk = loadDiscordVoiceSdk();
|
||||
const resource = voiceSdk.createAudioResource(audioPath);
|
||||
entry.player.play(resource);
|
||||
await entersState(entry.player, AudioPlayerStatus.Playing, PLAYBACK_READY_TIMEOUT_MS).catch(
|
||||
() => undefined,
|
||||
);
|
||||
await entersState(entry.player, AudioPlayerStatus.Idle, SPEAKING_READY_TIMEOUT_MS).catch(
|
||||
() => undefined,
|
||||
);
|
||||
await voiceSdk
|
||||
.entersState(entry.player, voiceSdk.AudioPlayerStatus.Playing, PLAYBACK_READY_TIMEOUT_MS)
|
||||
.catch(() => undefined);
|
||||
await voiceSdk
|
||||
.entersState(entry.player, voiceSdk.AudioPlayerStatus.Idle, SPEAKING_READY_TIMEOUT_MS)
|
||||
.catch(() => undefined);
|
||||
logVoiceVerbose(`playback done: guild ${entry.guildId} channel ${entry.channelId}`);
|
||||
});
|
||||
}
|
||||
|
||||
14
extensions/discord/src/voice/sdk-runtime.ts
Normal file
14
extensions/discord/src/voice/sdk-runtime.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
type DiscordVoiceSdk = typeof import("@discordjs/voice");
|
||||
|
||||
let cachedDiscordVoiceSdk: DiscordVoiceSdk | null = null;
|
||||
|
||||
export function loadDiscordVoiceSdk(): DiscordVoiceSdk {
|
||||
if (cachedDiscordVoiceSdk) {
|
||||
return cachedDiscordVoiceSdk;
|
||||
}
|
||||
const req = createRequire(import.meta.url);
|
||||
cachedDiscordVoiceSdk = req("@discordjs/voice") as DiscordVoiceSdk;
|
||||
return cachedDiscordVoiceSdk;
|
||||
}
|
||||
@ -32,6 +32,9 @@
|
||||
"localPath": "extensions/feishu",
|
||||
"defaultChoice": "npm"
|
||||
},
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"release": {
|
||||
"publishToNpm": true
|
||||
}
|
||||
|
||||
@ -38,11 +38,6 @@
|
||||
"npmSpec": "@openclaw/googlechat",
|
||||
"localPath": "extensions/googlechat",
|
||||
"defaultChoice": "npm"
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"google-auth-library"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,13 +33,6 @@
|
||||
},
|
||||
"release": {
|
||||
"publishToNpm": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs",
|
||||
"@vector-im/matrix-bot-sdk",
|
||||
"music-metadata"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,11 +32,6 @@
|
||||
},
|
||||
"release": {
|
||||
"publishToNpm": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@microsoft/agents-hosting"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,11 +29,6 @@
|
||||
},
|
||||
"release": {
|
||||
"publishToNpm": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"nostr-tools"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,13 +28,6 @@
|
||||
"npmSpec": "@openclaw/tlon",
|
||||
"localPath": "extensions/tlon",
|
||||
"defaultChoice": "npm"
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@tloncorp/api",
|
||||
"@tloncorp/tlon-skill",
|
||||
"@urbit/aura"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,11 +33,6 @@
|
||||
},
|
||||
"release": {
|
||||
"publishToNpm": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"zca-js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -476,10 +476,11 @@
|
||||
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
|
||||
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
|
||||
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
|
||||
"release:check": "pnpm config:docs:check && node --import tsx scripts/release-check.ts",
|
||||
"release:check": "pnpm config:docs:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && node --import tsx scripts/release-check.ts",
|
||||
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
|
||||
"release:plugins:npm:check": "node --import tsx scripts/plugin-npm-release-check.ts",
|
||||
"release:plugins:npm:plan": "node --import tsx scripts/plugin-npm-release-plan.ts",
|
||||
"stage:bundled-plugin-runtime-deps": "node scripts/stage-bundled-plugin-runtime-deps.mjs",
|
||||
"start": "node scripts/run-node.mjs",
|
||||
"test": "node scripts/test-parallel.mjs",
|
||||
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
|
||||
@ -534,14 +535,11 @@
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@aws-sdk/client-bedrock": "^3.1011.0",
|
||||
"@buape/carbon": "0.0.0-beta-20260216184201",
|
||||
"@clack/prompts": "^1.1.0",
|
||||
"@discordjs/voice": "^0.19.2",
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@homebridge/ciao": "^1.3.5",
|
||||
"@lancedb/lancedb": "^0.27.0",
|
||||
"@larksuiteoapi/node-sdk": "^1.59.0",
|
||||
"@line/bot-sdk": "^10.6.0",
|
||||
"@lydell/node-pty": "1.2.0-beta.3",
|
||||
"@mariozechner/pi-agent-core": "0.58.0",
|
||||
@ -560,7 +558,6 @@
|
||||
"cli-highlight": "^2.1.11",
|
||||
"commander": "^14.0.3",
|
||||
"croner": "^10.0.1",
|
||||
"discord-api-types": "^0.38.42",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"file-type": "21.3.3",
|
||||
@ -576,7 +573,6 @@
|
||||
"long": "^5.3.2",
|
||||
"markdown-it": "^14.1.1",
|
||||
"node-edge-tts": "^1.2.10",
|
||||
"opusscript": "^0.1.1",
|
||||
"osc-progress": "^0.3.0",
|
||||
"pdfjs-dist": "^5.5.207",
|
||||
"playwright-core": "1.58.2",
|
||||
|
||||
58
pnpm-lock.yaml
generated
58
pnpm-lock.yaml
generated
@ -34,15 +34,9 @@ importers:
|
||||
'@aws-sdk/client-bedrock':
|
||||
specifier: ^3.1011.0
|
||||
version: 3.1011.0
|
||||
'@buape/carbon':
|
||||
specifier: 0.0.0-beta-20260216184201
|
||||
version: 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)
|
||||
'@clack/prompts':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@discordjs/voice':
|
||||
specifier: ^0.19.2
|
||||
version: 0.19.2(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||
'@grammyjs/runner':
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(grammy@1.41.1)
|
||||
@ -55,9 +49,6 @@ importers:
|
||||
'@lancedb/lancedb':
|
||||
specifier: ^0.27.0
|
||||
version: 0.27.0(apache-arrow@18.1.0)
|
||||
'@larksuiteoapi/node-sdk':
|
||||
specifier: ^1.59.0
|
||||
version: 1.59.0
|
||||
'@line/bot-sdk':
|
||||
specifier: ^10.6.0
|
||||
version: 10.6.0
|
||||
@ -115,9 +106,6 @@ importers:
|
||||
croner:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
discord-api-types:
|
||||
specifier: ^0.38.42
|
||||
version: 0.38.42
|
||||
dotenv:
|
||||
specifier: ^17.3.1
|
||||
version: 17.3.1
|
||||
@ -166,9 +154,6 @@ importers:
|
||||
node-llama-cpp:
|
||||
specifier: 3.16.2
|
||||
version: 3.16.2(typescript@5.9.3)
|
||||
opusscript:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
osc-progress:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
@ -347,7 +332,20 @@ importers:
|
||||
specifier: 1.58.2
|
||||
version: 1.58.2
|
||||
|
||||
extensions/discord: {}
|
||||
extensions/discord:
|
||||
dependencies:
|
||||
'@buape/carbon':
|
||||
specifier: 0.0.0-beta-20260216184201
|
||||
version: 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)
|
||||
'@discordjs/voice':
|
||||
specifier: ^0.19.2
|
||||
version: 0.19.2(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||
discord-api-types:
|
||||
specifier: ^0.38.42
|
||||
version: 0.38.42
|
||||
opusscript:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
|
||||
extensions/elevenlabs: {}
|
||||
|
||||
@ -381,7 +379,7 @@ importers:
|
||||
version: 10.6.2
|
||||
openclaw:
|
||||
specifier: '>=2026.3.11'
|
||||
version: 2026.3.13(@discordjs/opus@0.10.0)(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(node-llama-cpp@3.16.2(typescript@5.9.3))
|
||||
version: 2026.3.13(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(node-llama-cpp@3.16.2(typescript@5.9.3))
|
||||
|
||||
extensions/huggingface: {}
|
||||
|
||||
@ -448,7 +446,7 @@ importers:
|
||||
dependencies:
|
||||
openclaw:
|
||||
specifier: '>=2026.3.11'
|
||||
version: 2026.3.13(@discordjs/opus@0.10.0)(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(node-llama-cpp@3.16.2(typescript@5.9.3))
|
||||
version: 2026.3.13(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(node-llama-cpp@3.16.2(typescript@5.9.3))
|
||||
|
||||
extensions/memory-lancedb:
|
||||
dependencies:
|
||||
@ -1210,10 +1208,6 @@ packages:
|
||||
resolution: {integrity: sha512-UyX6rGEXzVyPzb1yvjHtPfTlnLvB5jX/stAMdiytHhfoydX+98hfympdOwsnTktzr+IRvphxTbdErgYDJkEsvw==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
'@discordjs/voice@0.19.1':
|
||||
resolution: {integrity: sha512-XYbFVyUBB7zhRvrjREfiWDwio24nEp/vFaVe6u9aBIC5UYuT7HvoMt8LgNfZ5hOyaCW0flFr72pkhUGz+gWw4Q==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
'@discordjs/voice@0.19.2':
|
||||
resolution: {integrity: sha512-3yJ255e4ag3wfZu/DSxeOZK1UtnqNxnspmLaQetGT0pDkThNZoHs+Zg6dgZZ19JEVomXygvfHn9lNpICZuYtEA==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
@ -8386,22 +8380,6 @@ snapshots:
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
'@discordjs/voice@0.19.1(@discordjs/opus@0.10.0)(opusscript@0.1.1)':
|
||||
dependencies:
|
||||
'@snazzah/davey': 0.1.10
|
||||
'@types/ws': 8.18.1
|
||||
discord-api-types: 0.38.42
|
||||
prism-media: 1.3.5(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||
tslib: 2.8.1
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- '@discordjs/opus'
|
||||
- bufferutil
|
||||
- ffmpeg-static
|
||||
- node-opus
|
||||
- opusscript
|
||||
- utf-8-validate
|
||||
|
||||
'@discordjs/voice@0.19.2(@discordjs/opus@0.10.0)(opusscript@0.1.1)':
|
||||
dependencies:
|
||||
'@snazzah/davey': 0.1.10
|
||||
@ -13445,13 +13423,13 @@ snapshots:
|
||||
ws: 8.19.0
|
||||
zod: 4.3.6
|
||||
|
||||
openclaw@2026.3.13(@discordjs/opus@0.10.0)(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(node-llama-cpp@3.16.2(typescript@5.9.3)):
|
||||
openclaw@2026.3.13(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(node-llama-cpp@3.16.2(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 0.16.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock': 3.1009.0
|
||||
'@buape/carbon': 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)
|
||||
'@clack/prompts': 1.1.0
|
||||
'@discordjs/voice': 0.19.1(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||
'@discordjs/voice': 0.19.2(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||
'@grammyjs/runner': 2.0.3(grammy@1.41.1)
|
||||
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.41.1)
|
||||
'@homebridge/ciao': 1.3.5
|
||||
|
||||
@ -403,9 +403,6 @@ async function buildMissingPackages() {
|
||||
continue;
|
||||
}
|
||||
const meta = packageClusterMeta(relativePackagePath);
|
||||
const rootDependencyMirrorAllowlist = (
|
||||
pkg.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist ?? []
|
||||
).toSorted(compareStrings);
|
||||
const pluginSdkEntries = [...(pluginSdkReachability.get(meta.cluster) ?? new Set())].toSorted(
|
||||
compareStrings,
|
||||
);
|
||||
@ -421,9 +418,6 @@ async function buildMissingPackages() {
|
||||
packagePath: relativePackagePath,
|
||||
npmSpec: pkg.openclaw?.install?.npmSpec ?? null,
|
||||
private: pkg.private === true,
|
||||
rootDependencyMirrorAllowlist,
|
||||
mirrorAllowlistMatchesMissing:
|
||||
missing.join("\n") === rootDependencyMirrorAllowlist.join("\n"),
|
||||
pluginSdkReachability:
|
||||
pluginSdkEntries.length > 0 ? { staticEntryPoints: pluginSdkEntries } : undefined,
|
||||
missing,
|
||||
|
||||
@ -7,33 +7,10 @@ export type ExtensionPackageJson = {
|
||||
install?: {
|
||||
npmSpec?: string;
|
||||
};
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type BundledExtension = { id: string; packageJson: ExtensionPackageJson };
|
||||
export type BundledExtensionMetadata = BundledExtension & {
|
||||
npmSpec?: string;
|
||||
rootDependencyMirrorAllowlist: string[];
|
||||
};
|
||||
|
||||
export function normalizeBundledExtensionMetadata(
|
||||
extensions: BundledExtension[],
|
||||
): BundledExtensionMetadata[] {
|
||||
return extensions.map((extension) => ({
|
||||
...extension,
|
||||
npmSpec:
|
||||
typeof extension.packageJson.openclaw?.install?.npmSpec === "string"
|
||||
? extension.packageJson.openclaw.install.npmSpec.trim()
|
||||
: undefined,
|
||||
rootDependencyMirrorAllowlist:
|
||||
extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist?.filter(
|
||||
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
|
||||
) ?? [],
|
||||
}));
|
||||
}
|
||||
|
||||
export function collectBundledExtensionManifestErrors(extensions: BundledExtension[]): string[] {
|
||||
const errors: string[] = [];
|
||||
@ -48,23 +25,6 @@ export function collectBundledExtensionManifestErrors(extensions: BundledExtensi
|
||||
`bundled extension '${extension.id}' manifest invalid | openclaw.install.npmSpec must be a non-empty string`,
|
||||
);
|
||||
}
|
||||
|
||||
const allowlist = extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
|
||||
if (allowlist === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(allowlist)) {
|
||||
errors.push(
|
||||
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array of non-empty strings`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const invalidEntries = allowlist.filter((entry) => typeof entry !== "string" || !entry.trim());
|
||||
if (invalidEntries.length > 0) {
|
||||
errors.push(
|
||||
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must contain only non-empty strings`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
|
||||
@ -6,7 +6,6 @@ import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
collectBundledExtensionManifestErrors,
|
||||
normalizeBundledExtensionMetadata,
|
||||
type BundledExtension,
|
||||
type ExtensionPackageJson as PackageJson,
|
||||
} from "./lib/bundled-extension-manifest.ts";
|
||||
@ -34,45 +33,6 @@ const appcastPath = resolve("appcast.xml");
|
||||
const laneBuildMin = 1_000_000_000;
|
||||
const laneFloorAdoptionDateKey = 20260227;
|
||||
|
||||
export function collectBundledExtensionRootDependencyGapErrors(params: {
|
||||
rootPackage: PackageJson;
|
||||
extensions: BundledExtension[];
|
||||
}): string[] {
|
||||
const rootDeps = {
|
||||
...params.rootPackage.dependencies,
|
||||
...params.rootPackage.optionalDependencies,
|
||||
};
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const extension of normalizeBundledExtensionMetadata(params.extensions)) {
|
||||
if (!extension.npmSpec) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const missing = Object.keys(extension.packageJson.dependencies ?? {})
|
||||
.filter((dep) => dep !== "openclaw" && !rootDeps[dep])
|
||||
.toSorted();
|
||||
const allowlisted = extension.rootDependencyMirrorAllowlist.toSorted();
|
||||
if (missing.join("\n") !== allowlisted.join("\n")) {
|
||||
const unexpected = missing.filter((dep) => !allowlisted.includes(dep));
|
||||
const resolved = allowlisted.filter((dep) => !missing.includes(dep));
|
||||
const parts = [
|
||||
`bundled extension '${extension.id}' root dependency mirror drift`,
|
||||
`missing in root package: ${missing.length > 0 ? missing.join(", ") : "(none)"}`,
|
||||
];
|
||||
if (unexpected.length > 0) {
|
||||
parts.push(`new gaps: ${unexpected.join(", ")}`);
|
||||
}
|
||||
if (resolved.length > 0) {
|
||||
parts.push(`remove stale allowlist entries: ${resolved.join(", ")}`);
|
||||
}
|
||||
errors.push(parts.join(" | "));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function collectBundledExtensions(): BundledExtension[] {
|
||||
const extensionsDir = resolve("extensions");
|
||||
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
|
||||
@ -94,8 +54,7 @@ function collectBundledExtensions(): BundledExtension[] {
|
||||
});
|
||||
}
|
||||
|
||||
function checkBundledExtensionRootDependencyMirrors() {
|
||||
const rootPackage = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as PackageJson;
|
||||
function checkBundledExtensionMetadata() {
|
||||
const extensions = collectBundledExtensions();
|
||||
const manifestErrors = collectBundledExtensionManifestErrors(extensions);
|
||||
if (manifestErrors.length > 0) {
|
||||
@ -105,17 +64,6 @@ function checkBundledExtensionRootDependencyMirrors() {
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
const errors = collectBundledExtensionRootDependencyGapErrors({
|
||||
rootPackage,
|
||||
extensions,
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
console.error("release-check: bundled extension root dependency mirror validation failed:");
|
||||
for (const error of errors) {
|
||||
console.error(` - ${error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function runPackDry(): PackResult[] {
|
||||
@ -128,11 +76,13 @@ function runPackDry(): PackResult[] {
|
||||
}
|
||||
|
||||
export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
|
||||
const isAllowedBundledPluginNodeModulesPath = (path: string) =>
|
||||
/^dist\/extensions\/[^/]+\/node_modules\//.test(path);
|
||||
return [...paths]
|
||||
.filter(
|
||||
(path) =>
|
||||
forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) ||
|
||||
/(^|\/)node_modules\//.test(path),
|
||||
(/node_modules\//.test(path) && !isAllowedBundledPluginNodeModulesPath(path)),
|
||||
)
|
||||
.toSorted();
|
||||
}
|
||||
@ -338,7 +288,7 @@ async function checkPluginSdkExports() {
|
||||
async function main() {
|
||||
checkAppcastSparkleVersions();
|
||||
await checkPluginSdkExports();
|
||||
checkBundledExtensionRootDependencyMirrors();
|
||||
checkBundledExtensionMetadata();
|
||||
|
||||
const results = runPackDry();
|
||||
const files = results.flatMap((entry) => entry.files ?? []);
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { copyBundledPluginMetadata } from "./copy-bundled-plugin-metadata.mjs";
|
||||
import { copyPluginSdkRootAlias } from "./copy-plugin-sdk-root-alias.mjs";
|
||||
import { stageBundledPluginRuntimeDeps } from "./stage-bundled-plugin-runtime-deps.mjs";
|
||||
import { stageBundledPluginRuntime } from "./stage-bundled-plugin-runtime.mjs";
|
||||
|
||||
export function runRuntimePostBuild(params = {}) {
|
||||
copyPluginSdkRootAlias(params);
|
||||
copyBundledPluginMetadata(params);
|
||||
stageBundledPluginRuntimeDeps(params);
|
||||
stageBundledPluginRuntime(params);
|
||||
}
|
||||
|
||||
|
||||
74
scripts/stage-bundled-plugin-runtime-deps.mjs
Normal file
74
scripts/stage-bundled-plugin-runtime-deps.mjs
Normal file
@ -0,0 +1,74 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function removePathIfExists(targetPath) {
|
||||
fs.rmSync(targetPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function listBundledPluginRuntimeDirs(repoRoot) {
|
||||
const extensionsRoot = path.join(repoRoot, "dist", "extensions");
|
||||
if (!fs.existsSync(extensionsRoot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(extensionsRoot, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => path.join(extensionsRoot, dirent.name))
|
||||
.filter((pluginDir) => fs.existsSync(path.join(pluginDir, "package.json")));
|
||||
}
|
||||
|
||||
function hasRuntimeDeps(packageJson) {
|
||||
return (
|
||||
Object.keys(packageJson.dependencies ?? {}).length > 0 ||
|
||||
Object.keys(packageJson.optionalDependencies ?? {}).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function shouldStageRuntimeDeps(packageJson) {
|
||||
return packageJson.openclaw?.bundle?.stageRuntimeDependencies === true;
|
||||
}
|
||||
|
||||
function installPluginRuntimeDeps(pluginDir, pluginId) {
|
||||
const result = spawnSync(
|
||||
"npm",
|
||||
["install", "--omit=dev", "--silent", "--ignore-scripts", "--package-lock=false"],
|
||||
{
|
||||
cwd: pluginDir,
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
shell: process.platform === "win32",
|
||||
},
|
||||
);
|
||||
if (result.status === 0) {
|
||||
return;
|
||||
}
|
||||
const output = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
||||
throw new Error(
|
||||
`failed to stage bundled runtime deps for ${pluginId}: ${output || "npm install failed"}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function stageBundledPluginRuntimeDeps(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
for (const pluginDir of listBundledPluginRuntimeDirs(repoRoot)) {
|
||||
const pluginId = path.basename(pluginDir);
|
||||
const packageJson = readJson(path.join(pluginDir, "package.json"));
|
||||
const nodeModulesDir = path.join(pluginDir, "node_modules");
|
||||
removePathIfExists(nodeModulesDir);
|
||||
if (!hasRuntimeDeps(packageJson) || !shouldStageRuntimeDeps(packageJson)) {
|
||||
continue;
|
||||
}
|
||||
installPluginRuntimeDeps(pluginDir, pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
stageBundledPluginRuntimeDeps();
|
||||
}
|
||||
@ -98,7 +98,6 @@ export function stageBundledPluginRuntime(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const distRoot = path.join(repoRoot, "dist");
|
||||
const runtimeRoot = path.join(repoRoot, "dist-runtime");
|
||||
const sourceExtensionsRoot = path.join(repoRoot, "extensions");
|
||||
const distExtensionsRoot = path.join(distRoot, "extensions");
|
||||
const runtimeExtensionsRoot = path.join(runtimeRoot, "extensions");
|
||||
|
||||
@ -116,13 +115,12 @@ export function stageBundledPluginRuntime(params = {}) {
|
||||
}
|
||||
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
|
||||
const runtimePluginDir = path.join(runtimeExtensionsRoot, dirent.name);
|
||||
const sourcePluginNodeModulesDir = path.join(sourceExtensionsRoot, dirent.name, "node_modules");
|
||||
const distPluginNodeModulesDir = path.join(distPluginDir, "node_modules");
|
||||
|
||||
stagePluginRuntimeOverlay(distPluginDir, runtimePluginDir);
|
||||
linkPluginNodeModules({
|
||||
runtimePluginDir,
|
||||
distPluginDir,
|
||||
sourcePluginNodeModulesDir,
|
||||
sourcePluginNodeModulesDir: distPluginNodeModulesDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,9 +33,9 @@ function removeDistPluginNodeModulesSymlinks(rootDir) {
|
||||
|
||||
function pruneStaleRuntimeSymlinks() {
|
||||
const cwd = process.cwd();
|
||||
// runtime-postbuild links dist/dist-runtime plugin node_modules back into the
|
||||
// source extensions. Remove only those symlinks up front so tsdown's clean
|
||||
// step cannot traverse into the active pnpm install tree on rebuilds.
|
||||
// runtime-postbuild stages plugin-owned node_modules into dist/ and links the
|
||||
// dist-runtime overlay back to that tree. Remove only those symlinks up front
|
||||
// so tsdown's clean step cannot traverse stale runtime overlays on rebuilds.
|
||||
removeDistPluginNodeModulesSymlinks(path.join(cwd, "dist"));
|
||||
removeDistPluginNodeModulesSymlinks(path.join(cwd, "dist-runtime"));
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { TopLevelComponents } from "@buape/carbon";
|
||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
@ -276,12 +275,16 @@ export type ChannelStreamingAdapter = {
|
||||
};
|
||||
};
|
||||
|
||||
// Keep core transport-agnostic. Plugins can carry richer component types on
|
||||
// their side and cast at the boundary.
|
||||
export type ChannelStructuredComponents = unknown[];
|
||||
|
||||
export type ChannelCrossContextComponentsFactory = (params: {
|
||||
originLabel: string;
|
||||
message: string;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => TopLevelComponents[];
|
||||
}) => ChannelStructuredComponents;
|
||||
|
||||
export type ChannelReplyTransport = {
|
||||
replyToId?: string | null;
|
||||
|
||||
@ -70,6 +70,7 @@ export type {
|
||||
ChannelSetupInput,
|
||||
ChannelStatusIssue,
|
||||
ChannelStreamingAdapter,
|
||||
ChannelStructuredComponents,
|
||||
ChannelThreadingAdapter,
|
||||
ChannelThreadingContext,
|
||||
ChannelThreadingToolContext,
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import type { TopLevelComponents } from "@buape/carbon";
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { ChannelId, ChannelStructuredComponents } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
export type CrossContextComponentsBuilder = (message: string) => TopLevelComponents[];
|
||||
export type CrossContextComponentsBuilder = (message: string) => ChannelStructuredComponents;
|
||||
|
||||
export type CrossContextComponentsFactory = (params: {
|
||||
originLabel: string;
|
||||
message: string;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => TopLevelComponents[];
|
||||
}) => ChannelStructuredComponents;
|
||||
|
||||
export type ChannelMessageAdapter = {
|
||||
supportsComponentsV2: boolean;
|
||||
|
||||
@ -1,17 +1,9 @@
|
||||
import { RateLimitError } from "@buape/carbon";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { formatErrorMessage } from "./errors.js";
|
||||
import { type RetryConfig, resolveRetryConfig, retryAsync } from "./retry.js";
|
||||
|
||||
export type RetryRunner = <T>(fn: () => Promise<T>, label?: string) => Promise<T>;
|
||||
|
||||
export const DISCORD_RETRY_DEFAULTS = {
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
maxDelayMs: 30_000,
|
||||
jitter: 0.1,
|
||||
};
|
||||
|
||||
export const TELEGRAM_RETRY_DEFAULTS = {
|
||||
attempts: 3,
|
||||
minDelayMs: 400,
|
||||
@ -58,12 +50,16 @@ function getTelegramRetryAfterMs(err: unknown): number | undefined {
|
||||
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate * 1000 : undefined;
|
||||
}
|
||||
|
||||
export function createDiscordRetryRunner(params: {
|
||||
export function createRateLimitRetryRunner(params: {
|
||||
retry?: RetryConfig;
|
||||
configRetry?: RetryConfig;
|
||||
verbose?: boolean;
|
||||
defaults: Required<RetryConfig>;
|
||||
logLabel: string;
|
||||
shouldRetry: (err: unknown) => boolean;
|
||||
retryAfterMs?: (err: unknown) => number | undefined;
|
||||
}): RetryRunner {
|
||||
const retryConfig = resolveRetryConfig(DISCORD_RETRY_DEFAULTS, {
|
||||
const retryConfig = resolveRetryConfig(params.defaults, {
|
||||
...params.configRetry,
|
||||
...params.retry,
|
||||
});
|
||||
@ -71,14 +67,14 @@ export function createDiscordRetryRunner(params: {
|
||||
retryAsync(fn, {
|
||||
...retryConfig,
|
||||
label,
|
||||
shouldRetry: (err) => err instanceof RateLimitError,
|
||||
retryAfterMs: (err) => (err instanceof RateLimitError ? err.retryAfter * 1000 : undefined),
|
||||
shouldRetry: params.shouldRetry,
|
||||
retryAfterMs: params.retryAfterMs,
|
||||
onRetry: params.verbose
|
||||
? (info) => {
|
||||
const labelText = info.label ?? "request";
|
||||
const maxRetries = Math.max(1, info.maxAttempts - 1);
|
||||
log.warn(
|
||||
`discord ${labelText} rate limited, retry ${info.attempt}/${maxRetries} in ${info.delayMs}ms`,
|
||||
`${params.logLabel} ${labelText} rate limited, retry ${info.attempt}/${maxRetries} in ${info.delayMs}ms`,
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@ -14,6 +14,7 @@ export * from "../media/outbound-attachment.js";
|
||||
export * from "../media/png-encode.ts";
|
||||
export * from "../media/store.js";
|
||||
export * from "../media/temp-files.js";
|
||||
export * from "./agent-media-payload.js";
|
||||
export * from "../media-understanding/audio-preflight.ts";
|
||||
export * from "../media-understanding/defaults.js";
|
||||
export * from "../media-understanding/providers/image-runtime.ts";
|
||||
|
||||
@ -50,6 +50,22 @@ describe("resolveBundledPluginsDir", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to built dist/extensions in installed package roots", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-bundled-dir-dist-");
|
||||
fs.mkdirSync(path.join(repoRoot, "dist", "extensions"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoRoot, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw" }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
process.chdir(repoRoot);
|
||||
|
||||
expect(fs.realpathSync(resolveBundledPluginsDir() ?? "")).toBe(
|
||||
fs.realpathSync(path.join(repoRoot, "dist", "extensions")),
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers source extensions under vitest to avoid stale staged plugins", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-bundled-dir-vitest-");
|
||||
fs.mkdirSync(path.join(repoRoot, "extensions"), { recursive: true });
|
||||
|
||||
@ -29,6 +29,7 @@ export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env):
|
||||
);
|
||||
for (const packageRoot of packageRoots) {
|
||||
const sourceExtensionsDir = path.join(packageRoot, "extensions");
|
||||
const builtExtensionsDir = path.join(packageRoot, "dist", "extensions");
|
||||
if (
|
||||
(preferSourceCheckout || isSourceCheckoutRoot(packageRoot)) &&
|
||||
fs.existsSync(sourceExtensionsDir)
|
||||
@ -39,10 +40,12 @@ export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env):
|
||||
// dist-runtime/. Prefer that over source extensions only when the paired
|
||||
// dist/ tree exists; otherwise wrappers can drift ahead of the last build.
|
||||
const runtimeExtensionsDir = path.join(packageRoot, "dist-runtime", "extensions");
|
||||
const builtExtensionsDir = path.join(packageRoot, "dist", "extensions");
|
||||
if (fs.existsSync(runtimeExtensionsDir) && fs.existsSync(builtExtensionsDir)) {
|
||||
return runtimeExtensionsDir;
|
||||
}
|
||||
if (fs.existsSync(builtExtensionsDir)) {
|
||||
return builtExtensionsDir;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
@ -51,6 +54,10 @@ export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env):
|
||||
// bun --compile: ship a sibling `extensions/` next to the executable.
|
||||
try {
|
||||
const execDir = path.dirname(process.execPath);
|
||||
const siblingBuilt = path.join(execDir, "dist", "extensions");
|
||||
if (fs.existsSync(siblingBuilt)) {
|
||||
return siblingBuilt;
|
||||
}
|
||||
const sibling = path.join(execDir, "extensions");
|
||||
if (fs.existsSync(sibling)) {
|
||||
return sibling;
|
||||
|
||||
@ -12,25 +12,33 @@ function readJson<T>(relativePath: string): T {
|
||||
}
|
||||
|
||||
describe("bundled plugin runtime dependencies", () => {
|
||||
it("keeps bundled Feishu runtime deps available from the published root package", () => {
|
||||
it("keeps bundled Feishu runtime deps plugin-local instead of mirroring them into the root package", () => {
|
||||
const rootManifest = readJson<PackageManifest>("package.json");
|
||||
const feishuManifest = readJson<PackageManifest>("extensions/feishu/package.json");
|
||||
const feishuSpec = feishuManifest.dependencies?.["@larksuiteoapi/node-sdk"];
|
||||
const rootSpec = rootManifest.dependencies?.["@larksuiteoapi/node-sdk"];
|
||||
|
||||
expect(feishuSpec).toBeTruthy();
|
||||
expect(rootSpec).toBeTruthy();
|
||||
expect(rootSpec).toBe(feishuSpec);
|
||||
expect(rootSpec).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps bundled memory-lancedb runtime deps available from the published root package", () => {
|
||||
it("keeps bundled memory-lancedb runtime deps available from the root package while its native runtime stays bundled", () => {
|
||||
const rootManifest = readJson<PackageManifest>("package.json");
|
||||
const memoryManifest = readJson<PackageManifest>("extensions/memory-lancedb/package.json");
|
||||
const memorySpec = memoryManifest.dependencies?.["@lancedb/lancedb"];
|
||||
const rootSpec = rootManifest.dependencies?.["@lancedb/lancedb"];
|
||||
|
||||
expect(memorySpec).toBeTruthy();
|
||||
expect(rootSpec).toBeTruthy();
|
||||
expect(rootSpec).toBe(memorySpec);
|
||||
});
|
||||
|
||||
it("keeps bundled Discord runtime deps plugin-local instead of mirroring them into the root package", () => {
|
||||
const rootManifest = readJson<PackageManifest>("package.json");
|
||||
const discordManifest = readJson<PackageManifest>("extensions/discord/package.json");
|
||||
const discordSpec = discordManifest.dependencies?.["@buape/carbon"];
|
||||
const rootSpec = rootManifest.dependencies?.["@buape/carbon"];
|
||||
|
||||
expect(discordSpec).toBeTruthy();
|
||||
expect(rootSpec).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -22,18 +22,17 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("stageBundledPluginRuntime", () => {
|
||||
it("stages bundled dist plugins as runtime wrappers and links plugin-local node_modules", () => {
|
||||
it("stages bundled dist plugins as runtime wrappers and links staged dist node_modules", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-");
|
||||
const distPluginDir = path.join(repoRoot, "dist", "extensions", "diffs");
|
||||
fs.mkdirSync(path.join(repoRoot, "dist"), { recursive: true });
|
||||
const sourcePluginNodeModulesDir = path.join(repoRoot, "extensions", "diffs", "node_modules");
|
||||
fs.mkdirSync(distPluginDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(sourcePluginNodeModulesDir, "@pierre", "diffs"), {
|
||||
fs.mkdirSync(path.join(distPluginDir, "node_modules", "@pierre", "diffs"), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.writeFileSync(path.join(distPluginDir, "index.js"), "export default {}\n", "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(sourcePluginNodeModulesDir, "@pierre", "diffs", "index.js"),
|
||||
path.join(distPluginDir, "node_modules", "@pierre", "diffs", "index.js"),
|
||||
"export default {}\n",
|
||||
"utf8",
|
||||
);
|
||||
@ -47,9 +46,9 @@ describe("stageBundledPluginRuntime", () => {
|
||||
);
|
||||
expect(fs.lstatSync(path.join(runtimePluginDir, "node_modules")).isSymbolicLink()).toBe(true);
|
||||
expect(fs.realpathSync(path.join(runtimePluginDir, "node_modules"))).toBe(
|
||||
fs.realpathSync(sourcePluginNodeModulesDir),
|
||||
fs.realpathSync(path.join(distPluginDir, "node_modules")),
|
||||
);
|
||||
expect(fs.existsSync(path.join(distPluginDir, "node_modules"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(distPluginDir, "node_modules"))).toBe(true);
|
||||
});
|
||||
|
||||
it("writes wrappers that forward plugin entry imports into canonical dist files", async () => {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { TopLevelComponents } from "@buape/carbon";
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
@ -16,7 +15,11 @@ import type { ProviderCapabilities } from "../agents/provider-capabilities.js";
|
||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type {
|
||||
ChannelId,
|
||||
ChannelPlugin,
|
||||
ChannelStructuredComponents,
|
||||
} from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ModelProviderConfig } from "../config/types.js";
|
||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||
@ -1132,7 +1135,10 @@ export type PluginInteractiveDiscordHandlerContext = {
|
||||
acknowledge: () => Promise<void>;
|
||||
reply: (params: { text: string; ephemeral?: boolean }) => Promise<void>;
|
||||
followUp: (params: { text: string; ephemeral?: boolean }) => Promise<void>;
|
||||
editMessage: (params: { text?: string; components?: TopLevelComponents[] }) => Promise<void>;
|
||||
editMessage: (params: {
|
||||
text?: string;
|
||||
components?: ChannelStructuredComponents;
|
||||
}) => Promise<void>;
|
||||
clearComponents: (params?: { text?: string }) => Promise<void>;
|
||||
};
|
||||
requestConversationBinding: (
|
||||
|
||||
@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectAppcastSparkleVersionErrors,
|
||||
collectBundledExtensionManifestErrors,
|
||||
collectBundledExtensionRootDependencyGapErrors,
|
||||
collectForbiddenPackPaths,
|
||||
collectPackUnpackedSizeErrors,
|
||||
} from "../scripts/release-check.ts";
|
||||
@ -37,87 +36,6 @@ describe("collectAppcastSparkleVersionErrors", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectBundledExtensionRootDependencyGapErrors", () => {
|
||||
it("allows known gaps but still flags unallowlisted ones", () => {
|
||||
expect(
|
||||
collectBundledExtensionRootDependencyGapErrors({
|
||||
rootPackage: { dependencies: {} },
|
||||
extensions: [
|
||||
{
|
||||
id: "googlechat",
|
||||
packageJson: {
|
||||
dependencies: { "google-auth-library": "^1.0.0" },
|
||||
openclaw: {
|
||||
install: { npmSpec: "@openclaw/googlechat" },
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["google-auth-library"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "feishu",
|
||||
packageJson: {
|
||||
dependencies: { "@larksuiteoapi/node-sdk": "^1.59.0" },
|
||||
openclaw: { install: { npmSpec: "@openclaw/feishu" } },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
"bundled extension 'feishu' root dependency mirror drift | missing in root package: @larksuiteoapi/node-sdk | new gaps: @larksuiteoapi/node-sdk",
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags newly introduced bundled extension dependency gaps", () => {
|
||||
expect(
|
||||
collectBundledExtensionRootDependencyGapErrors({
|
||||
rootPackage: { dependencies: {} },
|
||||
extensions: [
|
||||
{
|
||||
id: "googlechat",
|
||||
packageJson: {
|
||||
dependencies: { "google-auth-library": "^1.0.0", undici: "^7.0.0" },
|
||||
openclaw: {
|
||||
install: { npmSpec: "@openclaw/googlechat" },
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["google-auth-library"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
"bundled extension 'googlechat' root dependency mirror drift | missing in root package: google-auth-library, undici | new gaps: undici",
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags stale allowlist entries once a gap is resolved", () => {
|
||||
expect(
|
||||
collectBundledExtensionRootDependencyGapErrors({
|
||||
rootPackage: { dependencies: { "google-auth-library": "^1.0.0" } },
|
||||
extensions: [
|
||||
{
|
||||
id: "googlechat",
|
||||
packageJson: {
|
||||
dependencies: { "google-auth-library": "^1.0.0" },
|
||||
openclaw: {
|
||||
install: { npmSpec: "@openclaw/googlechat" },
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["google-auth-library"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
"bundled extension 'googlechat' root dependency mirror drift | missing in root package: (none) | remove stale allowlist entries: google-auth-library",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectBundledExtensionManifestErrors", () => {
|
||||
it("flags invalid bundled extension install metadata", () => {
|
||||
expect(
|
||||
@ -135,33 +53,14 @@ describe("collectBundledExtensionManifestErrors", () => {
|
||||
"bundled extension 'broken' manifest invalid | openclaw.install.npmSpec must be a non-empty string",
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags invalid release-check allowlist metadata", () => {
|
||||
expect(
|
||||
collectBundledExtensionManifestErrors([
|
||||
{
|
||||
id: "broken",
|
||||
packageJson: {
|
||||
openclaw: {
|
||||
install: { npmSpec: "@openclaw/broken" },
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["ok", ""],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
).toEqual([
|
||||
"bundled extension 'broken' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must contain only non-empty strings",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectForbiddenPackPaths", () => {
|
||||
it("flags nested node_modules leaking into npm pack output", () => {
|
||||
it("allows bundled plugin runtime deps under dist/extensions but still blocks other node_modules", () => {
|
||||
expect(
|
||||
collectForbiddenPackPaths([
|
||||
"dist/index.js",
|
||||
"dist/extensions/discord/node_modules/@buape/carbon/index.js",
|
||||
"extensions/tlon/node_modules/.bin/tlon",
|
||||
"node_modules/.bin/openclaw",
|
||||
]),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user