refactor: extract media generation core package

Extract pure media generation catalog/model-ref/normalization helpers into a private workspace package and wire the package through build, watch, SDK alias, and plugin boundary d.ts paths.

Verification:
- node scripts/run-vitest.mjs test/scripts/crabbox-wrapper.test.ts packages/media-generation-core/src src/media-generation/runtime-shared.test.ts src/plugins/sdk-alias.test.ts src/infra/watch-node.test.ts src/plugins/registry.provider-like.test.ts src/agents/model-ref-shared.test.ts extensions/codex-supervisor/src/plugin-tools.test.ts extensions/codex-supervisor/src/supervisor.test.ts src/wizard/setup.official-plugins.test.ts src/infra/net/http-connect-tunnel.test.ts
- node scripts/prepare-extension-package-boundary-artifacts.mjs --mode=all
- node scripts/run-vitest.mjs src/plugins/contracts/extension-package-project-boundaries.test.ts src/plugins/sdk-alias.test.ts
- pnpm protocol:check
- pnpm check:changed
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- GitHub CI 26676608512
This commit is contained in:
Peter Steinberger 2026-05-30 08:17:43 +02:00 committed by GitHub
parent be2c43ee3e
commit 7c1484d637
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 480 additions and 189 deletions

View file

@ -1,5 +1,4 @@
import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core";
import { asSafeIntegerInRange } from "openclaw/plugin-sdk/number-runtime";
import { Type } from "typebox";
import {
redactCodexSupervisorEndpoint,
@ -74,14 +73,13 @@ function readIntegerParam(params: Record<string, unknown>, key: string): number
if (value === undefined) {
return undefined;
}
const integer = asSafeIntegerInRange(value, { min: 1, max: 1000 });
if (integer === undefined) {
if (typeof value === "number" && Number.isInteger(value)) {
throw new Error(`${key} must be between 1 and 1000`);
}
if (typeof value !== "number" || !Number.isInteger(value)) {
throw new Error(`${key} must be an integer`);
}
return integer;
if (value < 1 || value > 1000) {
throw new Error(`${key} must be between 1 and 1000`);
}
return value;
}
function readModeParam(params: Record<string, unknown>): CodexSupervisorTurnMode | undefined {

View file

@ -102,6 +102,24 @@
"../dist/plugin-sdk/packages/llm-core/src/validation.d.ts"
],
"@openclaw/llm-core/*": ["../dist/plugin-sdk/packages/llm-core/src/*.d.ts"],
"@openclaw/media-generation-core": [
"../dist/plugin-sdk/packages/media-generation-core/src/index.d.ts"
],
"@openclaw/media-generation-core/capability-model-ref": [
"../dist/plugin-sdk/packages/media-generation-core/src/capability-model-ref.d.ts"
],
"@openclaw/media-generation-core/catalog": [
"../dist/plugin-sdk/packages/media-generation-core/src/catalog.d.ts"
],
"@openclaw/media-generation-core/model-ref": [
"../dist/plugin-sdk/packages/media-generation-core/src/model-ref.d.ts"
],
"@openclaw/media-generation-core/normalization": [
"../dist/plugin-sdk/packages/media-generation-core/src/normalization.d.ts"
],
"@openclaw/media-generation-core/*": [
"../dist/plugin-sdk/packages/media-generation-core/src/*.d.ts"
],
"@openclaw/*.js": ["../packages/plugin-sdk/dist/extensions/*.d.ts", "../extensions/*"],
"@openclaw/*": ["../packages/plugin-sdk/dist/extensions/*", "../extensions/*"],
"openclaw/plugin-sdk/qa-channel": ["../dist/plugin-sdk/src/plugin-sdk/qa-channel.d.ts"],

View file

@ -111,6 +111,24 @@
"@openclaw/llm-core/*": [
"../../dist/plugin-sdk/packages/llm-core/src/*.d.ts"
],
"@openclaw/media-generation-core": [
"../../dist/plugin-sdk/packages/media-generation-core/src/index.d.ts"
],
"@openclaw/media-generation-core/capability-model-ref": [
"../../dist/plugin-sdk/packages/media-generation-core/src/capability-model-ref.d.ts"
],
"@openclaw/media-generation-core/catalog": [
"../../dist/plugin-sdk/packages/media-generation-core/src/catalog.d.ts"
],
"@openclaw/media-generation-core/model-ref": [
"../../dist/plugin-sdk/packages/media-generation-core/src/model-ref.d.ts"
],
"@openclaw/media-generation-core/normalization": [
"../../dist/plugin-sdk/packages/media-generation-core/src/normalization.d.ts"
],
"@openclaw/media-generation-core/*": [
"../../dist/plugin-sdk/packages/media-generation-core/src/*.d.ts"
],
"@openclaw/*.js": ["../../packages/plugin-sdk/dist/extensions/*.d.ts", "../*"],
"@openclaw/*": ["../*"],
"openclaw/plugin-sdk/qa-channel": [

View file

@ -0,0 +1,41 @@
{
"name": "@openclaw/media-generation-core",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./capability-model-ref": {
"types": "./dist/capability-model-ref.d.mts",
"import": "./dist/capability-model-ref.mjs",
"default": "./dist/capability-model-ref.mjs"
},
"./catalog": {
"types": "./dist/catalog.d.mts",
"import": "./dist/catalog.mjs",
"default": "./dist/catalog.mjs"
},
"./model-ref": {
"types": "./dist/model-ref.d.mts",
"import": "./dist/model-ref.mjs",
"default": "./dist/model-ref.mjs"
},
"./normalization": {
"types": "./dist/normalization.d.mts",
"import": "./dist/normalization.mjs",
"default": "./dist/normalization.mjs"
}
},
"scripts": {
"build": "tsdown src/index.ts src/capability-model-ref.ts src/catalog.ts src/model-ref.ts src/normalization.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
}
}

View file

@ -1,4 +1,4 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeOptionalString } from "./string.js";
export type CapabilityModelProviderCandidate = {
id: string;
@ -19,10 +19,7 @@ function normalizeProviderForMatch(
normalizeProviderId: ProviderIdNormalizer | undefined,
): string | undefined {
const normalized = normalizeOptionalString(value);
if (!normalized) {
return undefined;
}
return normalizeProviderId ? normalizeProviderId(normalized) : normalized;
return normalized && normalizeProviderId ? normalizeProviderId(normalized) : normalized;
}
export function findCapabilityProviderById<T extends CapabilityModelProviderCandidate>(params: {
@ -36,11 +33,12 @@ export function findCapabilityProviderById<T extends CapabilityModelProviderCand
}
return params.providers.find((provider) => {
const providerId = normalizeProviderForMatch(provider.id, params.normalizeProviderId);
if (providerId === selectedProvider) {
return true;
}
return (provider.aliases ?? []).some(
(alias) => normalizeProviderForMatch(alias, params.normalizeProviderId) === selectedProvider,
return (
providerId === selectedProvider ||
(provider.aliases ?? []).some(
(alias) =>
normalizeProviderForMatch(alias, params.normalizeProviderId) === selectedProvider,
)
);
});
}

View file

@ -1,23 +1,27 @@
import type {
UnifiedModelCatalogEntry,
UnifiedModelCatalogKind,
UnifiedModelCatalogSource,
} from "../model-catalog/types.js";
import { normalizeUniqueSingleOrTrimmedStringList } from "../shared/string-normalization.js";
import { uniqueTrimmedStrings } from "./string.js";
export type MediaGenerationCatalogKind = Extract<
UnifiedModelCatalogKind,
"image_generation" | "video_generation" | "music_generation"
>;
export type MediaGenerationCatalogKind =
| "image_generation"
| "video_generation"
| "music_generation";
export type MediaGenerationCatalogSource = Extract<
UnifiedModelCatalogSource,
"static" | "live" | "cache" | "configured"
>;
export type MediaGenerationCatalogSource = "static" | "live" | "cache" | "configured";
export type MediaGenerationCatalogEntry<TCapabilities> = UnifiedModelCatalogEntry<TCapabilities> & {
export type MediaGenerationCatalogEntry<TCapabilities = unknown> = {
kind: MediaGenerationCatalogKind;
provider: string;
model: string;
label?: string;
source: MediaGenerationCatalogSource;
default?: boolean;
configured?: boolean;
capabilities?: TCapabilities;
modes?: readonly string[];
authEnvVars?: readonly string[];
docsPath?: string;
fetchedAt?: number;
expiresAt?: number;
warnings?: readonly string[];
};
export type MediaGenerationCatalogProvider<TCapabilities = unknown> = {
@ -30,10 +34,7 @@ export type MediaGenerationCatalogProvider<TCapabilities = unknown> = {
};
function uniqueModels(provider: { defaultModel?: string; models?: readonly string[] }): string[] {
return normalizeUniqueSingleOrTrimmedStringList([
provider.defaultModel,
...(provider.models ?? []),
]);
return uniqueTrimmedStrings([provider.defaultModel, ...(provider.models ?? [])]);
}
export function synthesizeMediaGenerationCatalogEntries<TCapabilities>(params: {
@ -41,8 +42,7 @@ export function synthesizeMediaGenerationCatalogEntries<TCapabilities>(params: {
provider: MediaGenerationCatalogProvider<TCapabilities>;
modes?: readonly string[];
}): Array<MediaGenerationCatalogEntry<TCapabilities>> {
const models = uniqueModels(params.provider);
return models.map((model) => {
return uniqueModels(params.provider).map((model) => {
const entry: MediaGenerationCatalogEntry<TCapabilities> = {
kind: params.kind,
provider: params.provider.id,

View file

@ -0,0 +1,4 @@
export * from "./capability-model-ref.js";
export * from "./catalog.js";
export * from "./model-ref.js";
export * from "./normalization.js";

View file

@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import {
resolveCapabilityModelRefForProviders,
resolveCapabilityProviderModelOnlyRef,
} from "./capability-model-ref.js";
import { parseGenerationModelRef } from "./model-ref.js";
describe("media-generation model refs", () => {
it("parses provider/model refs without splitting slash-containing model ids", () => {
expect(parseGenerationModelRef("fal/fal-ai/flux/dev")).toEqual({
provider: "fal",
model: "fal-ai/flux/dev",
});
});
it("rejects incomplete provider/model refs", () => {
expect(parseGenerationModelRef(undefined)).toBeNull();
expect(parseGenerationModelRef("openai")).toBeNull();
expect(parseGenerationModelRef("/gpt-image-2")).toBeNull();
expect(parseGenerationModelRef("openai/")).toBeNull();
});
it("resolves model-only refs from provider metadata", () => {
expect(
resolveCapabilityProviderModelOnlyRef({
raw: "fal-ai/flux/dev",
providers: [
{
id: "fal",
defaultModel: "fal-ai/flux/dev",
models: ["fal-ai/flux/dev/image-to-image"],
},
],
}),
).toEqual({ provider: "fal", model: "fal-ai/flux/dev" });
});
it("keeps explicit provider refs ahead of colliding model-only refs", () => {
expect(
resolveCapabilityModelRefForProviders({
raw: "google/lyria-3-pro-preview",
parseModelRef: parseGenerationModelRef,
providers: [
{
id: "google",
defaultModel: "lyria-3-clip-preview",
models: ["lyria-3-pro-preview"],
},
{
id: "openrouter",
defaultModel: "google/lyria-3-pro-preview",
},
],
}),
).toEqual({ provider: "google", model: "lyria-3-pro-preview" });
});
it("matches provider aliases through a caller-supplied normalizer", () => {
expect(
resolveCapabilityModelRefForProviders({
raw: "openai-codex/gpt-image-2",
parseModelRef: parseGenerationModelRef,
normalizeProviderId: (value) => value.toLowerCase(),
providers: [
{
id: "openai",
aliases: ["openai-codex"],
defaultModel: "gpt-image-2",
},
],
}),
).toEqual({ provider: "openai-codex", model: "gpt-image-2" });
});
});

View file

@ -1,6 +1,6 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeOptionalString } from "./string.js";
type ParsedGenerationModelRef = {
export type ParsedGenerationModelRef = {
provider: string;
model: string;
};
@ -16,8 +16,5 @@ export function parseGenerationModelRef(raw: string | undefined): ParsedGenerati
}
const provider = normalizeOptionalString(trimmed.slice(0, slashIndex));
const model = normalizeOptionalString(trimmed.slice(slashIndex + 1));
if (!provider || !model) {
return null;
}
return { provider, model };
return provider && model ? { provider, model } : null;
}

View file

@ -13,3 +13,15 @@ export type MediaGenerationNormalizationMetadataInput = {
resolution?: MediaNormalizationEntry<string>;
durationSeconds?: MediaNormalizationEntry<number>;
};
export function hasMediaNormalizationEntry<TValue extends MediaNormalizationValue>(
entry: MediaNormalizationEntry<TValue> | undefined,
): entry is MediaNormalizationEntry<TValue> {
return Boolean(
entry &&
(entry.requested !== undefined ||
entry.applied !== undefined ||
entry.derivedFrom !== undefined ||
(entry.supportedValues?.length ?? 0) > 0),
);
}

View file

@ -0,0 +1,21 @@
export function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
export function uniqueTrimmedStrings(values: readonly unknown[]): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
const normalized = normalizeOptionalString(value);
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
result.push(normalized);
}
return result;
}

View file

@ -13,6 +13,7 @@
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": [
"../../packages/media-generation-core/src/**/*.ts",
"../../src/plugin-sdk/**/*.ts",
"../../src/video-generation/dashscope-compatible.ts",
"../../src/video-generation/types.ts",
@ -21,6 +22,7 @@
"exclude": [
"node_modules",
"dist",
"../../packages/**/*.test.ts",
"../../src/**/*.test.ts",
"../../src/plugin-sdk/AGENTS.md",
"../../src/plugin-sdk/CLAUDE.md"

2
pnpm-lock.yaml generated
View file

@ -1812,6 +1812,8 @@ importers:
specifier: workspace:*
version: link:../llm-core
packages/media-generation-core: {}
packages/memory-host-sdk: {}
packages/net-policy:

View file

@ -53,6 +53,7 @@ export const BUILD_ALL_STEPS = [
"src/plugin-sdk",
"packages/llm-core/src",
"packages/memory-host-sdk/src",
"packages/media-generation-core/src",
"src/types",
"src/video-generation/dashscope-compatible.ts",
"src/video-generation/types.ts",

View file

@ -59,6 +59,24 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
"@openclaw/llm-core/types": ["../dist/plugin-sdk/packages/llm-core/src/types.d.ts"],
"@openclaw/llm-core/validation": ["../dist/plugin-sdk/packages/llm-core/src/validation.d.ts"],
"@openclaw/llm-core/*": ["../dist/plugin-sdk/packages/llm-core/src/*.d.ts"],
"@openclaw/media-generation-core": [
"../dist/plugin-sdk/packages/media-generation-core/src/index.d.ts",
],
"@openclaw/media-generation-core/capability-model-ref": [
"../dist/plugin-sdk/packages/media-generation-core/src/capability-model-ref.d.ts",
],
"@openclaw/media-generation-core/catalog": [
"../dist/plugin-sdk/packages/media-generation-core/src/catalog.d.ts",
],
"@openclaw/media-generation-core/model-ref": [
"../dist/plugin-sdk/packages/media-generation-core/src/model-ref.d.ts",
],
"@openclaw/media-generation-core/normalization": [
"../dist/plugin-sdk/packages/media-generation-core/src/normalization.d.ts",
],
"@openclaw/media-generation-core/*": [
"../dist/plugin-sdk/packages/media-generation-core/src/*.d.ts",
],
"@openclaw/*.js": ["../packages/plugin-sdk/dist/extensions/*.d.ts", "../extensions/*"],
"@openclaw/*": ["../packages/plugin-sdk/dist/extensions/*", "../extensions/*"],
"openclaw/plugin-sdk/qa-channel": ["../dist/plugin-sdk/src/plugin-sdk/qa-channel.d.ts"],

View file

@ -17,6 +17,7 @@ const PLUGIN_SDK_TYPE_INPUTS = [
"src/auto-reply",
"packages/llm-core/src",
"packages/memory-host-sdk/src",
"packages/media-generation-core/src",
"src/video-generation/dashscope-compatible.ts",
"src/video-generation/types.ts",
"src/types",
@ -32,6 +33,11 @@ const ROOT_DTS_REQUIRED_OUTPUTS = [
"dist/plugin-sdk/packages/llm-core/src/utils/diagnostics.d.ts",
"dist/plugin-sdk/packages/llm-core/src/utils/event-stream.d.ts",
"dist/plugin-sdk/packages/llm-core/src/validation.d.ts",
"dist/plugin-sdk/packages/media-generation-core/src/capability-model-ref.d.ts",
"dist/plugin-sdk/packages/media-generation-core/src/catalog.d.ts",
"dist/plugin-sdk/packages/media-generation-core/src/index.d.ts",
"dist/plugin-sdk/packages/media-generation-core/src/model-ref.d.ts",
"dist/plugin-sdk/packages/media-generation-core/src/normalization.d.ts",
"dist/plugin-sdk/error-runtime.d.ts",
"dist/plugin-sdk/plugin-entry.d.ts",
"dist/plugin-sdk/provider-auth.d.ts",
@ -40,6 +46,11 @@ const ROOT_DTS_REQUIRED_OUTPUTS = [
const PACKAGE_DTS_INPUTS = ["packages/plugin-sdk/tsconfig.json", ...PLUGIN_SDK_TYPE_INPUTS];
const PACKAGE_DTS_STAMP = "packages/plugin-sdk/dist/.boundary-dts.stamp";
const PACKAGE_DTS_REQUIRED_OUTPUTS = [
"packages/plugin-sdk/dist/packages/media-generation-core/src/capability-model-ref.d.ts",
"packages/plugin-sdk/dist/packages/media-generation-core/src/catalog.d.ts",
"packages/plugin-sdk/dist/packages/media-generation-core/src/index.d.ts",
"packages/plugin-sdk/dist/packages/media-generation-core/src/model-ref.d.ts",
"packages/plugin-sdk/dist/packages/media-generation-core/src/normalization.d.ts",
"packages/plugin-sdk/dist/src/plugin-sdk/error-runtime.d.ts",
"packages/plugin-sdk/dist/src/plugin-sdk/plugin-entry.d.ts",
"packages/plugin-sdk/dist/src/plugin-sdk/provider-auth.d.ts",

View file

@ -40,36 +40,48 @@ const STRICT_LITERAL_STRUCTS = new Set([
"PluginsSessionActionFailureResult",
]);
const DEFAULTED_OPTIONAL_INIT_PARAMS: Record<string, Set<string>> = {
SessionOperationEvent: new Set(["agentId"]),
SessionsCompactionListParams: new Set(["agentId"]),
SessionsCompactionGetParams: new Set(["agentId"]),
SessionsCompactionBranchParams: new Set(["agentId"]),
SessionsCompactionRestoreParams: new Set(["agentId"]),
SessionsSendParams: new Set(["agentId"]),
SessionsMessagesSubscribeParams: new Set(["agentId"]),
SessionsMessagesUnsubscribeParams: new Set(["agentId"]),
SessionsAbortParams: new Set(["agentId"]),
SessionsPatchParams: new Set(["agentId"]),
SessionsResetParams: new Set(["agentId"]),
SessionsDeleteParams: new Set(["agentId"]),
SessionsCompactParams: new Set(["agentId"]),
SessionsUsageParams: new Set(["agentId", "agentScope"]),
ChatHistoryParams: new Set(["agentId"]),
ChatSendParams: new Set(["agentId"]),
ChatAbortParams: new Set(["agentId"]),
ChatInjectParams: new Set(["agentId"]),
ChatDeltaEvent: new Set(["agentId"]),
ChatFinalEvent: new Set(["agentId"]),
ChatAbortedEvent: new Set(["agentId"]),
ChatErrorEvent: new Set(["agentId"]),
ArtifactsListParams: new Set(["agentId"]),
ArtifactsGetParams: new Set(["agentId"]),
ArtifactsDownloadParams: new Set(["agentId"]),
MessageActionParams: new Set(["inboundTurnKind"]),
CronRunLogEntry: new Set(["errorReason", "failureNotificationDelivery"]),
ExecApprovalRequestParams: new Set(["requireDeliveryRoute", "suppressDelivery"]),
};
const DEFAULTED_OPTIONAL_INIT_PARAM_ENTRIES: readonly [string, readonly string[]][] = [
["SessionOperationEvent", ["agentId"]],
["SessionsCompactionListParams", ["agentId"]],
["SessionsCompactionGetParams", ["agentId"]],
["SessionsCompactionBranchParams", ["agentId"]],
["SessionsCompactionRestoreParams", ["agentId"]],
["SessionsSendParams", ["agentId"]],
["SessionsMessagesSubscribeParams", ["agentId"]],
["SessionsMessagesUnsubscribeParams", ["agentId"]],
["SessionsAbortParams", ["agentId"]],
["SessionsPatchParams", ["agentId"]],
["SessionsResetParams", ["agentId"]],
["SessionsDeleteParams", ["agentId"]],
["SessionsCompactParams", ["agentId"]],
["SessionsUsageParams", ["agentId", "agentScope"]],
["ChatHistoryParams", ["agentId"]],
["ChatSendParams", ["agentId"]],
["ChatAbortParams", ["agentId"]],
["ChatInjectParams", ["agentId"]],
["ChatDeltaEvent", ["agentId"]],
["ChatFinalEvent", ["agentId"]],
["ChatAbortedEvent", ["agentId"]],
["ChatErrorEvent", ["agentId"]],
["ArtifactsListParams", ["agentId"]],
["ArtifactsGetParams", ["agentId"]],
["ArtifactsDownloadParams", ["agentId"]],
["ChatAbortParams", ["agentId"]],
["ChatAbortedEvent", ["agentId"]],
["ChatDeltaEvent", ["agentId"]],
["ChatErrorEvent", ["agentId"]],
["ChatFinalEvent", ["agentId"]],
["ChatHistoryParams", ["agentId"]],
["ChatInjectParams", ["agentId"]],
["ChatSendParams", ["agentId"]],
["MessageActionParams", ["inboundTurnKind"]],
["CronRunLogEntry", ["errorReason", "failureNotificationDelivery"]],
["ExecApprovalRequestParams", ["requireDeliveryRoute", "suppressDelivery"]],
];
const DEFAULTED_OPTIONAL_INIT_PARAMS: Record<string, Set<string>> = Object.fromEntries(
DEFAULTED_OPTIONAL_INIT_PARAM_ENTRIES.map(([name, params]) => [name, new Set(params)]),
);
const header = `// Generated by scripts/protocol-gen-swift.ts — do not edit by hand\n// swiftlint:disable file_length\nimport Foundation\n\npublic let GATEWAY_PROTOCOL_VERSION = ${PROTOCOL_VERSION}\npublic let GATEWAY_MIN_PROTOCOL_VERSION = ${MIN_CLIENT_PROTOCOL_VERSION}\n\nprivate struct GatewayAnyCodingKey: CodingKey, Hashable {\n let stringValue: String\n let intValue: Int?\n\n init?(stringValue: String) {\n self.stringValue = stringValue\n self.intValue = nil\n }\n\n init?(intValue: Int) {\n self.stringValue = String(intValue)\n self.intValue = intValue\n }\n}\n\npublic enum ErrorCode: String, Codable, Sendable {\n${Object.values(
ErrorCodes,

View file

@ -10,6 +10,7 @@ const RUN_NODE_PACKAGE_SOURCE_ROOTS = [
// src/ so edits restart the same process that consumes them.
"packages/gateway-client/src",
"packages/gateway-protocol/src",
"packages/media-generation-core/src",
"packages/net-policy/src",
];

View file

@ -5,6 +5,8 @@ import { MAX_SAFE_TIMEOUT_DELAY_MS } from "../utils/timer-delay.js";
type StrictInlineEvalBoundary =
typeof import("./bash-tools.exec-host-shared.js").enforceStrictInlineEvalApprovalBoundary;
type ExecAutoReviewer = typeof import("../infra/exec-auto-review.js").defaultExecAutoReviewer;
type ExecAsk = import("../infra/exec-approvals.js").ExecAsk;
type ExecSecurity = import("../infra/exec-approvals.js").ExecSecurity;
type MockAllowlistSegment = {
raw?: string;
resolution: null;
@ -17,11 +19,21 @@ type MockAllowlistResult = {
segments: MockAllowlistSegment[];
segmentAllowlistEntries: unknown[];
};
type MockExecApprovalAllowlistEntry = {
type MockExecAllowlistEntry = {
pattern: string;
source?: string;
source?: "allow-always";
commandText?: string;
};
type MockExecApprovalsResolved = {
allowlist: MockExecAllowlistEntry[];
file: { version: 1; agents: Record<string, unknown> };
agent: {
security: ExecSecurity;
ask: ExecAsk;
askFallback: "deny";
autoAllowSkills: false;
};
};
const INLINE_EVAL_HIT = {
executable: "python3",
@ -60,16 +72,18 @@ const evaluateShellAllowlistMock = vi.hoisted(() =>
),
);
const resolveExecApprovalsFromFileMock = vi.hoisted(() =>
vi.fn(() => ({
allowlist: [] as MockExecApprovalAllowlistEntry[],
file: { version: 1, agents: {} },
agent: {
security: "full",
ask: "off",
askFallback: "deny",
autoAllowSkills: false,
},
})),
vi.fn(
(): MockExecApprovalsResolved => ({
allowlist: [],
file: { version: 1, agents: {} },
agent: {
security: "full",
ask: "off",
askFallback: "deny",
autoAllowSkills: false,
},
}),
),
);
const requiresExecApprovalMock = vi.hoisted(() => vi.fn(() => true));
const hasDurableExecApprovalMock = vi.hoisted(() => vi.fn(() => false));

View file

@ -1,9 +1,9 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
listMediaGenerationProviderModels,
synthesizeMediaGenerationCatalogEntries,
type MediaGenerationCatalogKind,
} from "../../media-generation/catalog.js";
} from "../../../packages/media-generation-core/src/catalog.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { getProviderEnvVars } from "../../secrets/provider-env-vars.js";
import type { AuthProfileStore } from "../auth-profiles/types.js";
import { isCapabilityProviderConfigured } from "./media-tool-shared.js";

View file

@ -1,11 +1,11 @@
import {
findCapabilityProviderById,
resolveCapabilityModelRefForProviders,
} from "../../../packages/media-generation-core/src/capability-model-ref.js";
import type { AgentModelConfig } from "../../config/types.agents-shared.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { SsrFPolicy } from "../../infra/net/ssrf.js";
import type { Model } from "../../llm/types.js";
import {
findCapabilityProviderById,
resolveCapabilityModelRefForProviders,
} from "../../media-generation/capability-model-ref.js";
import { resolveChannelInboundAttachmentRootsForChannel } from "../../media/channel-inbound-roots.js";
import { normalizeInboundPathRoots } from "../../media/inbound-path-policy.js";
import { getDefaultLocalRoots } from "../../media/local-media-access.js";

View file

@ -1,4 +1,4 @@
import { parseGenerationModelRef } from "../media-generation/model-ref.js";
import { parseGenerationModelRef } from "../../packages/media-generation-core/src/model-ref.js";
export function parseImageGenerationModelRef(
raw: string | undefined,

View file

@ -1,7 +1,7 @@
import type { MediaNormalizationEntry } from "../../packages/media-generation-core/src/normalization.js";
import type { AuthProfileStore } from "../agents/auth-profiles/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import type { MediaNormalizationEntry } from "../media-generation/normalization.types.js";
export type GeneratedImageAsset = {
buffer: Buffer;

View file

@ -342,6 +342,7 @@ describe("openHttpConnectTunnel", () => {
targetPort: 443,
timeoutMs: 1,
});
void tunnel.catch(() => undefined);
const rejected = expect(tunnel).rejects.toThrow(
"Proxy CONNECT failed via http://proxy.example:8080: Proxy CONNECT timed out after 1ms",
);
@ -364,6 +365,7 @@ describe("openHttpConnectTunnel", () => {
targetPort: 443,
timeoutMs: Number.MAX_SAFE_INTEGER,
});
void tunnel.catch(() => undefined);
const rejected = expect(tunnel).rejects.toThrow(
`Proxy CONNECT failed via http://proxy.example:8080: Proxy CONNECT timed out after ${MAX_TIMER_TIMEOUT_MS}ms`,
);

View file

@ -132,6 +132,7 @@ describe("watch-node script", () => {
expect(watchPaths).toContain("extensions");
expect(watchPaths).toContain("packages/gateway-client/src");
expect(watchPaths).toContain("packages/gateway-protocol/src");
expect(watchPaths).toContain("packages/media-generation-core/src");
expect(watchPaths).toContain("packages/net-policy/src");
expect(watchPaths).toContain("tsdown.config.ts");
expect(watchOptions.ignoreInitial).toBe(true);
@ -140,6 +141,10 @@ describe("watch-node script", () => {
expect(watchOptions.ignored("packages/gateway-client/src/client.ts")).toBe(false);
expect(watchOptions.ignored("packages/gateway-client/src/client.test.ts")).toBe(true);
expect(watchOptions.ignored("packages/gateway-protocol/src/schema/cron.ts")).toBe(false);
expect(watchOptions.ignored("packages/media-generation-core/src/model-ref.ts")).toBe(false);
expect(watchOptions.ignored("packages/media-generation-core/src/model-ref.test.ts")).toBe(
true,
);
expect(watchOptions.ignored("packages/net-policy/src/ip.ts")).toBe(false);
expect(watchOptions.ignored("packages/net-policy/src/ip.test.ts")).toBe(true);
expect(watchOptions.ignored("extensions")).toBe(false);

View file

@ -1,3 +1,5 @@
import { resolveCapabilityModelRefForProviders } from "../../packages/media-generation-core/src/capability-model-ref.js";
import type { MediaGenerationNormalizationMetadataInput } from "../../packages/media-generation-core/src/normalization.js";
import { listProfilesForProvider } from "../agents/auth-profiles.js";
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
@ -14,23 +16,17 @@ import { formatErrorMessage } from "../infra/errors.js";
import { getProviderEnvVars as getDefaultProviderEnvVars } from "../secrets/provider-env-vars.js";
import { clampTimerTimeoutMs } from "../shared/number-coercion.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveCapabilityModelRefForProviders } from "./capability-model-ref.js";
import type {
export type {
MediaGenerationNormalizationMetadataInput,
MediaNormalizationEntry,
MediaNormalizationValue,
} from "./normalization.types.js";
} from "../../packages/media-generation-core/src/normalization.js";
export { hasMediaNormalizationEntry } from "../../packages/media-generation-core/src/normalization.js";
export type ParsedProviderModelRef = {
provider: string;
model: string;
};
export type {
MediaGenerationNormalizationMetadataInput,
MediaNormalizationEntry,
MediaNormalizationValue,
} from "./normalization.types.js";
export function recordCapabilityCandidateFailure(params: {
attempts: FallbackAttempt[];
provider: string;
@ -48,18 +44,6 @@ export function recordCapabilityCandidateFailure(params: {
});
}
export function hasMediaNormalizationEntry<TValue extends MediaNormalizationValue>(
entry: MediaNormalizationEntry<TValue> | undefined,
): entry is MediaNormalizationEntry<TValue> {
return Boolean(
entry &&
(entry.requested !== undefined ||
entry.applied !== undefined ||
entry.derivedFrom !== undefined ||
(entry.supportedValues?.length ?? 0) > 0),
);
}
const IMAGE_RESOLUTION_ORDER = ["1K", "2K", "4K"] as const;
export function resolveMediaProviderDefaultTimeoutMs(

View file

@ -1,4 +1,4 @@
import { parseGenerationModelRef } from "../media-generation/model-ref.js";
import { parseGenerationModelRef } from "../../packages/media-generation-core/src/model-ref.js";
export function parseMusicGenerationModelRef(
raw: string | undefined,

View file

@ -1,6 +1,6 @@
import type { MediaNormalizationEntry } from "../../packages/media-generation-core/src/normalization.js";
import type { AuthProfileStore } from "../agents/auth-profiles/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { MediaNormalizationEntry } from "../media-generation/normalization.types.js";
export type MusicGenerationOutputFormat = "mp3" | "wav";

View file

@ -194,6 +194,7 @@ describe("opt-in extension package boundaries", () => {
expect(tsconfig.compilerOptions?.outDir).toBe("dist");
expect(tsconfig.compilerOptions?.rootDir).toBe("../..");
expect(tsconfig.include).toEqual([
"../../packages/media-generation-core/src/**/*.ts",
"../../src/plugin-sdk/**/*.ts",
"../../src/video-generation/dashscope-compatible.ts",
"../../src/video-generation/types.ts",

View file

@ -1,13 +1,13 @@
import {
synthesizeMediaGenerationCatalogEntries,
type MediaGenerationCatalogKind,
type MediaGenerationCatalogProvider,
} from "../../packages/media-generation-core/src/catalog.js";
import {
synthesizeVoiceModelCatalogEntries,
type VoiceModelCapabilities,
type VoiceModelProvider,
} from "../../packages/speech-core/voice-models.js";
import {
synthesizeMediaGenerationCatalogEntries,
type MediaGenerationCatalogKind,
type MediaGenerationCatalogProvider,
} from "../media-generation/catalog.js";
import type {
UnifiedModelCatalogEntry,
UnifiedModelCatalogSource,

View file

@ -1378,6 +1378,18 @@ describe("plugin sdk alias helpers", () => {
srcFile: "index.ts",
distFile: "index.mjs",
});
const mediaGenerationCore = writeWorkspacePackageEntry({
root: fixture.root,
packageDir: "media-generation-core",
srcFile: "index.ts",
distFile: "index.mjs",
});
const mediaGenerationModelRef = writeWorkspacePackageEntry({
root: fixture.root,
packageDir: "media-generation-core",
srcFile: "model-ref.ts",
distFile: "model-ref.mjs",
});
const netPolicyIp = writeWorkspacePackageEntry({
root: fixture.root,
packageDir: "net-policy",
@ -1388,6 +1400,8 @@ describe("plugin sdk alias helpers", () => {
fs.rmSync(gatewayClientTimeouts.distFile);
fs.rmSync(gatewayProtocol.distFile);
fs.rmSync(gatewayProtocolSchema.distFile);
fs.rmSync(mediaGenerationCore.distFile);
fs.rmSync(mediaGenerationModelRef.distFile);
fs.rmSync(netPolicy.distFile);
fs.rmSync(netPolicyIp.distFile);
const sourcePluginEntry = writePluginEntry(
@ -1411,6 +1425,12 @@ describe("plugin sdk alias helpers", () => {
expect(fs.realpathSync(aliases["@openclaw/gateway-protocol/schema"] ?? "")).toBe(
fs.realpathSync(gatewayProtocolSchema.srcFile),
);
expect(fs.realpathSync(aliases["@openclaw/media-generation-core"] ?? "")).toBe(
fs.realpathSync(mediaGenerationCore.srcFile),
);
expect(fs.realpathSync(aliases["@openclaw/media-generation-core/model-ref"] ?? "")).toBe(
fs.realpathSync(mediaGenerationModelRef.srcFile),
);
expect(fs.realpathSync(aliases["@openclaw/net-policy"] ?? "")).toBe(
fs.realpathSync(netPolicy.srcFile),
);
@ -1433,6 +1453,12 @@ describe("plugin sdk alias helpers", () => {
srcFile: "connect-error-details.ts",
distFile: "connect-error-details.mjs",
});
const mediaGenerationCore = writeWorkspacePackageEntry({
root: fixture.root,
packageDir: "media-generation-core",
srcFile: "catalog.ts",
distFile: "catalog.mjs",
});
const netPolicy = writeWorkspacePackageEntry({
root: fixture.root,
packageDir: "net-policy",
@ -1454,6 +1480,9 @@ describe("plugin sdk alias helpers", () => {
expect(fs.realpathSync(aliases["@openclaw/gateway-protocol/connect-error-details"] ?? "")).toBe(
fs.realpathSync(gatewayProtocol.distFile),
);
expect(fs.realpathSync(aliases["@openclaw/media-generation-core/catalog"] ?? "")).toBe(
fs.realpathSync(mediaGenerationCore.distFile),
);
expect(fs.realpathSync(aliases["@openclaw/net-policy/redact-sensitive-url"] ?? "")).toBe(
fs.realpathSync(netPolicy.distFile),
);

View file

@ -560,6 +560,41 @@ const WORKSPACE_PACKAGE_ALIAS_ENTRIES = [
srcFile: "version.ts",
distFile: "version.mjs",
},
{
packageName: "@openclaw/media-generation-core",
packageDir: "media-generation-core",
subpath: "",
srcFile: "index.ts",
distFile: "index.mjs",
},
{
packageName: "@openclaw/media-generation-core",
packageDir: "media-generation-core",
subpath: "capability-model-ref",
srcFile: "capability-model-ref.ts",
distFile: "capability-model-ref.mjs",
},
{
packageName: "@openclaw/media-generation-core",
packageDir: "media-generation-core",
subpath: "catalog",
srcFile: "catalog.ts",
distFile: "catalog.mjs",
},
{
packageName: "@openclaw/media-generation-core",
packageDir: "media-generation-core",
subpath: "model-ref",
srcFile: "model-ref.ts",
distFile: "model-ref.mjs",
},
{
packageName: "@openclaw/media-generation-core",
packageDir: "media-generation-core",
subpath: "normalization",
srcFile: "normalization.ts",
distFile: "normalization.mjs",
},
{
packageName: "@openclaw/net-policy",
packageDir: "net-policy",

View file

@ -1,4 +1,4 @@
import { parseGenerationModelRef } from "../media-generation/model-ref.js";
import { parseGenerationModelRef } from "../../packages/media-generation-core/src/model-ref.js";
export function parseVideoGenerationModelRef(
raw: string | undefined,

View file

@ -1,6 +1,6 @@
import type { MediaNormalizationEntry } from "../../packages/media-generation-core/src/normalization.js";
import type { AuthProfileStore } from "../agents/auth-profiles/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { MediaNormalizationEntry } from "../media-generation/normalization.types.js";
export type GeneratedVideoAsset = {
/** Raw video bytes. Required for local delivery; omit when url is provided instead. */

View file

@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createWizardPrompter } from "../../test/helpers/wizard-prompter.js";
import { createNonExitingRuntime } from "../runtime.js";
import type { WizardPrompter } from "./prompts.js";
import type { WizardMultiSelectParams, WizardPrompter } from "./prompts.js";
const ensureOnboardingPluginInstalled = vi.hoisted(() =>
vi.fn(async ({ cfg }: { cfg: Record<string, unknown> }) => ({
@ -92,9 +92,9 @@ describe("setupOfficialPluginInstalls", () => {
});
it("installs selected optional official plugins through the shared onboarding installer", async () => {
const multiselect = vi.fn(async () => ["diagnostics-otel"]);
const multiselect = vi.fn(async (_params: WizardMultiSelectParams) => ["diagnostics-otel"]);
const prompter = createWizardPrompter({
multiselect: multiselect as WizardPrompter["multiselect"],
multiselect: multiselect as unknown as WizardPrompter["multiselect"],
});
const runtime = createNonExitingRuntime();
@ -105,14 +105,19 @@ describe("setupOfficialPluginInstalls", () => {
workspaceDir: "/tmp/workspace",
});
expect(multiselect).toHaveBeenCalledExactlyOnceWith({
message: "Install optional plugins",
options: [
{
value: "__skip__",
label: "Skip for now",
hint: "Continue without installing optional plugins",
},
expect(multiselect).toHaveBeenCalledTimes(1);
const prompt = multiselect.mock.calls[0]?.[0];
if (!prompt) {
throw new Error("expected optional plugin multiselect prompt");
}
expect(prompt.message).toBe("Install optional plugins");
expect(prompt.options[0]).toEqual({
value: "__skip__",
label: "Skip for now",
hint: "Continue without installing optional plugins",
});
expect(prompt.options).toEqual(
expect.arrayContaining([
{
value: "acpx",
label: "ACPX Runtime",
@ -128,58 +133,8 @@ describe("setupOfficialPluginInstalls", () => {
label: "Diagnostics Prometheus",
hint: "OpenClaw diagnostics Prometheus exporter",
},
{
value: "diffs-language-pack",
label: "Diff Viewer Language Pack",
hint: "OpenClaw diffs viewer syntax highlighting language pack",
},
{
value: "diffs",
label: "Diffs",
hint: "OpenClaw diff viewer plugin",
},
{
value: "copilot",
label: "GitHub Copilot agent runtime",
hint: "OpenClaw GitHub Copilot agent runtime plugin",
},
{
value: "google-meet",
label: "Google Meet",
hint: "OpenClaw Google Meet participant plugin",
},
{
value: "lobster",
label: "Lobster",
hint: "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
},
{
value: "memory-lancedb",
label: "Memory LanceDB",
hint: "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
},
{
value: "openshell",
label: "OpenShell Sandbox",
hint: "OpenClaw OpenShell sandbox backend",
},
{
value: "pixverse",
label: "PixVerse",
hint: "OpenClaw PixVerse video generation provider plugin",
},
{
value: "tokenjuice",
label: "Tokenjuice",
hint: "OpenClaw tokenjuice exec output compaction plugin",
},
{
value: "voice-call",
label: "Voice Call",
hint: "OpenClaw voice-call plugin",
},
],
});
]),
);
expect(ensureOnboardingPluginInstalled).toHaveBeenCalledExactlyOnceWith({
cfg: {},
entry: {

View file

@ -1,5 +1,12 @@
import { spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
import {
chmodSync,
mkdirSync,
mkdtempSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

View file

@ -51,6 +51,20 @@
],
"@openclaw/gateway-protocol/version": ["./packages/gateway-protocol/src/version.ts"],
"@openclaw/gateway-protocol/*": ["./packages/gateway-protocol/src/*"],
"@openclaw/media-generation-core": ["./packages/media-generation-core/src/index.ts"],
"@openclaw/media-generation-core/capability-model-ref": [
"./packages/media-generation-core/src/capability-model-ref.ts"
],
"@openclaw/media-generation-core/catalog": [
"./packages/media-generation-core/src/catalog.ts"
],
"@openclaw/media-generation-core/model-ref": [
"./packages/media-generation-core/src/model-ref.ts"
],
"@openclaw/media-generation-core/normalization": [
"./packages/media-generation-core/src/normalization.ts"
],
"@openclaw/media-generation-core/*": ["./packages/media-generation-core/src/*"],
"@openclaw/net-policy": ["./packages/net-policy/src/index.ts"],
"@openclaw/net-policy/ip": ["./packages/net-policy/src/ip.ts"],
"@openclaw/net-policy/ipv4": ["./packages/net-policy/src/ipv4.ts"],

View file

@ -14,10 +14,11 @@
"include": [
"src/plugin-sdk/**/*.ts",
"packages/llm-core/src/**/*.ts",
"packages/media-generation-core/src/**/*.ts",
"packages/memory-host-sdk/src/**/*.ts",
"src/video-generation/dashscope-compatible.ts",
"src/video-generation/types.ts",
"src/types/**/*.d.ts"
],
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
"exclude": ["node_modules", "dist", "packages/**/*.test.ts", "src/**/*.test.ts"]
}

View file

@ -384,6 +384,16 @@ function buildNetPolicyDistEntries(): Record<string, string> {
};
}
function buildMediaGenerationCoreDistEntries(): Record<string, string> {
return {
index: "packages/media-generation-core/src/index.ts",
"capability-model-ref": "packages/media-generation-core/src/capability-model-ref.ts",
catalog: "packages/media-generation-core/src/catalog.ts",
"model-ref": "packages/media-generation-core/src/model-ref.ts",
normalization: "packages/media-generation-core/src/normalization.ts",
};
}
function buildSpeechCoreDistEntries(): Record<string, string> {
return {
api: "packages/speech-core/api.ts",
@ -523,6 +533,12 @@ export default defineConfig([
neverBundle: shouldExternalizeNetPolicyDependency,
},
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,
entry: buildMediaGenerationCoreDistEntries(),
outDir: "packages/media-generation-core/dist",
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,