mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-08 23:14:17 +08:00
Compare commits
156 Commits
fix/plugin
...
feat/code-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2f893c14a | ||
|
|
f44af7eebf | ||
|
|
65fe2b7e91 | ||
|
|
941e04e9f3 | ||
|
|
f327073fb3 | ||
|
|
41e5acbb6c | ||
|
|
2333d47a1e | ||
|
|
c9e481ac48 | ||
|
|
462e315953 | ||
|
|
6b14df7792 | ||
|
|
e449392c4f | ||
|
|
326db58229 | ||
|
|
3caf4facec | ||
|
|
c9a97f54e0 | ||
|
|
85506c36a0 | ||
|
|
a176b8ec2f | ||
|
|
2b726457d8 | ||
|
|
6464f8d1d9 | ||
|
|
a17c7a56da | ||
|
|
98a1aa491f | ||
|
|
25b87b111d | ||
|
|
f823123aa5 | ||
|
|
d717ff71bf | ||
|
|
840192caa9 | ||
|
|
61ef6b12dd | ||
|
|
660a6dec7f | ||
|
|
e49ef86945 | ||
|
|
d2f69ecc3b | ||
|
|
a89abcb1e9 | ||
|
|
8bf7bc5b5c | ||
|
|
4e2ef87c31 | ||
|
|
ec58491f75 | ||
|
|
0840fea50d | ||
|
|
cf60e83118 | ||
|
|
7ad2ebb515 | ||
|
|
3c41e1722f | ||
|
|
dd5b70bcc4 | ||
|
|
30c0422a8e | ||
|
|
6d43200248 | ||
|
|
be3153cabb | ||
|
|
56995069f1 | ||
|
|
2238e0ce76 | ||
|
|
38a463fe93 | ||
|
|
e1f462b352 | ||
|
|
ccd635fdb9 | ||
|
|
27dce6c6bb | ||
|
|
9c08d8cd35 | ||
|
|
dc5b3ecc4c | ||
|
|
95f66a34e7 | ||
|
|
1695ee2f43 | ||
|
|
801520b0f0 | ||
|
|
8ba79d72b4 | ||
|
|
5876ba6152 | ||
|
|
5b895f2592 | ||
|
|
fb61363763 | ||
|
|
07e0af44b3 | ||
|
|
059d5405fe | ||
|
|
cd37dbd4e5 | ||
|
|
3e8d06a6be | ||
|
|
2f07e4e6c0 | ||
|
|
15fb3314de | ||
|
|
5a019e7725 | ||
|
|
aea31934d4 | ||
|
|
8ec7e80cb2 | ||
|
|
6c3533d8c4 | ||
|
|
9c313a7826 | ||
|
|
368a719879 | ||
|
|
ec7e3eaf64 | ||
|
|
8bcdab8933 | ||
|
|
c2f0d811e7 | ||
|
|
8f3d3a549d | ||
|
|
d389a52494 | ||
|
|
346b14a51a | ||
|
|
ffa2da8478 | ||
|
|
61a768be75 | ||
|
|
3d8a77a113 | ||
|
|
a6a358f1a6 | ||
|
|
131dc4eaeb | ||
|
|
022fd55bad | ||
|
|
d9820e4098 | ||
|
|
a4ebdc9aa1 | ||
|
|
cf2461f7f6 | ||
|
|
f5f829db79 | ||
|
|
a06daab97e | ||
|
|
09f094057a | ||
|
|
9def042fab | ||
|
|
f6adea5757 | ||
|
|
78f4a5c05f | ||
|
|
731a7af9c5 | ||
|
|
ffa4342a6a | ||
|
|
550a134cf9 | ||
|
|
1b43e84d0d | ||
|
|
31f0635f4f | ||
|
|
1c65e2e7c1 | ||
|
|
b6f3fe7938 | ||
|
|
d65b3a68aa | ||
|
|
e2b54fecd8 | ||
|
|
b8067d073a | ||
|
|
e420c001d0 | ||
|
|
44b6b79a66 | ||
|
|
3ef2935ac9 | ||
|
|
fced29de17 | ||
|
|
4f074c3235 | ||
|
|
5df00520cb | ||
|
|
b2c85bc0a2 | ||
|
|
5e2e78a75a | ||
|
|
2196f107da | ||
|
|
ff56a2d7b3 | ||
|
|
24cff8a3bc | ||
|
|
b495ac2abb | ||
|
|
3f2585424d | ||
|
|
9d1a3007d9 | ||
|
|
b5c163dffa | ||
|
|
ee0cf9e5bb | ||
|
|
37fdfa0e0b | ||
|
|
d550b804b8 | ||
|
|
05988500bc | ||
|
|
b01290cf64 | ||
|
|
117f6fb254 | ||
|
|
c363816fea | ||
|
|
aeed31cdb1 | ||
|
|
58c8c022c5 | ||
|
|
2cfae61743 | ||
|
|
c6b4daf426 | ||
|
|
348fabe04d | ||
|
|
6c83e8e7e4 | ||
|
|
817b6259c4 | ||
|
|
959af0fa5b | ||
|
|
669b26a3dc | ||
|
|
67c139fc36 | ||
|
|
8b6829e1bc | ||
|
|
86e6fbcf52 | ||
|
|
9b4b3aa348 | ||
|
|
51ab2c0d79 | ||
|
|
bdd9c70787 | ||
|
|
1ff95ff3e6 | ||
|
|
7c5b55c5ff | ||
|
|
b0d6076208 | ||
|
|
4385e57dce | ||
|
|
eb45c1c623 | ||
|
|
adf981de89 | ||
|
|
023a101b91 | ||
|
|
8b92aca27f | ||
|
|
b13fb788b5 | ||
|
|
87c0ee7685 | ||
|
|
eef32e94c7 | ||
|
|
1350efcfd8 | ||
|
|
e7ef051149 | ||
|
|
2b5ddf8f2a | ||
|
|
6f655573d3 | ||
|
|
8aabf45ddb | ||
|
|
4d4748e807 | ||
|
|
439c09668e | ||
|
|
54bbe87cd5 | ||
|
|
6804b7cb71 | ||
|
|
63470e99f0 |
@@ -6,18 +6,10 @@ class: standard
|
||||
capacity:
|
||||
market: spot
|
||||
strategy: most-available
|
||||
fallback: on-demand-after-120s
|
||||
# Fail closed instead of silently falling back to on-demand while the
|
||||
# Azure-backed billing account is the default runner path.
|
||||
fallback: spot-only
|
||||
hints: true
|
||||
availabilityZones:
|
||||
- eu-west-1a
|
||||
- eu-west-1b
|
||||
- eu-west-1c
|
||||
regions:
|
||||
- eu-west-1
|
||||
- eu-west-2
|
||||
- eu-central-1
|
||||
- us-east-1
|
||||
- us-west-2
|
||||
actions:
|
||||
workflow: .github/workflows/crabbox-hydrate.yml
|
||||
# Default AWS hydration uses local Actions replay. Use
|
||||
@@ -37,6 +29,8 @@ blacksmith:
|
||||
job: check
|
||||
ref: main
|
||||
aws:
|
||||
# AWS-specific overrides still pin direct `--provider aws` runs without
|
||||
# leaking AWS region names into the Azure default capacity fallback list.
|
||||
region: eu-west-1
|
||||
rootGB: 400
|
||||
sync:
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -1420,10 +1420,12 @@ jobs:
|
||||
find src \
|
||||
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
|
||||
-exec touch -t 200001010000 {} +
|
||||
find packages/llm-core/src \
|
||||
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
|
||||
-exec touch -t 200001010000 {} +
|
||||
touch -t 200001010000 \
|
||||
if [ -d packages/llm-core/src ]; then
|
||||
find packages/llm-core/src \
|
||||
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
|
||||
-exec touch -t 200001010000 {} +
|
||||
fi
|
||||
cache_inputs=(
|
||||
tsconfig.json \
|
||||
tsconfig.plugin-sdk.dts.json \
|
||||
packages/plugin-sdk/tsconfig.json \
|
||||
@@ -1435,6 +1437,12 @@ jobs:
|
||||
scripts/lib/plugin-sdk-entries.mjs \
|
||||
package.json \
|
||||
pnpm-lock.yaml
|
||||
)
|
||||
for cache_input in "${cache_inputs[@]}"; do
|
||||
if [ -e "$cache_input" ]; then
|
||||
touch -t 200001010000 "$cache_input"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Run additional check shard
|
||||
env:
|
||||
|
||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
|
||||
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
|
||||
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
|
||||
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
|
||||
@@ -33,6 +34,16 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.
|
||||
- Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.
|
||||
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, changelog restore, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
|
||||
- Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.
|
||||
- Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.
|
||||
- CI/Crabbox: keep default runner capacity spot-only and provider-neutral so OpenClaw remote validation does not silently fall back to on-demand leases or stale AWS region hints.
|
||||
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
|
||||
- CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.
|
||||
- CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.
|
||||
- CI/tooling: route package, release, and install helper edits to their owner tests so changed-test gates cover publish and installer script changes.
|
||||
- CI/tooling: route shared script library edits through their owner tests so lock, process, safety, and scan helpers do not skip changed-test coverage.
|
||||
- CI/tooling: skip expensive import-graph scans once a changed diff already requires broad fallback, keeping local changed-test planning fast while still collecting explicit owner tests.
|
||||
- CI/tooling: route script edits through conventional owner tests when matching `test/scripts` or `src/scripts` coverage already exists.
|
||||
- Performance: reuse prepared provider handles, strict tool schemas, gateway runtime metadata, session maintenance config, plugin metadata, bundled skill allowlists, package-local plugin artifacts, and single-entry store writes.
|
||||
|
||||
## 2026.5.28
|
||||
|
||||
@@ -17,6 +17,13 @@ export {
|
||||
import { buildAnthropicVertexProvider } from "./provider-catalog.js";
|
||||
import { hasAnthropicVertexAvailableAuth } from "./region.js";
|
||||
|
||||
let streamRuntimeModulePromise: Promise<typeof import("./stream-runtime.js")> | null = null;
|
||||
|
||||
const loadStreamRuntimeModule = async () => {
|
||||
streamRuntimeModulePromise ??= import("./stream-runtime.js");
|
||||
return await streamRuntimeModulePromise;
|
||||
};
|
||||
|
||||
export function mergeImplicitAnthropicVertexProvider(params: {
|
||||
existing?: ReturnType<typeof buildAnthropicVertexProvider>;
|
||||
implicit: ReturnType<typeof buildAnthropicVertexProvider>;
|
||||
@@ -50,7 +57,7 @@ export function createAnthropicVertexStreamFn(
|
||||
baseURL?: string,
|
||||
deps?: AnthropicVertexStreamDeps,
|
||||
): StreamFn {
|
||||
const streamFnPromise = import("./stream-runtime.js").then((runtime) =>
|
||||
const streamFnPromise = loadStreamRuntimeModule().then((runtime) =>
|
||||
runtime.createAnthropicVertexStreamFn(projectId, region, baseURL, deps),
|
||||
);
|
||||
return async (model, context, options) => {
|
||||
@@ -64,7 +71,7 @@ export function createAnthropicVertexStreamFnForModel(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
deps?: AnthropicVertexStreamDeps,
|
||||
): StreamFn {
|
||||
const streamFnPromise = import("./stream-runtime.js").then((runtime) =>
|
||||
const streamFnPromise = loadStreamRuntimeModule().then((runtime) =>
|
||||
runtime.createAnthropicVertexStreamFnForModel(model, env, deps),
|
||||
);
|
||||
return async (...args) => {
|
||||
|
||||
@@ -15,6 +15,15 @@ import { BrowserToolSchema } from "./src/browser-tool.schema.js";
|
||||
|
||||
const EAGER_BROWSER_CONTROL_SERVICE_ENV = "OPENCLAW_EAGER_BROWSER_CONTROL_SERVER";
|
||||
|
||||
let browserRegistrationRuntimeModulePromise: Promise<
|
||||
typeof import("./register.runtime.js")
|
||||
> | null = null;
|
||||
|
||||
const loadBrowserRegistrationRuntimeModule = async () => {
|
||||
browserRegistrationRuntimeModulePromise ??= import("./register.runtime.js");
|
||||
return await browserRegistrationRuntimeModulePromise;
|
||||
};
|
||||
|
||||
function isTruthyEnvValue(value: string | undefined): boolean {
|
||||
return /^(?:1|true|yes|on)$/iu.test(value?.trim() ?? "");
|
||||
}
|
||||
@@ -51,7 +60,7 @@ function createLazyBrowserTool(opts?: {
|
||||
].join(" "),
|
||||
parameters: BrowserToolSchema,
|
||||
execute: async (toolCallId, args, signal, onUpdate) => {
|
||||
const { createBrowserTool } = await import("./register.runtime.js");
|
||||
const { createBrowserTool } = await loadBrowserRegistrationRuntimeModule();
|
||||
const tool = createBrowserTool(opts);
|
||||
return await tool.execute(toolCallId, args, signal, onUpdate);
|
||||
},
|
||||
@@ -65,7 +74,7 @@ export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
|
||||
command: "browser.proxy",
|
||||
cap: "browser",
|
||||
handle: async (paramsJSON) => {
|
||||
const { runBrowserProxyCommand } = await import("./register.runtime.js");
|
||||
const { runBrowserProxyCommand } = await loadBrowserRegistrationRuntimeModule();
|
||||
return await runBrowserProxyCommand(paramsJSON);
|
||||
},
|
||||
},
|
||||
@@ -73,7 +82,7 @@ export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
|
||||
|
||||
export const browserSecurityAuditCollectors: OpenClawPluginSecurityAuditCollector[] = [
|
||||
async (ctx) => {
|
||||
const { collectBrowserSecurityAuditFindings } = await import("./register.runtime.js");
|
||||
const { collectBrowserSecurityAuditFindings } = await loadBrowserRegistrationRuntimeModule();
|
||||
return collectBrowserSecurityAuditFindings(ctx);
|
||||
},
|
||||
];
|
||||
@@ -82,7 +91,7 @@ function createLazyBrowserPluginService(): OpenClawPluginService {
|
||||
let service: OpenClawPluginService | null = null;
|
||||
const loadService = async () => {
|
||||
if (!service) {
|
||||
const { createBrowserPluginService } = await import("./register.runtime.js");
|
||||
const { createBrowserPluginService } = await loadBrowserRegistrationRuntimeModule();
|
||||
service = createBrowserPluginService();
|
||||
}
|
||||
return service;
|
||||
@@ -124,7 +133,7 @@ export function registerBrowserPlugin(api: OpenClawPluginApi) {
|
||||
api.registerGatewayMethod(
|
||||
BROWSER_REQUEST_GATEWAY_METHOD,
|
||||
async (opts) => {
|
||||
const { handleBrowserGatewayRequest } = await import("./register.runtime.js");
|
||||
const { handleBrowserGatewayRequest } = await loadBrowserRegistrationRuntimeModule();
|
||||
return await handleBrowserGatewayRequest(opts);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { redactCdpUrl } from "../cdp.helpers.js";
|
||||
import { snapshotAria } from "../cdp.js";
|
||||
import { getChromeMcpPid } from "../chrome-mcp.js";
|
||||
import { getChromeMcpPid, takeChromeMcpSnapshot } from "../chrome-mcp.js";
|
||||
import { resolveBrowserExecutableForPlatform } from "../chrome.executables.js";
|
||||
import { resolveManagedBrowserHeadlessMode } from "../config.js";
|
||||
import { buildBrowserDoctorReport } from "../doctor.js";
|
||||
@@ -227,7 +227,6 @@ async function runBrowserLiveProbe(req: BrowserRequest, ctx: BrowserRouteContext
|
||||
try {
|
||||
const tab = await profileCtx.ensureTabAvailable();
|
||||
if (capabilities.usesChromeMcp) {
|
||||
const { takeChromeMcpSnapshot } = await import("../chrome-mcp.js");
|
||||
await takeChromeMcpSnapshot({
|
||||
profileName: profileCtx.profile.name,
|
||||
profile: profileCtx.profile,
|
||||
|
||||
@@ -13,13 +13,20 @@ export type CodexAppServerClientFactory = (
|
||||
config?: AuthProfileOrderConfig,
|
||||
) => Promise<CodexAppServerClient>;
|
||||
|
||||
let sharedClientModulePromise: Promise<typeof import("./shared-client.js")> | null = null;
|
||||
|
||||
const loadSharedClientModule = async () => {
|
||||
sharedClientModulePromise ??= import("./shared-client.js");
|
||||
return await sharedClientModulePromise;
|
||||
};
|
||||
|
||||
export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
|
||||
startOptions,
|
||||
authProfileId,
|
||||
agentDir,
|
||||
config,
|
||||
) =>
|
||||
import("./shared-client.js").then(({ getSharedCodexAppServerClient }) =>
|
||||
loadSharedClientModule().then(({ getSharedCodexAppServerClient }) =>
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
|
||||
);
|
||||
|
||||
@@ -29,6 +36,6 @@ export const defaultLeasedCodexAppServerClientFactory: CodexAppServerClientFacto
|
||||
agentDir,
|
||||
config,
|
||||
) =>
|
||||
import("./shared-client.js").then(({ getLeasedSharedCodexAppServerClient }) =>
|
||||
loadSharedClientModule().then(({ getLeasedSharedCodexAppServerClient }) =>
|
||||
getLeasedSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
|
||||
);
|
||||
|
||||
@@ -54,6 +54,21 @@ describe("DiffArtifactStore", () => {
|
||||
expect(await store.readHtml(artifact.id)).toBe("<html>demo</html>");
|
||||
});
|
||||
|
||||
it("caps artifact expiry instead of throwing near the Date boundary", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(8_640_000_000_000_000 - 1_000));
|
||||
|
||||
const artifact = await store.createArtifact({
|
||||
html: "<html>demo</html>",
|
||||
title: "Demo",
|
||||
inputKind: "patch",
|
||||
fileCount: 1,
|
||||
ttlMs: 60_000,
|
||||
});
|
||||
|
||||
expect(artifact.expiresAt).toBe("+275760-09-13T00:00:00.000Z");
|
||||
});
|
||||
|
||||
it("expires artifacts after the ttl", async () => {
|
||||
vi.useFakeTimers();
|
||||
const now = new Date("2026-02-27T16:00:00Z");
|
||||
@@ -131,6 +146,15 @@ describe("DiffArtifactStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("caps standalone file expiry instead of throwing near the Date boundary", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(8_640_000_000_000_000 - 1_000));
|
||||
|
||||
const standalone = await store.createStandaloneFileArtifact({ ttlMs: 60_000 });
|
||||
|
||||
expect(standalone.expiresAt).toBe("+275760-09-13T00:00:00.000Z");
|
||||
});
|
||||
|
||||
it("expires standalone file artifacts using ttl metadata", async () => {
|
||||
vi.useFakeTimers();
|
||||
const now = new Date("2026-02-27T16:00:00Z");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { PluginLogger } from "../api.js";
|
||||
@@ -64,15 +65,16 @@ export class DiffArtifactStore {
|
||||
const htmlPath = path.join(artifactDir, "viewer.html");
|
||||
const ttlMs = normalizeTtlMs(params.ttlMs);
|
||||
const createdAt = new Date();
|
||||
const expiresAt = new Date(createdAt.getTime() + ttlMs);
|
||||
const createdAtIso = createdAt.toISOString();
|
||||
const expiresAt = resolveExpiresAtIso(createdAt.getTime(), ttlMs);
|
||||
const meta: DiffArtifactMeta = {
|
||||
id,
|
||||
token,
|
||||
title: params.title,
|
||||
inputKind: params.inputKind,
|
||||
fileCount: params.fileCount,
|
||||
createdAt: createdAt.toISOString(),
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
createdAt: createdAtIso,
|
||||
expiresAt,
|
||||
viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
|
||||
htmlPath,
|
||||
...(params.context ? { context: params.context } : {}),
|
||||
@@ -144,11 +146,12 @@ export class DiffArtifactStore {
|
||||
const filePath = path.join(artifactDir, `preview.${format}`);
|
||||
const ttlMs = normalizeTtlMs(params.ttlMs);
|
||||
const createdAt = new Date();
|
||||
const expiresAt = new Date(createdAt.getTime() + ttlMs).toISOString();
|
||||
const createdAtIso = createdAt.toISOString();
|
||||
const expiresAt = resolveExpiresAtIso(createdAt.getTime(), ttlMs);
|
||||
const meta: StandaloneFileMeta = {
|
||||
kind: "standalone_file",
|
||||
id,
|
||||
createdAt: createdAt.toISOString(),
|
||||
createdAt: createdAtIso,
|
||||
expiresAt,
|
||||
filePath: this.normalizeStoredPath(filePath, "filePath"),
|
||||
...(params.context ? { context: params.context } : {}),
|
||||
@@ -357,6 +360,14 @@ function normalizeTtlMs(value?: number): number {
|
||||
return Math.min(rounded, MAX_TTL_MS);
|
||||
}
|
||||
|
||||
function resolveExpiresAtIso(createdAtMs: number, ttlMs: number): string {
|
||||
return (
|
||||
timestampMsToIsoString(createdAtMs + ttlMs) ??
|
||||
timestampMsToIsoString(MAX_DATE_TIMESTAMP_MS) ??
|
||||
"1970-01-01T00:00:00.000Z"
|
||||
);
|
||||
}
|
||||
|
||||
function isExpired(meta: { expiresAt: string }): boolean {
|
||||
const expiresAt = Date.parse(meta.expiresAt);
|
||||
if (!Number.isFinite(expiresAt)) {
|
||||
|
||||
@@ -63,4 +63,88 @@ describe("Discord model picker preference migration", () => {
|
||||
updatedAt: "2026-05-29T00:00:00.001Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("plans legacy JSON import with max Date timestamps", async () => {
|
||||
const stateDir = await makeStateDir();
|
||||
const sourcePath = path.join(stateDir, "discord", "model-picker-preferences.json");
|
||||
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
sourcePath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
entries: {
|
||||
"discord:default:dm:user:max-date": {
|
||||
recent: ["openai/gpt-5", "openai/gpt-4.1"],
|
||||
updatedAt: "+275760-09-13T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const plans = await Promise.resolve(
|
||||
detectDiscordLegacyStateMigrations({
|
||||
cfg: {},
|
||||
env: {},
|
||||
oauthDir: path.join(stateDir, "credentials"),
|
||||
stateDir,
|
||||
}),
|
||||
);
|
||||
|
||||
const plan = plans?.[0];
|
||||
if (plan?.kind !== "plugin-state-import") {
|
||||
throw new Error("expected plugin-state import plan");
|
||||
}
|
||||
const entries = await plan.readEntries();
|
||||
expect(
|
||||
entries.map((entry) => {
|
||||
const value = entry.value as { updatedAt?: unknown };
|
||||
return value.updatedAt;
|
||||
}),
|
||||
).toEqual(["+275760-09-13T00:00:00.000Z", "+275760-09-12T23:59:59.999Z"]);
|
||||
});
|
||||
|
||||
it("keeps legacy JSON import order near max Date", async () => {
|
||||
const stateDir = await makeStateDir();
|
||||
const sourcePath = path.join(stateDir, "discord", "model-picker-preferences.json");
|
||||
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
sourcePath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
entries: {
|
||||
"discord:default:dm:user:near-max-date": {
|
||||
recent: ["openai/gpt-5", "openai/gpt-4.1"],
|
||||
updatedAt: "+275760-09-12T23:59:59.999Z",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const plans = await Promise.resolve(
|
||||
detectDiscordLegacyStateMigrations({
|
||||
cfg: {},
|
||||
env: {},
|
||||
oauthDir: path.join(stateDir, "credentials"),
|
||||
stateDir,
|
||||
}),
|
||||
);
|
||||
|
||||
const plan = plans?.[0];
|
||||
if (plan?.kind !== "plugin-state-import") {
|
||||
throw new Error("expected plugin-state import plan");
|
||||
}
|
||||
const entries = await plan.readEntries();
|
||||
expect(
|
||||
entries.map((entry) => {
|
||||
const value = entry.value as { modelRef?: unknown };
|
||||
return value.modelRef;
|
||||
}),
|
||||
).toEqual(["openai/gpt-5", "openai/gpt-4.1"]);
|
||||
expect(
|
||||
entries.map((entry) => {
|
||||
const value = entry.value as { updatedAt?: unknown };
|
||||
return value.updatedAt;
|
||||
}),
|
||||
).toEqual(["+275760-09-13T00:00:00.000Z", "+275760-09-12T23:59:59.999Z"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { BundledChannelLegacyStateMigrationDetector } from "openclaw/plugin-sdk/channel-entry-contract";
|
||||
import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
const PREFERENCE_MAX_ENTRIES = 2_000;
|
||||
@@ -92,7 +93,15 @@ function timestampMs(value: unknown): number {
|
||||
}
|
||||
|
||||
function legacyUpdatedAtForIndex(updatedAt: unknown, index: number, total: number): string {
|
||||
return new Date(timestampMs(updatedAt) + Math.max(0, total - index)).toISOString();
|
||||
const baseMs = timestampMs(updatedAt);
|
||||
const anchorMs = Math.min(baseMs + Math.max(0, total), MAX_DATE_TIMESTAMP_MS);
|
||||
const shiftedMs = anchorMs - Math.max(0, index);
|
||||
return (
|
||||
timestampMsToIsoString(shiftedMs) ??
|
||||
timestampMsToIsoString(baseMs) ??
|
||||
timestampMsToIsoString(Math.max(0, total - index)) ??
|
||||
"1970-01-01T00:00:00.000Z"
|
||||
);
|
||||
}
|
||||
|
||||
export const detectDiscordLegacyStateMigrations: BundledChannelLegacyStateMigrationDetector = ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
createPluginStateKeyedStoreForTests,
|
||||
resetPluginStateStoreForTests,
|
||||
} from "openclaw/plugin-sdk/plugin-state-test-runtime";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { setDiscordRuntime, type DiscordRuntime } from "../runtime.js";
|
||||
import {
|
||||
buildDiscordModelPickerPreferenceKey,
|
||||
@@ -163,6 +163,68 @@ describe("discord model picker preferences", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("imports legacy JSON preferences with max Date timestamps", async () => {
|
||||
const env = await createStateEnv();
|
||||
const scope = { accountId: "main", guildId: "guild-max-date", userId: "user-max-date" };
|
||||
const key = buildDiscordModelPickerPreferenceKey(scope);
|
||||
expect(key).toBeTruthy();
|
||||
const legacyPath = path.join(
|
||||
env.OPENCLAW_STATE_DIR as string,
|
||||
"discord",
|
||||
"model-picker-preferences.json",
|
||||
);
|
||||
await fs.mkdir(path.dirname(legacyPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
legacyPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
entries: {
|
||||
[key as string]: {
|
||||
recent: ["openai/gpt-4.1", "openai/gpt-4o"],
|
||||
updatedAt: "+275760-09-13T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(readDiscordModelPickerRecentModels({ env, scope })).resolves.toEqual([
|
||||
"openai/gpt-4.1",
|
||||
"openai/gpt-4o",
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves legacy JSON preference order near max Date", async () => {
|
||||
const env = await createStateEnv();
|
||||
const scope = { accountId: "main", guildId: "guild-near-max-date", userId: "user-near-max" };
|
||||
const key = buildDiscordModelPickerPreferenceKey(scope);
|
||||
expect(key).toBeTruthy();
|
||||
const legacyPath = path.join(
|
||||
env.OPENCLAW_STATE_DIR as string,
|
||||
"discord",
|
||||
"model-picker-preferences.json",
|
||||
);
|
||||
await fs.mkdir(path.dirname(legacyPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
legacyPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
entries: {
|
||||
[key as string]: {
|
||||
recent: ["openai/gpt-4.1", "openai/gpt-4o"],
|
||||
updatedAt: "+275760-09-12T23:59:59.999Z",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(readDiscordModelPickerRecentModels({ env, scope })).resolves.toEqual([
|
||||
"openai/gpt-4.1",
|
||||
"openai/gpt-4o",
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips malformed legacy JSON entries during import", async () => {
|
||||
const env = await createStateEnv();
|
||||
const scope = { userId: "valid-legacy-user" };
|
||||
@@ -207,4 +269,34 @@ describe("discord model picker preferences", () => {
|
||||
const recent = await readDiscordModelPickerRecentModels({ env, scope });
|
||||
expect(new Set(recent)).toEqual(new Set(["openai/gpt-4o", "openai/gpt-4.1"]));
|
||||
});
|
||||
|
||||
it("keeps selections recent when the process clock is outside the Date range", async () => {
|
||||
const env = await createStateEnv();
|
||||
const scope = { userId: "invalid-clock-user" };
|
||||
await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4.1" });
|
||||
await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4o" });
|
||||
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
|
||||
|
||||
try {
|
||||
await recordDiscordModelPickerRecentModel({
|
||||
env,
|
||||
scope,
|
||||
modelRef: "openai/gpt-5.5",
|
||||
limit: 2,
|
||||
});
|
||||
await recordDiscordModelPickerRecentModel({
|
||||
env,
|
||||
scope,
|
||||
modelRef: "openai/gpt-5.6",
|
||||
limit: 2,
|
||||
});
|
||||
} finally {
|
||||
dateNowSpy.mockRestore();
|
||||
}
|
||||
|
||||
await expect(readDiscordModelPickerRecentModels({ env, scope, limit: 3 })).resolves.toEqual([
|
||||
"openai/gpt-5.6",
|
||||
"openai/gpt-5.5",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,12 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeAccountId as normalizeSharedAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
MAX_DATE_TIMESTAMP_MS,
|
||||
resolveDateTimestampMs,
|
||||
resolveTimestampMsToIsoString,
|
||||
timestampMsToIsoString,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -13,11 +19,13 @@ const PREFERENCE_MAX_ENTRIES = 2_000;
|
||||
const MAX_PLUGIN_STATE_KEY_BYTES = 512;
|
||||
const textEncoder = new TextEncoder();
|
||||
let lastPreferenceTimestampMs = 0;
|
||||
let lastPreferenceOrder = 0;
|
||||
|
||||
type ModelPickerPreferencesEntry = {
|
||||
scopeKey: string;
|
||||
modelRef: string;
|
||||
updatedAt: string;
|
||||
updatedOrder?: number;
|
||||
};
|
||||
|
||||
type LegacyModelPickerPreferencesEntry = {
|
||||
@@ -124,6 +132,7 @@ function sanitizeStoredPreferenceEntry(value: unknown): ModelPickerPreferencesEn
|
||||
scopeKey?: unknown;
|
||||
modelRef?: unknown;
|
||||
updatedAt?: unknown;
|
||||
updatedOrder?: unknown;
|
||||
};
|
||||
if (typeof typedValue.scopeKey !== "string" || typeof typedValue.modelRef !== "string") {
|
||||
return undefined;
|
||||
@@ -136,6 +145,10 @@ function sanitizeStoredPreferenceEntry(value: unknown): ModelPickerPreferencesEn
|
||||
scopeKey: typedValue.scopeKey,
|
||||
modelRef,
|
||||
updatedAt: typeof typedValue.updatedAt === "string" ? typedValue.updatedAt : "",
|
||||
updatedOrder:
|
||||
typeof typedValue.updatedOrder === "number" && Number.isSafeInteger(typedValue.updatedOrder)
|
||||
? typedValue.updatedOrder
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,13 +165,58 @@ function timestampMs(value: string): number {
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function legacyUpdatedAtForIndex(updatedAt: string, index: number, total: number): string {
|
||||
return new Date(timestampMs(updatedAt) + Math.max(0, total - index)).toISOString();
|
||||
function timestampOrder(value?: number): number {
|
||||
return value !== undefined && value >= 0 ? value : 0;
|
||||
}
|
||||
|
||||
function nextPreferenceTimestampIso(): string {
|
||||
lastPreferenceTimestampMs = Math.max(Date.now(), lastPreferenceTimestampMs + 1);
|
||||
return new Date(lastPreferenceTimestampMs).toISOString();
|
||||
function comparePreferenceEntries(
|
||||
left: { key: string; value: ModelPickerPreferencesEntry },
|
||||
right: { key: string; value: ModelPickerPreferencesEntry },
|
||||
): number {
|
||||
return (
|
||||
timestampMs(right.value.updatedAt) - timestampMs(left.value.updatedAt) ||
|
||||
timestampOrder(right.value.updatedOrder) - timestampOrder(left.value.updatedOrder) ||
|
||||
left.key.localeCompare(right.key)
|
||||
);
|
||||
}
|
||||
|
||||
function legacyUpdatedAtForIndex(updatedAt: string, index: number, total: number): string {
|
||||
const baseMs = timestampMs(updatedAt);
|
||||
const anchorMs = Math.min(baseMs + Math.max(0, total), MAX_DATE_TIMESTAMP_MS);
|
||||
const shiftedMs = anchorMs - Math.max(0, index);
|
||||
return (
|
||||
timestampMsToIsoString(shiftedMs) ??
|
||||
timestampMsToIsoString(baseMs) ??
|
||||
timestampMsToIsoString(Math.max(0, total - index)) ??
|
||||
"1970-01-01T00:00:00.000Z"
|
||||
);
|
||||
}
|
||||
|
||||
function nextPreferenceTimestamp(existingEntries: ModelPickerPreferencesEntry[]): {
|
||||
updatedAt: string;
|
||||
updatedOrder: number;
|
||||
} {
|
||||
const existingMaxTimestampMs = existingEntries.reduce(
|
||||
(max, entry) => Math.max(max, timestampMs(entry.updatedAt)),
|
||||
0,
|
||||
);
|
||||
lastPreferenceTimestampMs = Math.min(
|
||||
Math.max(
|
||||
resolveDateTimestampMs(Date.now(), 0),
|
||||
lastPreferenceTimestampMs + 1,
|
||||
existingMaxTimestampMs + 1,
|
||||
),
|
||||
MAX_DATE_TIMESTAMP_MS,
|
||||
);
|
||||
const existingMaxOrder = existingEntries.reduce(
|
||||
(max, entry) => Math.max(max, timestampOrder(entry.updatedOrder)),
|
||||
0,
|
||||
);
|
||||
lastPreferenceOrder = Math.max(lastPreferenceOrder + 1, existingMaxOrder + 1);
|
||||
return {
|
||||
updatedAt: resolveTimestampMsToIsoString(lastPreferenceTimestampMs),
|
||||
updatedOrder: lastPreferenceOrder,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLegacyPreferenceKey(key: string): string | undefined {
|
||||
@@ -237,10 +295,13 @@ export async function readDiscordModelPickerRecentModels(params: {
|
||||
await importLegacyPreferences(params.env);
|
||||
const store = openPreferenceStore(params.env);
|
||||
const recent = (await store.entries())
|
||||
.map((entry) => sanitizeStoredPreferenceEntry(entry.value))
|
||||
.filter((entry): entry is ModelPickerPreferencesEntry => entry?.scopeKey === key)
|
||||
.toSorted((left, right) => timestampMs(right.updatedAt) - timestampMs(left.updatedAt))
|
||||
.map((entry) => entry.modelRef);
|
||||
.map((entry) => ({ key: entry.key, value: sanitizeStoredPreferenceEntry(entry.value) }))
|
||||
.filter(
|
||||
(entry): entry is { key: string; value: ModelPickerPreferencesEntry } =>
|
||||
entry.value?.scopeKey === key,
|
||||
)
|
||||
.toSorted(comparePreferenceEntries)
|
||||
.map((entry) => entry.value.modelRef);
|
||||
if (!params.allowedModelRefs || params.allowedModelRefs.size === 0) {
|
||||
return sanitizeRecentModels(recent, limit);
|
||||
}
|
||||
@@ -268,10 +329,14 @@ export async function recordDiscordModelPickerRecentModel(params: {
|
||||
try {
|
||||
await importLegacyPreferences(params.env);
|
||||
const store = openPreferenceStore(params.env);
|
||||
const existingEntries = (await store.entries())
|
||||
.map((entry) => sanitizeStoredPreferenceEntry(entry.value))
|
||||
.filter((entry): entry is ModelPickerPreferencesEntry => entry?.scopeKey === key);
|
||||
const timestamp = nextPreferenceTimestamp(existingEntries);
|
||||
await store.register(buildPreferenceModelKey(key, normalizedModelRef), {
|
||||
scopeKey: key,
|
||||
modelRef: normalizedModelRef,
|
||||
updatedAt: nextPreferenceTimestampIso(),
|
||||
...timestamp,
|
||||
});
|
||||
const limit = Math.max(1, Math.min(params.limit ?? DEFAULT_RECENT_LIMIT, 10));
|
||||
const scopedEntries = (await store.entries())
|
||||
@@ -280,11 +345,7 @@ export async function recordDiscordModelPickerRecentModel(params: {
|
||||
(entry): entry is { key: string; value: ModelPickerPreferencesEntry } =>
|
||||
entry.value?.scopeKey === key,
|
||||
)
|
||||
.toSorted(
|
||||
(left, right) =>
|
||||
timestampMs(right.value.updatedAt) - timestampMs(left.value.updatedAt) ||
|
||||
left.key.localeCompare(right.key),
|
||||
);
|
||||
.toSorted(comparePreferenceEntries);
|
||||
await Promise.all(scopedEntries.slice(limit).map((entry) => store.delete(entry.key)));
|
||||
} catch {
|
||||
return;
|
||||
|
||||
@@ -121,6 +121,7 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -322,6 +323,18 @@ describe("sendMessageDiscord", () => {
|
||||
).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
it("rejects timeout durations outside Date range", async () => {
|
||||
const { rest, patchMock } = makeDiscordRest();
|
||||
|
||||
await expect(
|
||||
timeoutMemberDiscord(
|
||||
{ guildId: "g1", userId: "u1", durationMinutes: 8_640_000_000_000_001 },
|
||||
discordClientOpts(rest),
|
||||
),
|
||||
).rejects.toThrow("Discord timeout duration is outside the supported Date range");
|
||||
expect(patchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds and removes roles", async () => {
|
||||
const { rest, putMock, deleteMock } = makeDiscordRest();
|
||||
putMock.mockResolvedValue({});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
APIVoiceState,
|
||||
RESTPostAPIGuildScheduledEventJSONBody,
|
||||
} from "discord-api-types/v10";
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
|
||||
import {
|
||||
@@ -139,7 +140,10 @@ export async function timeoutMemberDiscord(
|
||||
let until = payload.until;
|
||||
if (!until && payload.durationMinutes) {
|
||||
const ms = payload.durationMinutes * 60 * 1000;
|
||||
until = new Date(Date.now() + ms).toISOString();
|
||||
until = timestampMsToIsoString(Date.now() + ms);
|
||||
if (!until) {
|
||||
throw new Error("Discord timeout duration is outside the supported Date range");
|
||||
}
|
||||
}
|
||||
return await timeoutGuildMember(rest, payload.guildId, payload.userId, {
|
||||
body: { communication_disabled_until: until ?? null },
|
||||
|
||||
@@ -251,6 +251,13 @@ function applyNewAppSecurityPolicy(
|
||||
// Scan-to-create flow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let appRegistrationModulePromise: Promise<typeof import("./app-registration.js")> | null = null;
|
||||
|
||||
const loadAppRegistrationModule = async () => {
|
||||
appRegistrationModulePromise ??= import("./app-registration.js");
|
||||
return await appRegistrationModulePromise;
|
||||
};
|
||||
|
||||
async function promptFeishuDomain(params: {
|
||||
prompter: WizardPrompter;
|
||||
initialValue?: FeishuDomain;
|
||||
@@ -281,7 +288,7 @@ async function runScanToCreate(
|
||||
domain: FeishuDomain,
|
||||
): Promise<AppRegistrationResult | null> {
|
||||
const { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode } =
|
||||
await import("./app-registration.js");
|
||||
await loadAppRegistrationModule();
|
||||
try {
|
||||
await initAppRegistration(domain);
|
||||
} catch {
|
||||
@@ -392,7 +399,7 @@ async function runNewAppFlow(params: {
|
||||
|
||||
// Fetch openId via API for manual flow.
|
||||
if (appId && appSecretProbeValue) {
|
||||
const { getAppOwnerOpenId } = await import("./app-registration.js");
|
||||
const { getAppOwnerOpenId } = await loadAppRegistrationModule();
|
||||
scanOpenId = await getAppOwnerOpenId({
|
||||
appId,
|
||||
appSecret: appSecretProbeValue,
|
||||
|
||||
@@ -23,9 +23,3 @@ export function registerFeishuSubagentHooks(api: OpenClawPluginApi): void {
|
||||
handleFeishuSubagentEnded(event);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
handleFeishuSubagentDeliveryTarget,
|
||||
handleFeishuSubagentEnded,
|
||||
handleFeishuSubagentSpawning,
|
||||
} from "./src/subagent-hooks.js";
|
||||
|
||||
@@ -37,6 +37,19 @@ import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js";
|
||||
import { GoogleMeetRuntime } from "./src/runtime.js";
|
||||
import { isGoogleMeetBrowserManualActionError } from "./src/transports/chrome-create.js";
|
||||
|
||||
let googleMeetCreateModulePromise: Promise<typeof import("./src/create.js")> | null = null;
|
||||
let googleMeetCliModulePromise: Promise<typeof import("./src/cli.js")> | null = null;
|
||||
|
||||
const loadGoogleMeetCreateModule = async () => {
|
||||
googleMeetCreateModulePromise ??= import("./src/create.js");
|
||||
return await googleMeetCreateModulePromise;
|
||||
};
|
||||
|
||||
const loadGoogleMeetCliModule = async () => {
|
||||
googleMeetCliModulePromise ??= import("./src/cli.js");
|
||||
return await googleMeetCliModulePromise;
|
||||
};
|
||||
|
||||
const googleMeetConfigSchema = {
|
||||
parse(value: unknown) {
|
||||
return resolveGoogleMeetConfig(value);
|
||||
@@ -493,7 +506,7 @@ async function createMeetFromParams(params: {
|
||||
runtime: OpenClawPluginApi["runtime"];
|
||||
raw: Record<string, unknown>;
|
||||
}) {
|
||||
const create = await import("./src/create.js");
|
||||
const create = await loadGoogleMeetCreateModule();
|
||||
return create.createMeetFromParams(params);
|
||||
}
|
||||
|
||||
@@ -503,7 +516,7 @@ async function createAndJoinMeetFromParams(params: {
|
||||
raw: Record<string, unknown>;
|
||||
ensureRuntime: () => Promise<GoogleMeetRuntime>;
|
||||
}) {
|
||||
const create = await import("./src/create.js");
|
||||
const create = await loadGoogleMeetCreateModule();
|
||||
return create.createAndJoinMeetFromParams(params);
|
||||
}
|
||||
|
||||
@@ -615,7 +628,7 @@ async function exportGoogleMeetBundleFromParams(
|
||||
}),
|
||||
]);
|
||||
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } =
|
||||
await import("./src/cli.js");
|
||||
await loadGoogleMeetCliModule();
|
||||
const calendarId = normalizeOptionalString(raw.calendarId);
|
||||
const request = {
|
||||
...(resolved.meeting ? { meeting: resolved.meeting } : {}),
|
||||
@@ -1189,7 +1202,7 @@ export default definePluginEntry({
|
||||
|
||||
api.registerCli(
|
||||
async ({ program }) => {
|
||||
const { registerGoogleMeetCli } = await import("./src/cli.js");
|
||||
const { registerGoogleMeetCli } = await loadGoogleMeetCliModule();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config,
|
||||
|
||||
@@ -20,6 +20,13 @@ const ENV_VARS = [
|
||||
"GEMINI_CLI_OAUTH_CLIENT_SECRET",
|
||||
] as const;
|
||||
|
||||
let oauthRuntimeModulePromise: Promise<typeof import("./oauth.runtime.js")> | null = null;
|
||||
|
||||
const loadOauthRuntimeModule = async () => {
|
||||
oauthRuntimeModulePromise ??= import("./oauth.runtime.js");
|
||||
return await oauthRuntimeModulePromise;
|
||||
};
|
||||
|
||||
async function fetchGeminiCliUsage(ctx: ProviderFetchUsageSnapshotContext) {
|
||||
return await fetchGeminiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn, PROVIDER_ID);
|
||||
}
|
||||
@@ -58,7 +65,7 @@ export function buildGoogleGeminiCliProvider(): ProviderPlugin {
|
||||
|
||||
const spin = ctx.prompter.progress("Starting Gemini CLI OAuth…");
|
||||
try {
|
||||
const { loginGeminiCliOAuth } = await import("./oauth.runtime.js");
|
||||
const { loginGeminiCliOAuth } = await loadOauthRuntimeModule();
|
||||
const result = await loginGeminiCliOAuth({
|
||||
isRemote: ctx.isRemote,
|
||||
openUrl: ctx.openUrl,
|
||||
@@ -122,7 +129,7 @@ export function buildGoogleGeminiCliProvider(): ProviderPlugin {
|
||||
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
|
||||
formatApiKey: (cred) => formatGoogleOauthApiKey(cred),
|
||||
refreshOAuth: async (cred) => {
|
||||
const { refreshGeminiCliOAuthToken } = await import("./oauth.runtime.js");
|
||||
const { refreshGeminiCliOAuthToken } = await loadOauthRuntimeModule();
|
||||
return await refreshGeminiCliOAuthToken(cred);
|
||||
},
|
||||
resolveUsageAuth: async (ctx) => {
|
||||
|
||||
@@ -100,6 +100,7 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
for (const key of ENV_KEYS) {
|
||||
const value = envSnapshot[key];
|
||||
if (value === undefined) {
|
||||
@@ -452,6 +453,20 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects browser session expiry outside Date range", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
|
||||
const provider = buildGoogleRealtimeVoiceProvider();
|
||||
|
||||
await expect(
|
||||
provider.createBrowserSession?.({
|
||||
providerConfig: {
|
||||
apiKey: "gemini-key",
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("Google realtime browser session expiry is outside the supported Date range");
|
||||
expect(createTokenMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can opt out of Google Live session resumption and context compression", async () => {
|
||||
const provider = buildGoogleRealtimeVoiceProvider();
|
||||
const bridge = provider.createBridge({
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ThinkingConfig,
|
||||
TurnCoverage,
|
||||
} from "@google/genai";
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
|
||||
import type {
|
||||
RealtimeVoiceAudioFormat,
|
||||
@@ -857,6 +858,11 @@ async function createGoogleRealtimeBrowserSession(
|
||||
const voice = req.voice ?? config.voice ?? GOOGLE_REALTIME_DEFAULT_VOICE;
|
||||
const expiresAtMs = Date.now() + GOOGLE_REALTIME_BROWSER_SESSION_TTL_MS;
|
||||
const newSessionExpiresAtMs = Date.now() + GOOGLE_REALTIME_BROWSER_NEW_SESSION_TTL_MS;
|
||||
const expireTime = timestampMsToIsoString(expiresAtMs);
|
||||
const newSessionExpireTime = timestampMsToIsoString(newSessionExpiresAtMs);
|
||||
if (!expireTime || !newSessionExpireTime) {
|
||||
throw new Error("Google realtime browser session expiry is outside the supported Date range");
|
||||
}
|
||||
const ai = createGoogleGenAI({
|
||||
apiKey,
|
||||
httpOptions: {
|
||||
@@ -866,8 +872,8 @@ async function createGoogleRealtimeBrowserSession(
|
||||
const token = await ai.authTokens.create({
|
||||
config: {
|
||||
uses: 1,
|
||||
expireTime: new Date(expiresAtMs).toISOString(),
|
||||
newSessionExpireTime: new Date(newSessionExpiresAtMs).toISOString(),
|
||||
expireTime,
|
||||
newSessionExpireTime,
|
||||
liveConnectConstraints: {
|
||||
model,
|
||||
config: buildGoogleLiveConnectConfig({
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
rememberIMessageReplyCache,
|
||||
type IMessageChatContext,
|
||||
} from "./monitor-reply-cache.js";
|
||||
import { getCachedIMessagePrivateApiStatus } from "./probe.js";
|
||||
import { getCachedIMessagePrivateApiStatus, probeIMessagePrivateApi } from "./probe.js";
|
||||
import { parseIMessageTarget, type IMessageTarget } from "./targets.js";
|
||||
|
||||
const loadIMessageActionsRuntime = createLazyRuntimeNamedExport(
|
||||
@@ -417,7 +417,6 @@ export const imessageMessageActions: ChannelMessageActionAdapter = {
|
||||
// status adapter, which doesn't fire eagerly on first dispatch. Run
|
||||
// an inline probe so the first react/send-rich attempt after `imsg
|
||||
// launch` succeeds without requiring a manual `channels status`.
|
||||
const { probeIMessagePrivateApi } = await import("./probe.js");
|
||||
privateApiStatus = await probeIMessagePrivateApi(
|
||||
cliPathForProbe,
|
||||
account.config.probeTimeoutMs ?? DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS,
|
||||
|
||||
@@ -3,7 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { runIMessageCatchup } from "./catchup-bridge.js";
|
||||
import { resolveCatchupConfig } from "./catchup.js";
|
||||
import { resolveCatchupConfig, saveIMessageCatchupCursor } from "./catchup.js";
|
||||
import type { IMessagePayload } from "./types.js";
|
||||
|
||||
type RpcCall = {
|
||||
@@ -157,6 +157,32 @@ describe("runIMessageCatchup", () => {
|
||||
expect(summary.replayed).toBe(1);
|
||||
});
|
||||
|
||||
it("does not crash on Date-invalid persisted cursor timestamps", async () => {
|
||||
const log = vi.fn();
|
||||
await saveIMessageCatchupCursor("default", {
|
||||
lastSeenMs: 8_700_000_000_000_000,
|
||||
lastSeenRowid: 10,
|
||||
});
|
||||
const { client, calls } = makeFakeClient(() => {
|
||||
throw new Error("unexpected rpc");
|
||||
});
|
||||
|
||||
const summary = await runIMessageCatchup({
|
||||
client: client as never,
|
||||
accountId: "default",
|
||||
config: resolveCatchupConfig({ enabled: true, perRunLimit: 50, maxAgeMinutes: 60 }),
|
||||
includeAttachments: false,
|
||||
dispatchPayload: async () => {},
|
||||
runtime: { log },
|
||||
});
|
||||
|
||||
expect(summary.querySucceeded).toBe(false);
|
||||
expect(calls).toEqual([]);
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("imessage catchup: invalid since timestamp"),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns querySucceeded=false when chats.list throws", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-05-08T12:00:00Z"));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { warn } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { IMessageRpcClient } from "../client.js";
|
||||
import {
|
||||
@@ -88,6 +89,11 @@ export async function runIMessageCatchup(
|
||||
const payloadByGuid = new Map<string, IMessagePayload>();
|
||||
|
||||
const fetchFn: CatchupFetchFn = async ({ sinceMs, sinceRowid, limit }) => {
|
||||
const sinceISO = timestampMsToIsoString(sinceMs);
|
||||
if (!sinceISO) {
|
||||
warnLog(`imessage catchup: invalid since timestamp ${sinceMs}`);
|
||||
return { resolved: false, rows: [] };
|
||||
}
|
||||
let chatsResult: { chats?: ChatsListEntry[] } | undefined;
|
||||
try {
|
||||
chatsResult = await client.request<{ chats?: ChatsListEntry[] }>(
|
||||
@@ -100,7 +106,6 @@ export async function runIMessageCatchup(
|
||||
return { resolved: false, rows: [] };
|
||||
}
|
||||
const chats = chatsResult?.chats ?? [];
|
||||
const sinceISO = new Date(sinceMs).toISOString();
|
||||
const collected: IMessageCatchupRow[] = [];
|
||||
const perChatLimit = Math.min(limit, PER_CHAT_HISTORY_LIMIT_CAP);
|
||||
let historyFetchFailed = false;
|
||||
|
||||
@@ -85,6 +85,12 @@ const loadMatrixChannelRuntime = createLazyRuntimeNamedExport(
|
||||
() => import("./channel.runtime.js"),
|
||||
"matrixChannelRuntime",
|
||||
);
|
||||
let matrixDoctorModulePromise: Promise<typeof import("./doctor.js")> | null = null;
|
||||
|
||||
const loadMatrixDoctorModule = async () => {
|
||||
matrixDoctorModulePromise ??= import("./doctor.js");
|
||||
return await matrixDoctorModulePromise;
|
||||
};
|
||||
|
||||
const meta = {
|
||||
id: "matrix",
|
||||
@@ -118,9 +124,9 @@ const matrixDoctor: ChannelDoctorAdapter = {
|
||||
legacyConfigRules: MATRIX_LEGACY_CONFIG_RULES,
|
||||
normalizeCompatibilityConfig: normalizeMatrixCompatibilityConfig,
|
||||
runConfigSequence: async ({ cfg, env, shouldRepair }) =>
|
||||
await (await import("./doctor.js")).runMatrixDoctorSequence({ cfg, env, shouldRepair }),
|
||||
await (await loadMatrixDoctorModule()).runMatrixDoctorSequence({ cfg, env, shouldRepair }),
|
||||
cleanStaleConfig: async ({ cfg }) =>
|
||||
await (await import("./doctor.js")).cleanStaleMatrixPluginConfig(cfg),
|
||||
await (await loadMatrixDoctorModule()).cleanStaleMatrixPluginConfig(cfg),
|
||||
};
|
||||
|
||||
const listMatrixDirectoryPeersFromConfig =
|
||||
|
||||
@@ -950,6 +950,28 @@ describe("matrix CLI verification commands", () => {
|
||||
expect(console.log).toHaveBeenCalledWith("- BritdXC6iL (OpenClaw Gateway)");
|
||||
});
|
||||
|
||||
it("omits invalid matrix device last seen timestamps", async () => {
|
||||
listMatrixOwnDevicesMock.mockResolvedValue([
|
||||
{
|
||||
deviceId: "DEVICE123",
|
||||
displayName: "OpenClaw Gateway",
|
||||
lastSeenIp: "127.0.0.1",
|
||||
lastSeenTs: 8_700_000_000_000_000,
|
||||
current: true,
|
||||
},
|
||||
]);
|
||||
const program = buildProgram();
|
||||
|
||||
await program.parseAsync(["matrix", "devices", "list", "--account", "poe"], { from: "user" });
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith("Account: poe");
|
||||
expect(console.log).toHaveBeenCalledWith("- DEVICE123 (current, OpenClaw Gateway)");
|
||||
expect(console.log).toHaveBeenCalledWith(" Last IP: 127.0.0.1");
|
||||
expect(
|
||||
consoleLogMock.mock.calls.some(([message]) => String(message).startsWith(" Last seen:")),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("prunes stale matrix gateway devices", async () => {
|
||||
pruneMatrixStaleGatewayDevicesMock.mockResolvedValue({
|
||||
before: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { parseStrictInteger, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import type { ChannelSetupInput } from "openclaw/plugin-sdk/setup";
|
||||
import { resolveMatrixAccount, resolveMatrixAccountConfig } from "./matrix/accounts.js";
|
||||
import { listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } from "./matrix/actions/devices.js";
|
||||
@@ -216,8 +216,9 @@ function printMatrixOwnDevices(
|
||||
console.log(
|
||||
`- ${formatMatrixCliText(device.deviceId)}${labels.length ? ` (${labels.join(", ")})` : ""}`,
|
||||
);
|
||||
if (device.lastSeenTs) {
|
||||
printTimestamp(" Last seen", new Date(device.lastSeenTs).toISOString());
|
||||
const lastSeenAt = timestampMsToIsoString(device.lastSeenTs);
|
||||
if (lastSeenAt) {
|
||||
printTimestamp(" Last seen", lastSeenAt);
|
||||
}
|
||||
if (device.lastSeenIp) {
|
||||
console.log(` Last IP: ${formatMatrixCliText(device.lastSeenIp)}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ensureMatrixStartupVerification } from "./startup-verification.js";
|
||||
|
||||
function createTempStateDir(): string {
|
||||
@@ -80,6 +80,10 @@ function createHarness(params?: {
|
||||
}
|
||||
|
||||
describe("ensureMatrixStartupVerification", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("skips automatic requests when the device is already verified", async () => {
|
||||
const tempHome = createTempStateDir();
|
||||
const harness = createHarness({ verified: true });
|
||||
@@ -203,6 +207,27 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
expect(fs.existsSync(createStateFilePath(tempHome))).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back when startup verification nowMs is outside Date range", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-05-30T12:00:00.000Z"));
|
||||
const tempHome = createTempStateDir();
|
||||
const stateFilePath = createStateFilePath(tempHome);
|
||||
const harness = createHarness();
|
||||
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath,
|
||||
nowMs: 8_640_000_000_000_001,
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("requested");
|
||||
const state = JSON.parse(fs.readFileSync(stateFilePath, "utf-8")) as {
|
||||
attemptedAt?: string;
|
||||
};
|
||||
expect(state.attemptedAt).toBe("2026-05-30T12:00:00.000Z");
|
||||
});
|
||||
|
||||
it("keeps startup verification failures non-fatal", async () => {
|
||||
const tempHome = createTempStateDir();
|
||||
const harness = createHarness({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import type { MatrixConfig } from "../../types.js";
|
||||
import { resolveMatrixStoragePaths } from "../client/storage.js";
|
||||
import type { MatrixAuth } from "../client/types.js";
|
||||
@@ -95,6 +96,14 @@ function resolveRetryAfterMs(params: {
|
||||
return remaining > 0 ? remaining : undefined;
|
||||
}
|
||||
|
||||
function resolveStartupVerificationTimestamp(nowMs: unknown): string {
|
||||
return (
|
||||
timestampMsToIsoString(nowMs) ??
|
||||
timestampMsToIsoString(Date.now()) ??
|
||||
"1970-01-01T00:00:00.000Z"
|
||||
);
|
||||
}
|
||||
|
||||
function shouldHonorCooldown(params: {
|
||||
state: MatrixStartupVerificationState | null;
|
||||
verification: MatrixOwnDeviceVerificationStatus;
|
||||
@@ -189,6 +198,7 @@ export async function ensureMatrixStartupVerification(params: {
|
||||
);
|
||||
const cooldownMs = cooldownHours * 60 * 60 * 1000;
|
||||
const nowMs = params.nowMs ?? Date.now();
|
||||
const attemptedAt = resolveStartupVerificationTimestamp(nowMs);
|
||||
const state = await readStartupVerificationState(statePath);
|
||||
const stateCooldownMs = resolveStateCooldownMs(state, cooldownMs);
|
||||
if (shouldHonorCooldown({ state, verification, stateCooldownMs, nowMs })) {
|
||||
@@ -208,7 +218,7 @@ export async function ensureMatrixStartupVerification(params: {
|
||||
await writeJsonFileAtomically(statePath, {
|
||||
userId: verification.userId,
|
||||
deviceId: verification.deviceId,
|
||||
attemptedAt: new Date(nowMs).toISOString(),
|
||||
attemptedAt,
|
||||
outcome: "requested",
|
||||
requestId: request.id,
|
||||
transactionId: request.transactionId,
|
||||
@@ -224,7 +234,7 @@ export async function ensureMatrixStartupVerification(params: {
|
||||
await writeJsonFileAtomically(statePath, {
|
||||
userId: verification.userId,
|
||||
deviceId: verification.deviceId,
|
||||
attemptedAt: new Date(nowMs).toISOString(),
|
||||
attemptedAt,
|
||||
outcome: "failed",
|
||||
error,
|
||||
} satisfies MatrixStartupVerificationState).catch(() => {});
|
||||
|
||||
@@ -166,6 +166,26 @@ describe("MatrixVerificationManager", () => {
|
||||
expect(summary.phaseName).toBe("requested");
|
||||
});
|
||||
|
||||
it("tracks verification requests when the process clock is outside the Date range", () => {
|
||||
const manager = new MatrixVerificationManager();
|
||||
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
|
||||
|
||||
try {
|
||||
const summary = manager.trackVerificationRequest(
|
||||
new MockVerificationRequest({
|
||||
transactionId: "txn-invalid-clock",
|
||||
phase: VerificationPhase.Requested,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(summary.createdAt).toBe("1970-01-01T00:00:00.000Z");
|
||||
expect(summary.updatedAt).toBe("1970-01-01T00:00:00.000Z");
|
||||
expect(manager.listVerifications()).toHaveLength(1);
|
||||
} finally {
|
||||
dateNowSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("reuses the same tracked id for repeated transaction IDs", () => {
|
||||
const manager = new MatrixVerificationManager();
|
||||
const first = new MockVerificationRequest({
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
VerifierEvent,
|
||||
} from "matrix-js-sdk/lib/crypto-api/verification.js";
|
||||
import { VerificationMethod } from "matrix-js-sdk/lib/types.js";
|
||||
import {
|
||||
resolveDateTimestampMs,
|
||||
resolveTimestampMsToIsoString,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { formatMatrixErrorMessage } from "../errors.js";
|
||||
|
||||
export type MatrixVerificationMethod = "sas" | "show-qr" | "scan-qr";
|
||||
@@ -266,7 +270,7 @@ export class MatrixVerificationManager {
|
||||
}
|
||||
|
||||
private touchVerificationSession(session: MatrixVerificationSession): void {
|
||||
session.updatedAtMs = Date.now();
|
||||
session.updatedAtMs = resolveDateTimestampMs(Date.now());
|
||||
this.emitVerificationSummary(session);
|
||||
}
|
||||
|
||||
@@ -317,8 +321,8 @@ export class MatrixVerificationManager {
|
||||
hasReciprocateQr: Boolean(session.reciprocateQrCallbacks),
|
||||
completed: phase === VerificationPhase.Done,
|
||||
error: session.error,
|
||||
createdAt: new Date(session.createdAtMs).toISOString(),
|
||||
updatedAt: new Date(session.updatedAtMs).toISOString(),
|
||||
createdAt: resolveTimestampMsToIsoString(session.createdAtMs),
|
||||
updatedAt: resolveTimestampMsToIsoString(session.updatedAtMs),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -594,7 +598,7 @@ export class MatrixVerificationManager {
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const now = resolveDateTimestampMs(Date.now());
|
||||
const id = `verification-${++this.verificationSessionCounter}`;
|
||||
const session: MatrixVerificationSession = {
|
||||
id,
|
||||
|
||||
@@ -23,9 +23,3 @@ export function registerMatrixSubagentHooks(api: OpenClawPluginApi): void {
|
||||
return handleMatrixSubagentDeliveryTarget(event);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
handleMatrixSubagentDeliveryTarget,
|
||||
handleMatrixSubagentEnded,
|
||||
handleMatrixSubagentSpawning,
|
||||
} from "./src/matrix/subagent-hooks.js";
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { writeDailyDreamingPhaseBlock, writeDeepDreamingReport } from "./dreaming-markdown.js";
|
||||
import { createMemoryCoreTestHarness } from "./test-helpers.js";
|
||||
|
||||
const { createTempWorkspace } = createMemoryCoreTestHarness();
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function expectPathMissing(targetPath: string): Promise<void> {
|
||||
const error = await fs.access(targetPath).then(
|
||||
() => undefined,
|
||||
@@ -55,6 +59,25 @@ describe("dreaming markdown storage", () => {
|
||||
expect(content).toContain("- Candidate: remember the API key is fake");
|
||||
});
|
||||
|
||||
it("falls back when the injected timestamp is outside Date range", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-markdown-");
|
||||
|
||||
const result = await writeDailyDreamingPhaseBlock({
|
||||
workspaceDir,
|
||||
phase: "light",
|
||||
bodyLines: ["- Candidate: bounded fallback"],
|
||||
nowMs: 8_640_000_000_000_001,
|
||||
timezone,
|
||||
storage: {
|
||||
mode: "inline",
|
||||
separateReports: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(requireInlinePath(result)).toBe(path.join(workspaceDir, "memory", "2026-05-30.md"));
|
||||
});
|
||||
|
||||
it("keeps multiple inline phases in the shared daily memory file", async () => {
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-markdown-");
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
replaceManagedMarkdownBlock,
|
||||
withTrailingNewline,
|
||||
} from "openclaw/plugin-sdk/memory-host-markdown";
|
||||
import { resolveMemoryCoreNowMs, resolveMemoryCoreTimestamp } from "./time.js";
|
||||
|
||||
const DAILY_PHASE_HEADINGS: Record<Exclude<MemoryDreamingPhaseName, "deep">, string> = {
|
||||
light: "## Light Sleep",
|
||||
@@ -63,7 +64,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
|
||||
timezone?: string;
|
||||
storage: MemoryDreamingStorageConfig;
|
||||
}): Promise<{ inlinePath?: string; reportPath?: string }> {
|
||||
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
|
||||
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
|
||||
const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No notable updates.";
|
||||
let inlinePath: string | undefined;
|
||||
let reportPath: string | undefined;
|
||||
@@ -107,7 +108,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
|
||||
|
||||
await appendMemoryHostEvent(params.workspaceDir, {
|
||||
type: "memory.dream.completed",
|
||||
timestamp: new Date(nowMs).toISOString(),
|
||||
timestamp: resolveMemoryCoreTimestamp(nowMs),
|
||||
phase: params.phase,
|
||||
...(inlinePath ? { inlinePath } : {}),
|
||||
...(reportPath ? { reportPath } : {}),
|
||||
@@ -131,14 +132,14 @@ export async function writeDeepDreamingReport(params: {
|
||||
if (!shouldWriteSeparate(params.storage)) {
|
||||
return undefined;
|
||||
}
|
||||
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
|
||||
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
|
||||
const reportPath = resolveSeparateReportPath(params.workspaceDir, "deep", nowMs, params.timezone);
|
||||
await fs.mkdir(path.dirname(reportPath), { recursive: true });
|
||||
const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No durable changes.";
|
||||
await fs.writeFile(reportPath, `# Deep Sleep\n\n${body}\n`, "utf-8");
|
||||
await appendMemoryHostEvent(params.workspaceDir, {
|
||||
type: "memory.dream.completed",
|
||||
timestamp: new Date(nowMs).toISOString(),
|
||||
timestamp: resolveMemoryCoreTimestamp(nowMs),
|
||||
phase: "deep",
|
||||
reportPath,
|
||||
lineCount: params.bodyLines.length,
|
||||
|
||||
18
extensions/memory-core/src/flush-plan.test.ts
Normal file
18
extensions/memory-core/src/flush-plan.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildMemoryFlushPlan } from "./flush-plan.js";
|
||||
|
||||
describe("buildMemoryFlushPlan", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("falls back when the injected timestamp is outside Date range", () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
|
||||
|
||||
const plan = buildMemoryFlushPlan({
|
||||
nowMs: 8_640_000_000_000_001,
|
||||
});
|
||||
|
||||
expect(plan?.relativePath).toBe("memory/2026-05-30.md");
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type MemoryFlushPlan,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-runtime-core";
|
||||
import { resolveMemoryCoreNowMs } from "./time.js";
|
||||
|
||||
export const DEFAULT_MEMORY_FLUSH_SOFT_TOKENS = 4000;
|
||||
export const DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
||||
@@ -53,7 +54,7 @@ function formatDateStampInTimezone(nowMs: number, timezone: string): string {
|
||||
if (year && month && day) {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
return new Date(nowMs).toISOString().slice(0, 10);
|
||||
return new Date(resolveMemoryCoreNowMs(nowMs)).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function normalizeNonNegativeInt(value: unknown): number | null {
|
||||
@@ -99,7 +100,7 @@ export function buildMemoryFlushPlan(
|
||||
} = {},
|
||||
): MemoryFlushPlan | null {
|
||||
const resolved = params;
|
||||
const nowMs = Number.isFinite(resolved.nowMs) ? (resolved.nowMs as number) : Date.now();
|
||||
const nowMs = resolveMemoryCoreNowMs(resolved.nowMs);
|
||||
const cfg = resolved.cfg;
|
||||
const defaults = cfg?.agents?.defaults?.compaction?.memoryFlush;
|
||||
if (defaults?.enabled === false) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/memory-host-events", () => ({
|
||||
appendMemoryHostEvent: vi.fn(async () => {}),
|
||||
@@ -40,6 +40,10 @@ describe("short-term promotion", () => {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function withTempWorkspace(run: (workspaceDir: string) => Promise<void>) {
|
||||
const workspaceDir = path.join(fixtureRoot, `case-${caseId++}`);
|
||||
await fs.mkdir(path.join(workspaceDir, "memory", ".dreams"), { recursive: true });
|
||||
@@ -89,19 +93,31 @@ describe("short-term promotion", () => {
|
||||
return candidate.promotedAt;
|
||||
}
|
||||
|
||||
async function readRecallStoreEntries(
|
||||
workspaceDir: string,
|
||||
): Promise<
|
||||
async function readRecallStoreEntries(workspaceDir: string): Promise<
|
||||
Record<
|
||||
string,
|
||||
{ claimHash?: unknown; recallCount?: unknown; snippet?: unknown; totalScore?: unknown }
|
||||
{
|
||||
claimHash?: unknown;
|
||||
firstRecalledAt?: unknown;
|
||||
lastRecalledAt?: unknown;
|
||||
recallCount?: unknown;
|
||||
snippet?: unknown;
|
||||
totalScore?: unknown;
|
||||
}
|
||||
>
|
||||
> {
|
||||
const raw = await fs.readFile(resolveShortTermRecallStorePath(workspaceDir), "utf-8");
|
||||
const store = JSON.parse(raw) as {
|
||||
entries?: Record<
|
||||
string,
|
||||
{ claimHash?: unknown; recallCount?: unknown; snippet?: unknown; totalScore?: unknown }
|
||||
{
|
||||
claimHash?: unknown;
|
||||
firstRecalledAt?: unknown;
|
||||
lastRecalledAt?: unknown;
|
||||
recallCount?: unknown;
|
||||
snippet?: unknown;
|
||||
totalScore?: unknown;
|
||||
}
|
||||
>;
|
||||
};
|
||||
return store.entries ?? {};
|
||||
@@ -160,6 +176,35 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back when the injected recall timestamp is outside Date range", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
const notePath = await writeDailyMemoryNote(workspaceDir, "2026-05-30", [
|
||||
"Bounded recall timestamp note.",
|
||||
]);
|
||||
|
||||
await recordShortTermRecalls({
|
||||
workspaceDir,
|
||||
query: "bounded recall",
|
||||
nowMs: 8_640_000_000_000_001,
|
||||
results: [
|
||||
{
|
||||
path: path.relative(workspaceDir, notePath).replaceAll("\\", "/"),
|
||||
source: "memory",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.9,
|
||||
snippet: "Bounded recall timestamp note.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [entry] = Object.values(await readRecallStoreEntries(workspaceDir));
|
||||
expect(entry?.firstRecalledAt).toBe("2026-05-30T12:00:00.000Z");
|
||||
expect(entry?.lastRecalledAt).toBe("2026-05-30T12:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
it("records short-term recall for notes stored in spaced and Unicode memory subdirectories", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
const spacedPath = await writeDailyMemoryNoteInSubdir(
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "./concept-vocabulary.js";
|
||||
import { asRecord } from "./dreaming-shared.js";
|
||||
import { compactMemoryForBudget, DEFAULT_MEMORY_FILE_MAX_CHARS } from "./memory-budget.js";
|
||||
import { resolveMemoryCoreNowMs, resolveMemoryCoreTimestamp } from "./time.js";
|
||||
|
||||
const SHORT_TERM_PATH_RE = /(?:^|\/)memory\/(?:[^/]+\/)*(\d{4})-(\d{2})-(\d{2})(?:-[^/]+)?\.md$/;
|
||||
const DREAMING_MEMORY_PATH_RE = /(?:^|\/)memory\/dreaming\//;
|
||||
@@ -1052,8 +1053,8 @@ export async function recordShortTermRecalls(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
|
||||
const nowIso = resolveMemoryCoreTimestamp(nowMs);
|
||||
const signalType = params.signalType ?? "recall";
|
||||
const queryHash = hashQuery(query);
|
||||
const todayBucket =
|
||||
@@ -1199,8 +1200,8 @@ export async function recordGroundedShortTermCandidates(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
|
||||
const nowIso = resolveMemoryCoreTimestamp(nowMs);
|
||||
const fallbackDayBucket = formatMemoryDreamingDay(nowMs, params.timezone);
|
||||
await withShortTermLock(workspaceDir, async () => {
|
||||
const store = await readStore(workspaceDir, nowIso);
|
||||
@@ -1281,8 +1282,8 @@ export async function recordDreamingPhaseSignals(params: {
|
||||
if (keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
|
||||
const nowIso = resolveMemoryCoreTimestamp(nowMs);
|
||||
|
||||
await withShortTermLock(workspaceDir, async () => {
|
||||
const [store, phaseSignals] = await Promise.all([
|
||||
@@ -1334,8 +1335,8 @@ export async function recordRemConsideredPhaseSignals(params: {
|
||||
if (keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
|
||||
const nowIso = resolveMemoryCoreTimestamp(nowMs);
|
||||
|
||||
await withShortTermLock(workspaceDir, async () => {
|
||||
const [store, phaseSignals] = await Promise.all([
|
||||
@@ -1376,8 +1377,8 @@ export async function readLightStagedKeys(params: {
|
||||
if (!workspaceDir) {
|
||||
return new Set();
|
||||
}
|
||||
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
|
||||
const nowIso = resolveMemoryCoreTimestamp(nowMs);
|
||||
const store = await readPhaseSignalStore(workspaceDir, nowIso);
|
||||
const keys = new Set<string>();
|
||||
for (const [key, entry] of Object.entries(store.entries)) {
|
||||
@@ -1409,8 +1410,8 @@ export async function rankShortTermPromotionCandidates(
|
||||
return [];
|
||||
}
|
||||
|
||||
const nowMs = Number.isFinite(options.nowMs) ? (options.nowMs as number) : Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const nowMs = resolveMemoryCoreNowMs(options.nowMs);
|
||||
const nowIso = resolveMemoryCoreTimestamp(nowMs);
|
||||
const minScore = toFiniteScore(options.minScore, DEFAULT_PROMOTION_MIN_SCORE);
|
||||
const minRecallCount = toFiniteNonNegativeInt(
|
||||
options.minRecallCount,
|
||||
@@ -1550,8 +1551,8 @@ export async function readShortTermRecallEntries(params: {
|
||||
if (!workspaceDir) {
|
||||
return [];
|
||||
}
|
||||
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
|
||||
const nowIso = resolveMemoryCoreTimestamp(nowMs);
|
||||
const store = await readStore(workspaceDir, nowIso);
|
||||
return Object.values(store.entries).filter(
|
||||
(entry): entry is ShortTermRecallEntry =>
|
||||
@@ -1838,8 +1839,8 @@ export async function applyShortTermPromotions(
|
||||
options: ApplyShortTermPromotionsOptions,
|
||||
): Promise<ApplyShortTermPromotionsResult> {
|
||||
const workspaceDir = options.workspaceDir.trim();
|
||||
const nowMs = Number.isFinite(options.nowMs) ? (options.nowMs as number) : Date.now();
|
||||
const nowIso = new Date(nowMs).toISOString();
|
||||
const nowMs = resolveMemoryCoreNowMs(options.nowMs);
|
||||
const nowIso = resolveMemoryCoreTimestamp(nowMs);
|
||||
const limit = Number.isFinite(options.limit)
|
||||
? Math.max(0, Math.floor(options.limit as number))
|
||||
: options.candidates.length;
|
||||
|
||||
10
extensions/memory-core/src/time.ts
Normal file
10
extensions/memory-core/src/time.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
|
||||
export function resolveMemoryCoreNowMs(nowMs: unknown): number {
|
||||
return timestampMsToIsoString(nowMs) === undefined ? Date.now() : (nowMs as number);
|
||||
}
|
||||
|
||||
export function resolveMemoryCoreTimestamp(nowMs: unknown): string {
|
||||
const timestampMs = resolveMemoryCoreNowMs(nowMs);
|
||||
return timestampMsToIsoString(timestampMs) ?? new Date().toISOString();
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
replaceManagedMarkdownBlock,
|
||||
withTrailingNewline,
|
||||
} from "openclaw/plugin-sdk/memory-host-markdown";
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { compileMemoryWikiVault } from "./compile.js";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
WIKI_RELATED_END_MARKER,
|
||||
WIKI_RELATED_START_MARKER,
|
||||
} from "./markdown.js";
|
||||
import { resolveMemoryWikiTimestamp } from "./time.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
||||
const CHATGPT_PREFERENCE_SIGNAL_RE =
|
||||
@@ -203,7 +205,7 @@ function isoFromUnix(raw: unknown): string | undefined {
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return undefined;
|
||||
}
|
||||
return new Date(numeric * 1000).toISOString();
|
||||
return timestampMsToIsoString(numeric * 1000);
|
||||
}
|
||||
|
||||
function cleanMessageText(value: string): string {
|
||||
@@ -745,7 +747,7 @@ export async function importChatGptConversations(params: {
|
||||
let updatedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let runId: string | undefined;
|
||||
const nowIso = new Date(params.nowMs ?? Date.now()).toISOString();
|
||||
const nowIso = resolveMemoryWikiTimestamp(params.nowMs);
|
||||
|
||||
let importRunRecord: ChatGptImportRunRecord | undefined;
|
||||
let importRunDir = "";
|
||||
|
||||
@@ -605,4 +605,30 @@ cli note
|
||||
.then((entries) => entries.filter((entry) => entry !== "index.md")),
|
||||
).resolves.toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("imports ChatGPT exports with out-of-range Unix timestamps", async () => {
|
||||
const { rootDir, config } = await createCliVault({ initialize: true });
|
||||
const exportDir = await createChatGptExport(rootDir);
|
||||
const conversationsPath = path.join(exportDir, "conversations.json");
|
||||
const conversations = JSON.parse(await fs.readFile(conversationsPath, "utf8")) as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
conversations[0].update_time = 9_000_000_000_000;
|
||||
await fs.writeFile(conversationsPath, `${JSON.stringify(conversations, null, 2)}\n`, "utf8");
|
||||
|
||||
const result = await runWikiChatGptImport({
|
||||
config,
|
||||
exportPath: exportDir,
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result.createdCount).toBe(1);
|
||||
const sourceFile = (await fs.readdir(path.join(rootDir, "sources"))).find(
|
||||
(entry) => entry !== "index.md",
|
||||
);
|
||||
expect(sourceFile).toBeDefined();
|
||||
const pageContent = await fs.readFile(path.join(rootDir, "sources", sourceFile ?? ""), "utf8");
|
||||
expect(pageContent).toContain("- Created: 2024-04-06T00:26:40.000Z");
|
||||
expect(pageContent).toContain("- Updated: 2024-04-06T00:26:40.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { compileMemoryWikiVault } from "./compile.js";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { appendMemoryWikiLog } from "./log.js";
|
||||
import { renderMarkdownFence, renderWikiMarkdown, slugifyWikiSegment } from "./markdown.js";
|
||||
import { resolveMemoryWikiTimestamp } from "./time.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
||||
type IngestMemoryWikiSourceResult = {
|
||||
@@ -48,7 +49,7 @@ export async function ingestMemoryWikiSource(params: {
|
||||
const pageRelativePath = path.join("sources", `${slug}.md`);
|
||||
const pagePath = path.join(params.config.vault.path, pageRelativePath);
|
||||
const created = !(await pathExists(pagePath));
|
||||
const timestamp = new Date(params.nowMs ?? Date.now()).toISOString();
|
||||
const timestamp = resolveMemoryWikiTimestamp(params.nowMs);
|
||||
|
||||
const markdown = renderWikiMarkdown({
|
||||
frontmatter: {
|
||||
|
||||
48
extensions/memory-wiki/src/source-page-shared.test.ts
Normal file
48
extensions/memory-wiki/src/source-page-shared.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { writeImportedSourcePage } from "./source-page-shared.js";
|
||||
|
||||
describe("writeImportedSourcePage", () => {
|
||||
let suiteRoot: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
suiteRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-source-page-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await fs.rm(suiteRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("falls back when the source mtime is outside the Date range", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-05-01T12:00:00.000Z"));
|
||||
const sourcePath = path.join(suiteRoot, "source.txt");
|
||||
await fs.writeFile(sourcePath, "source body", "utf8");
|
||||
const state: Parameters<typeof writeImportedSourcePage>[0]["state"] = {
|
||||
entries: {},
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const result = await writeImportedSourcePage({
|
||||
vaultRoot: suiteRoot,
|
||||
syncKey: "unsafe:source",
|
||||
sourcePath,
|
||||
sourceUpdatedAtMs: 8_700_000_000_000_000,
|
||||
sourceSize: 11,
|
||||
renderFingerprint: "fingerprint",
|
||||
pagePath: "pages/source.md",
|
||||
group: "unsafe-local",
|
||||
state,
|
||||
buildRendered: (raw, updatedAt) => `updatedAt: ${updatedAt}\n${raw}`,
|
||||
});
|
||||
|
||||
await expect(fs.readFile(path.join(suiteRoot, "pages/source.md"), "utf8")).resolves.toBe(
|
||||
"updatedAt: 2026-05-01T12:00:00.000Z\nsource body",
|
||||
);
|
||||
expect(result).toEqual({ pagePath: "pages/source.md", changed: true, created: true });
|
||||
expect(state.entries["unsafe:source"]?.sourceUpdatedAtMs).toBe(8_700_000_000_000_000);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { FsSafeError, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
|
||||
import {
|
||||
setImportedSourceEntry,
|
||||
@@ -48,7 +49,7 @@ export async function writeImportedSourcePage(params: {
|
||||
throw error;
|
||||
});
|
||||
const created = !pageStat;
|
||||
const updatedAt = new Date(params.sourceUpdatedAtMs).toISOString();
|
||||
const updatedAt = timestampMsToIsoString(params.sourceUpdatedAtMs) ?? new Date().toISOString();
|
||||
const shouldSkip = await shouldSkipImportedSourceWrite({
|
||||
vaultRoot: params.vaultRoot,
|
||||
syncKey: params.syncKey,
|
||||
|
||||
20
extensions/memory-wiki/src/time.test.ts
Normal file
20
extensions/memory-wiki/src/time.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveMemoryWikiTimestamp } from "./time.js";
|
||||
|
||||
describe("resolveMemoryWikiTimestamp", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("uses valid injected timestamps", () => {
|
||||
expect(resolveMemoryWikiTimestamp(Date.UTC(2026, 3, 5, 12, 0, 0))).toBe(
|
||||
"2026-04-05T12:00:00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back when injected timestamps are outside Date range", () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
|
||||
|
||||
expect(resolveMemoryWikiTimestamp(8_640_000_000_000_001)).toBe("2026-05-30T12:00:00.000Z");
|
||||
});
|
||||
});
|
||||
7
extensions/memory-wiki/src/time.ts
Normal file
7
extensions/memory-wiki/src/time.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
|
||||
export function resolveMemoryWikiTimestamp(nowMs?: number): string {
|
||||
return (
|
||||
timestampMsToIsoString(nowMs) ?? timestampMsToIsoString(Date.now()) ?? new Date().toISOString()
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { FsSafeError, pathExists, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { appendMemoryWikiLog } from "./log.js";
|
||||
import { resolveMemoryWikiTimestamp } from "./time.js";
|
||||
|
||||
export const WIKI_VAULT_DIRECTORIES = [
|
||||
"entities",
|
||||
@@ -128,7 +129,7 @@ export async function initializeMemoryWikiVault(
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
createdAt: new Date(options?.nowMs ?? Date.now()).toISOString(),
|
||||
createdAt: resolveMemoryWikiTimestamp(options?.nowMs),
|
||||
renderMode: config.vault.renderMode,
|
||||
},
|
||||
null,
|
||||
@@ -142,7 +143,7 @@ export async function initializeMemoryWikiVault(
|
||||
if (createdDirectories.length > 0 || createdFiles.length > 0) {
|
||||
await appendMemoryWikiLog(rootDir, {
|
||||
type: "init",
|
||||
timestamp: new Date(options?.nowMs ?? Date.now()).toISOString(),
|
||||
timestamp: resolveMemoryWikiTimestamp(options?.nowMs),
|
||||
details: {
|
||||
createdDirectories: createdDirectories.map((dir) => path.relative(rootDir, dir) || "."),
|
||||
createdFiles: createdFiles.map((file) => path.relative(rootDir, file)),
|
||||
|
||||
@@ -22,6 +22,13 @@ import {
|
||||
} from "../policy-state.js";
|
||||
import { POLICY_TOOL_GROUPS } from "../tool-policy-conformance.js";
|
||||
|
||||
let fsPromisesModulePromise: Promise<typeof import("node:fs/promises")> | null = null;
|
||||
|
||||
const loadFsPromisesModule = async () => {
|
||||
fsPromisesModulePromise ??= import("node:fs/promises");
|
||||
return await fsPromisesModulePromise;
|
||||
};
|
||||
|
||||
const CHECK_IDS = {
|
||||
policyAttestationMismatch: "policy/attestation-hash-mismatch",
|
||||
policyDeniedChannelProvider: "policy/channels-denied-provider",
|
||||
@@ -5268,7 +5275,7 @@ async function readPolicyFile(
|
||||
const displayName = policyDisplayName(ctx);
|
||||
const path = resolveWorkspacePath(ctx, policyPathSetting(ctx));
|
||||
try {
|
||||
const fs = await import("node:fs/promises");
|
||||
const fs = await loadFsPromisesModule();
|
||||
return {
|
||||
raw: await fs.readFile(path, "utf-8"),
|
||||
path,
|
||||
@@ -5289,7 +5296,7 @@ async function readWorkspaceFile(
|
||||
): Promise<{ raw: string; path: string } | null> {
|
||||
const path = resolveWorkspacePath(ctx, fileName);
|
||||
try {
|
||||
const fs = await import("node:fs/promises");
|
||||
const fs = await loadFsPromisesModule();
|
||||
return { raw: await fs.readFile(path, "utf-8"), path };
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
|
||||
@@ -38,6 +38,14 @@ import {
|
||||
import type { FetchMediaOptions, FetchMediaResult } from "../engine/adapter/types.js";
|
||||
import { getBridgeLogger } from "./logger.js";
|
||||
|
||||
let mediaRuntimeModulePromise: Promise<typeof import("openclaw/plugin-sdk/media-runtime")> | null =
|
||||
null;
|
||||
|
||||
const loadMediaRuntimeModule = async () => {
|
||||
mediaRuntimeModulePromise ??= import("openclaw/plugin-sdk/media-runtime");
|
||||
return await mediaRuntimeModulePromise;
|
||||
};
|
||||
|
||||
function createBuiltinAdapter(): PlatformAdapter {
|
||||
return {
|
||||
async validateRemoteUrl(_url: string, _options?: { allowPrivate?: boolean }): Promise<void> {
|
||||
@@ -52,7 +60,7 @@ function createBuiltinAdapter(): PlatformAdapter {
|
||||
},
|
||||
|
||||
async downloadFile(url: string, destDir: string, filename?: string): Promise<string> {
|
||||
const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
|
||||
const { readRemoteMediaBuffer } = await loadMediaRuntimeModule();
|
||||
const result = await readRemoteMediaBuffer({ url, filePathHint: filename });
|
||||
const fs = await import("node:fs");
|
||||
const path = await import("node:path");
|
||||
@@ -65,7 +73,7 @@ function createBuiltinAdapter(): PlatformAdapter {
|
||||
},
|
||||
|
||||
async fetchMedia(options: FetchMediaOptions): Promise<FetchMediaResult> {
|
||||
const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
|
||||
const { readRemoteMediaBuffer } = await loadMediaRuntimeModule();
|
||||
const result = await readRemoteMediaBuffer({
|
||||
url: options.url,
|
||||
filePathHint: options.filePathHint,
|
||||
|
||||
@@ -37,6 +37,14 @@ function loadGatewayModule(): Promise<typeof import("./bridge/gateway.js")> {
|
||||
return gatewayModulePromise;
|
||||
}
|
||||
|
||||
let outboundMessagingModulePromise:
|
||||
| Promise<typeof import("./engine/messaging/outbound.js")>
|
||||
| undefined;
|
||||
function loadOutboundMessagingModule(): Promise<typeof import("./engine/messaging/outbound.js")> {
|
||||
outboundMessagingModulePromise ??= import("./engine/messaging/outbound.js");
|
||||
return outboundMessagingModulePromise;
|
||||
}
|
||||
|
||||
function createQQBotSendReceipt(params: {
|
||||
messageId?: string;
|
||||
target: string;
|
||||
@@ -69,7 +77,7 @@ async function sendQQBotText(params: {
|
||||
// platform adapter, etc.) have executed before engine code runs.
|
||||
await loadGatewayModule();
|
||||
const account = resolveQQBotAccount(params.cfg, params.accountId);
|
||||
const { sendText } = await import("./engine/messaging/outbound.js");
|
||||
const { sendText } = await loadOutboundMessagingModule();
|
||||
const result = await sendText({
|
||||
to: params.to,
|
||||
text: params.text,
|
||||
@@ -100,7 +108,7 @@ async function sendQQBotMedia(params: {
|
||||
// Same guard as sendText — ensure adapters are registered.
|
||||
await loadGatewayModule();
|
||||
const account = resolveQQBotAccount(params.cfg, params.accountId);
|
||||
const { sendMedia } = await import("./engine/messaging/outbound.js");
|
||||
const { sendMedia } = await loadOutboundMessagingModule();
|
||||
const result = await sendMedia({
|
||||
to: params.to,
|
||||
text: params.text ?? "",
|
||||
|
||||
@@ -86,4 +86,25 @@ describe("QQBot token manager", () => {
|
||||
expiresAt: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not throw while logging fetched tokens when the process clock is outside the Date range", async () => {
|
||||
const logger = { debug: vi.fn(), info: vi.fn(), error: vi.fn() };
|
||||
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
|
||||
mockGuardedTokenResponse('{"access_token":"token-1","expires_in":7200}', {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
const manager = new TokenManager({ logger });
|
||||
try {
|
||||
await expect(manager.getAccessToken("app-id", "secret")).resolves.toBe("token-1");
|
||||
} finally {
|
||||
dateNowSpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(manager.getStatus("app-id").expiresAt).toBe(7_200_000);
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
"[qqbot:token:app-id] Cached, expires at: 1970-01-01T02:00:00.000Z",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
* globals, fully supporting multi-account concurrent operation.
|
||||
*/
|
||||
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import {
|
||||
parseStrictPositiveInteger,
|
||||
resolveDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationSeconds,
|
||||
resolveTimestampMsToIsoString,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import type { EngineLogger } from "../types.js";
|
||||
import { formatErrorMessage } from "../utils/format.js";
|
||||
@@ -273,10 +278,14 @@ export class TokenManager {
|
||||
throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + resolveTokenExpiresInSeconds(data.expires_in) * 1000;
|
||||
const nowMs = resolveDateTimestampMs(Date.now());
|
||||
const expiresAt =
|
||||
resolveExpiresAtMsFromDurationSeconds(resolveTokenExpiresInSeconds(data.expires_in), {
|
||||
nowMs,
|
||||
}) ?? nowMs;
|
||||
this.cache.set(appId, { token: data.access_token, expiresAt, appId });
|
||||
this.logger?.debug?.(
|
||||
`[qqbot:token:${appId}] Cached, expires at: ${new Date(expiresAt).toISOString()}`,
|
||||
`[qqbot:token:${appId}] Cached, expires at: ${resolveTimestampMsToIsoString(expiresAt)}`,
|
||||
);
|
||||
|
||||
return data.access_token;
|
||||
|
||||
@@ -4,7 +4,10 @@ import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway
|
||||
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import { resolveCommandAuthorization } from "openclaw/plugin-sdk/command-auth-native";
|
||||
import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
|
||||
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
|
||||
import {
|
||||
parseStrictFiniteNumber,
|
||||
timestampMsToIsoString,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeUniqueTrimmedStringList,
|
||||
@@ -319,7 +322,10 @@ function formatInteractionSelectionLabel(params: {
|
||||
return params.summary.selectedTime;
|
||||
}
|
||||
if (typeof params.summary.selectedDateTime === "number") {
|
||||
return new Date(params.summary.selectedDateTime * 1000).toISOString();
|
||||
const selectedDateTime = timestampMsToIsoString(params.summary.selectedDateTime * 1000);
|
||||
if (selectedDateTime) {
|
||||
return selectedDateTime;
|
||||
}
|
||||
}
|
||||
if (params.summary.richTextPreview) {
|
||||
return params.summary.richTextPreview;
|
||||
|
||||
@@ -2075,6 +2075,54 @@ describe("registerSlackInteractionEvents", () => {
|
||||
expect(payload.selectedDateTime).toBe(1_771_700_200);
|
||||
});
|
||||
|
||||
it("falls back when Slack datetime selection is outside Date range", async () => {
|
||||
const { ctx, app, getHandler } = createContext();
|
||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||
const handler = getHandler();
|
||||
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
await handler({
|
||||
ack,
|
||||
body: {
|
||||
user: { id: "U333" },
|
||||
channel: { id: "C3" },
|
||||
message: {
|
||||
ts: "555.669",
|
||||
text: "fallback",
|
||||
blocks: [
|
||||
{
|
||||
type: "actions",
|
||||
block_id: "datetime_block",
|
||||
elements: [{ type: "datetimepicker", action_id: "openclaw:datetime" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: {
|
||||
type: "datetimepicker",
|
||||
action_id: "openclaw:datetime",
|
||||
block_id: "datetime_block",
|
||||
selected_date_time: 9_000_000_000_000,
|
||||
},
|
||||
});
|
||||
|
||||
expectRecordFields(chatUpdateCall(app), {
|
||||
channel: "C3",
|
||||
ts: "555.669",
|
||||
blocks: [
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: ":white_check_mark: *openclaw:datetime* selected by <@U333>",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("captures workflow button trigger metadata", async () => {
|
||||
enqueueSystemEventMock.mockClear();
|
||||
const { ctx, getHandler } = createContext();
|
||||
|
||||
@@ -583,6 +583,37 @@ describe("voice-call plugin", () => {
|
||||
expect(runtimeStub.manager.speak).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports stale call history with invalid ended timestamps", async () => {
|
||||
runtimeStub.manager.getCall = vi.fn(() => undefined);
|
||||
runtimeStub.manager.getCallByProviderCallId = vi.fn(() => undefined);
|
||||
runtimeStub.manager.getCallHistory = vi.fn(async () => [
|
||||
createCallRecord({
|
||||
callId: "call-1",
|
||||
providerCallId: "CA123",
|
||||
state: "completed",
|
||||
endReason: "completed",
|
||||
endedAt: Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
]);
|
||||
const { methods } = setup({ provider: "mock" });
|
||||
const handler = methods.get("voicecall.speak") as
|
||||
| ((ctx: {
|
||||
params: Record<string, unknown>;
|
||||
respond: ReturnType<typeof vi.fn>;
|
||||
}) => Promise<void>)
|
||||
| undefined;
|
||||
const respond = vi.fn();
|
||||
|
||||
await handler?.({ params: { callId: "CA123", message: "hello" }, respond });
|
||||
|
||||
const [ok, , error] = firstRespondCall(respond);
|
||||
expect(ok).toBe(false);
|
||||
expect(error?.message).toContain("call is not active");
|
||||
expect(error?.message).toContain("last state=completed");
|
||||
expect(error?.message).toContain("endReason=completed");
|
||||
expect(error?.message).not.toContain("endedAt=");
|
||||
});
|
||||
|
||||
it("normalizes legacy config through runtime creation and warns to run doctor", async () => {
|
||||
const { methods } = setup({
|
||||
enabled: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
||||
import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { Type } from "typebox";
|
||||
import {
|
||||
@@ -347,10 +348,11 @@ export default definePluginEntry({
|
||||
if (!call) {
|
||||
return undefined;
|
||||
}
|
||||
const endedAt = timestampMsToIsoString(call.endedAt);
|
||||
const details = [
|
||||
`last state=${call.state}`,
|
||||
call.endReason ? `endReason=${call.endReason}` : undefined,
|
||||
call.endedAt ? `endedAt=${new Date(call.endedAt).toISOString()}` : undefined,
|
||||
endedAt ? `endedAt=${endedAt}` : undefined,
|
||||
].filter(Boolean);
|
||||
return `call is not active (${details.join(", ")})`;
|
||||
};
|
||||
|
||||
545
npm-shrinkwrap.json
generated
545
npm-shrinkwrap.json
generated
@@ -74,7 +74,6 @@
|
||||
"node": ">=22.19.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sharp": "0.34.5",
|
||||
"sqlite-vec": "0.1.9"
|
||||
}
|
||||
},
|
||||
@@ -168,16 +167,6 @@
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/genai": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.6.0.tgz",
|
||||
@@ -265,472 +254,6 @@
|
||||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
@@ -1475,16 +998,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz",
|
||||
@@ -3238,19 +2751,6 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
@@ -3314,51 +2814,6 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -1941,7 +1941,6 @@
|
||||
"vitest": "4.1.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sharp": "0.34.5",
|
||||
"sqlite-vec": "0.1.9"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
34
packages/code-mode-runtime/package.json
Normal file
34
packages/code-mode-runtime/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@openclaw/code-mode-runtime",
|
||||
"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"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.mts",
|
||||
"import": "./dist/types.mjs",
|
||||
"default": "./dist/types.mjs"
|
||||
},
|
||||
"./worker": {
|
||||
"types": "./dist/worker.d.mts",
|
||||
"import": "./dist/worker.mjs",
|
||||
"default": "./dist/worker.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown src/index.ts src/types.ts src/worker.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"quickjs-wasi": "3.0.0"
|
||||
}
|
||||
}
|
||||
18
packages/code-mode-runtime/src/index.ts
Normal file
18
packages/code-mode-runtime/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export type {
|
||||
CodeModeBridgeMethod,
|
||||
CodeModeFailureCode,
|
||||
CodeModePendingBridgeRequest,
|
||||
CodeModeRuntimeConfig,
|
||||
CodeModeSettledBridgeRequest,
|
||||
CodeModeWorkerInput,
|
||||
CodeModeWorkerResult,
|
||||
} from "./types.js";
|
||||
|
||||
export function resolveCodeModeRuntimeWorkerUrl(currentModuleUrl = import.meta.url): URL {
|
||||
const currentPath = fileURLToPath(currentModuleUrl);
|
||||
const extension = path.extname(currentPath) || ".js";
|
||||
return new URL(`./worker${extension}`, currentModuleUrl);
|
||||
}
|
||||
62
packages/code-mode-runtime/src/types.ts
Normal file
62
packages/code-mode-runtime/src/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export type CodeModeBridgeMethod = "search" | "describe" | "call" | "yield";
|
||||
|
||||
export type CodeModeRuntimeConfig = {
|
||||
timeoutMs: number;
|
||||
memoryLimitBytes: number;
|
||||
maxPendingToolCalls: number;
|
||||
maxSnapshotBytes: number;
|
||||
};
|
||||
|
||||
export type CodeModePendingBridgeRequest = {
|
||||
id: string;
|
||||
method: CodeModeBridgeMethod;
|
||||
args: unknown[];
|
||||
};
|
||||
|
||||
export type CodeModeSettledBridgeRequest = {
|
||||
id: string;
|
||||
ok: boolean;
|
||||
value?: unknown;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type CodeModeWorkerInput =
|
||||
| {
|
||||
kind: "exec";
|
||||
source: string;
|
||||
config: CodeModeRuntimeConfig;
|
||||
catalog: unknown[];
|
||||
}
|
||||
| {
|
||||
kind: "resume";
|
||||
snapshotBytes: Uint8Array;
|
||||
config: CodeModeRuntimeConfig;
|
||||
settledRequests: CodeModeSettledBridgeRequest[];
|
||||
};
|
||||
|
||||
export type CodeModeFailureCode =
|
||||
| "invalid_input"
|
||||
| "runtime_unavailable"
|
||||
| "timeout"
|
||||
| "output_limit_exceeded"
|
||||
| "snapshot_limit_exceeded"
|
||||
| "internal_error";
|
||||
|
||||
export type CodeModeWorkerResult =
|
||||
| {
|
||||
status: "completed";
|
||||
value: unknown;
|
||||
output: unknown[];
|
||||
}
|
||||
| {
|
||||
status: "waiting";
|
||||
snapshotBytes: Uint8Array;
|
||||
pendingRequests: CodeModePendingBridgeRequest[];
|
||||
output: unknown[];
|
||||
}
|
||||
| {
|
||||
status: "failed";
|
||||
error: string;
|
||||
code: CodeModeFailureCode;
|
||||
output: unknown[];
|
||||
};
|
||||
555
packages/code-mode-runtime/src/worker.ts
Normal file
555
packages/code-mode-runtime/src/worker.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { parentPort, workerData } from "node:worker_threads";
|
||||
import { EvalFlags, Intrinsics, JSException, QuickJS, type JSValueHandle } from "quickjs-wasi";
|
||||
import type {
|
||||
CodeModePendingBridgeRequest,
|
||||
CodeModeRuntimeConfig,
|
||||
CodeModeSettledBridgeRequest,
|
||||
CodeModeWorkerInput,
|
||||
CodeModeWorkerResult,
|
||||
} from "./types.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const QUICKJS_WASM_PATH = require.resolve("quickjs-wasi/quickjs.wasm");
|
||||
let quickJsWasmModulePromise: Promise<WebAssembly.Module> | undefined;
|
||||
|
||||
class CodeModeWorkerFailure extends Error {
|
||||
readonly code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"];
|
||||
|
||||
constructor(
|
||||
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
|
||||
message: string,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
this.name = "CodeModeWorkerFailure";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
class CodeModeWorkerFailureWithOutput extends CodeModeWorkerFailure {
|
||||
readonly output: unknown[];
|
||||
|
||||
constructor(
|
||||
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
|
||||
message: string,
|
||||
output: unknown[],
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.name = "CodeModeWorkerFailureWithOutput";
|
||||
this.output = output;
|
||||
}
|
||||
}
|
||||
|
||||
class CodeModeGuestError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "CodeModeGuestError";
|
||||
}
|
||||
}
|
||||
|
||||
function isQuickJsInterruptedError(error: unknown): boolean {
|
||||
if (error instanceof CodeModeGuestError) {
|
||||
return false;
|
||||
}
|
||||
return errorMessage(error) === "interrupted";
|
||||
}
|
||||
|
||||
type VmRun = {
|
||||
vm: QuickJS;
|
||||
didTimeout: () => boolean;
|
||||
};
|
||||
|
||||
function getQuickJsWasmModule(): Promise<WebAssembly.Module> {
|
||||
quickJsWasmModulePromise ??= readFile(QUICKJS_WASM_PATH).then((bytes) =>
|
||||
WebAssembly.compile(bytes),
|
||||
);
|
||||
return quickJsWasmModulePromise;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function toJsonSafe(value: unknown): unknown {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value)) as unknown;
|
||||
} catch {
|
||||
if (value instanceof Error) {
|
||||
return { name: value.name, message: value.message };
|
||||
}
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
case "number":
|
||||
case "boolean":
|
||||
return value;
|
||||
case "bigint":
|
||||
case "symbol":
|
||||
case "function":
|
||||
return String(value);
|
||||
default:
|
||||
return Object.prototype.toString.call(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
if (error instanceof JSException) {
|
||||
return error.stack || error.message || String(error);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message || String(error);
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
const CONTROLLER_SOURCE = String.raw`
|
||||
(() => {
|
||||
const output = [];
|
||||
const pending = new Map();
|
||||
const catalog = Array.isArray(globalThis.__openclawCatalog) ? globalThis.__openclawCatalog : [];
|
||||
|
||||
function safe(value) {
|
||||
if (value === undefined) return null;
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch {
|
||||
if (value instanceof Error) {
|
||||
return { name: value.name, message: value.message };
|
||||
}
|
||||
if (value === null) return null;
|
||||
const type = typeof value;
|
||||
if (type === "string" || type === "number" || type === "boolean") return value;
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function asText(value) {
|
||||
if (typeof value === "string") return value;
|
||||
const encoded = JSON.stringify(safe(value));
|
||||
return typeof encoded === "string" ? encoded : String(value);
|
||||
}
|
||||
|
||||
function request(method, args) {
|
||||
const id = String(globalThis.__openclawHostRequest(String(method), JSON.stringify(safe(args ?? []))));
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
function settle(id, ok, payload) {
|
||||
const entry = pending.get(String(id));
|
||||
if (!entry) return false;
|
||||
pending.delete(String(id));
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(String(payload));
|
||||
} catch {
|
||||
parsed = String(payload);
|
||||
}
|
||||
if (ok) {
|
||||
entry.resolve(parsed);
|
||||
} else {
|
||||
const error = new Error(typeof parsed === "string" ? parsed : parsed?.message ?? "nested tool failed");
|
||||
entry.reject(error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const baseTools = Object.create(null);
|
||||
Object.defineProperties(baseTools, {
|
||||
search: { value: (query, options) => request("search", [query, options]), enumerable: true },
|
||||
describe: { value: (id) => request("describe", [id]), enumerable: true },
|
||||
call: { value: (id, input) => request("call", [id, input]), enumerable: true },
|
||||
});
|
||||
|
||||
const safeNameCounts = new Map();
|
||||
for (const tool of catalog) {
|
||||
const name = typeof tool?.name === "string" ? tool.name : "";
|
||||
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue;
|
||||
safeNameCounts.set(name, (safeNameCounts.get(name) ?? 0) + 1);
|
||||
}
|
||||
for (const tool of catalog) {
|
||||
const name = typeof tool?.name === "string" ? tool.name : "";
|
||||
const id = typeof tool?.id === "string" ? tool.id : "";
|
||||
if (!id || safeNameCounts.get(name) !== 1 || Object.prototype.hasOwnProperty.call(baseTools, name)) {
|
||||
continue;
|
||||
}
|
||||
Object.defineProperty(baseTools, name, {
|
||||
value: (input) => request("call", [id, input]),
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperties(globalThis, {
|
||||
ALL_TOOLS: { value: Object.freeze(catalog.slice()), enumerable: true },
|
||||
tools: { value: Object.freeze(baseTools), enumerable: true },
|
||||
text: { value: (value) => output.push({ type: "text", text: asText(value) }), enumerable: true },
|
||||
json: { value: (value) => output.push({ type: "json", value: safe(value) }), enumerable: true },
|
||||
yield_control: { value: (reason) => request("yield", [reason]), enumerable: true },
|
||||
__openclawSettleBridge: { value: settle },
|
||||
__openclawTakeOutput: { value: () => output.splice(0) },
|
||||
});
|
||||
})();
|
||||
`;
|
||||
|
||||
function buildUserSource(code: string): string {
|
||||
return `globalThis.__openclawResult = (async () => {\n${code}\n})()`;
|
||||
}
|
||||
|
||||
function createHostRequestHandler(params: {
|
||||
vm: QuickJS;
|
||||
pendingRequests: CodeModePendingBridgeRequest[];
|
||||
config: CodeModeRuntimeConfig;
|
||||
}): (this: JSValueHandle, method: JSValueHandle, argsJson: JSValueHandle) => JSValueHandle {
|
||||
return (methodHandle, argsHandle) => {
|
||||
if (params.pendingRequests.length >= params.config.maxPendingToolCalls) {
|
||||
throw new Error("too many pending code mode tool calls");
|
||||
}
|
||||
const method = methodHandle.toString();
|
||||
if (method !== "search" && method !== "describe" && method !== "call" && method !== "yield") {
|
||||
throw new Error("unsupported code mode bridge method");
|
||||
}
|
||||
let args: unknown = [];
|
||||
try {
|
||||
args = JSON.parse(argsHandle.toString()) as unknown;
|
||||
} catch {
|
||||
args = [];
|
||||
}
|
||||
const id = `bridge:${params.pendingRequests.length + 1}:${randomUUID()}`;
|
||||
params.pendingRequests.push({
|
||||
id,
|
||||
method,
|
||||
args: Array.isArray(args) ? args : [],
|
||||
});
|
||||
return params.vm.newString(id);
|
||||
};
|
||||
}
|
||||
|
||||
async function createVm(params: {
|
||||
catalog: unknown[];
|
||||
config: CodeModeRuntimeConfig;
|
||||
pendingRequests: CodeModePendingBridgeRequest[];
|
||||
}): Promise<VmRun> {
|
||||
const startedAt = Date.now();
|
||||
let timedOut = false;
|
||||
const vm = await QuickJS.create({
|
||||
wasm: await getQuickJsWasmModule(),
|
||||
memoryLimit: params.config.memoryLimitBytes,
|
||||
intrinsics: Intrinsics.ALL,
|
||||
timezoneOffset: 0,
|
||||
interruptHandler: () => {
|
||||
timedOut = Date.now() - startedAt > params.config.timeoutMs;
|
||||
return timedOut;
|
||||
},
|
||||
});
|
||||
const catalogHandle = vm.hostToHandle(params.catalog);
|
||||
try {
|
||||
vm.setProp(vm.global, "__openclawCatalog", catalogHandle);
|
||||
} finally {
|
||||
catalogHandle.dispose();
|
||||
}
|
||||
const hostRequest = vm.newFunction(
|
||||
"__openclawHostRequest",
|
||||
createHostRequestHandler({
|
||||
vm,
|
||||
pendingRequests: params.pendingRequests,
|
||||
config: params.config,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
vm.setProp(vm.global, "__openclawHostRequest", hostRequest);
|
||||
} finally {
|
||||
hostRequest.dispose();
|
||||
}
|
||||
vm.evalCode(CONTROLLER_SOURCE, "openclaw-code-mode:controller.js").dispose();
|
||||
return { vm, didTimeout: () => timedOut };
|
||||
}
|
||||
|
||||
async function restoreVm(params: {
|
||||
snapshotBytes: Uint8Array;
|
||||
config: CodeModeRuntimeConfig;
|
||||
pendingRequests: CodeModePendingBridgeRequest[];
|
||||
}): Promise<VmRun> {
|
||||
const startedAt = Date.now();
|
||||
let timedOut = false;
|
||||
const snapshot = QuickJS.deserializeSnapshot(params.snapshotBytes);
|
||||
const vm = await QuickJS.restore(snapshot, {
|
||||
wasm: await getQuickJsWasmModule(),
|
||||
memoryLimit: params.config.memoryLimitBytes,
|
||||
intrinsics: Intrinsics.ALL,
|
||||
timezoneOffset: 0,
|
||||
interruptHandler: () => {
|
||||
timedOut = Date.now() - startedAt > params.config.timeoutMs;
|
||||
return timedOut;
|
||||
},
|
||||
});
|
||||
vm.registerHostCallback(
|
||||
"__openclawHostRequest",
|
||||
createHostRequestHandler({
|
||||
vm,
|
||||
pendingRequests: params.pendingRequests,
|
||||
config: params.config,
|
||||
}),
|
||||
);
|
||||
return { vm, didTimeout: () => timedOut };
|
||||
}
|
||||
|
||||
function takeOutput(vm: QuickJS): unknown[] {
|
||||
const take = vm.global.getProp("__openclawTakeOutput");
|
||||
try {
|
||||
const output = vm.callFunction(take, vm.undefined);
|
||||
try {
|
||||
const dumped = vm.dump(output);
|
||||
return Array.isArray(dumped) ? (dumped as unknown[]) : [];
|
||||
} finally {
|
||||
output.dispose();
|
||||
}
|
||||
} finally {
|
||||
take.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function takeOutputSafely(vm: QuickJS): unknown[] {
|
||||
try {
|
||||
return takeOutput(vm);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function throwWorkerFailureWithOutput(params: {
|
||||
error: unknown;
|
||||
didTimeout: () => boolean;
|
||||
output: unknown[];
|
||||
vm: QuickJS;
|
||||
}): never {
|
||||
const timedOut = params.didTimeout() || isQuickJsInterruptedError(params.error);
|
||||
const failureOutput = params.output.length > 0 ? params.output : takeOutputSafely(params.vm);
|
||||
if (timedOut) {
|
||||
throw new CodeModeWorkerFailureWithOutput(
|
||||
"timeout",
|
||||
"code mode timeout exceeded",
|
||||
failureOutput,
|
||||
{ cause: params.error },
|
||||
);
|
||||
}
|
||||
if (params.error instanceof CodeModeWorkerFailure) {
|
||||
throw new CodeModeWorkerFailureWithOutput(
|
||||
params.error.code,
|
||||
params.error.message,
|
||||
failureOutput,
|
||||
{ cause: params.error },
|
||||
);
|
||||
}
|
||||
if (failureOutput.length > 0) {
|
||||
throw new CodeModeWorkerFailureWithOutput(
|
||||
"internal_error",
|
||||
errorMessage(params.error),
|
||||
failureOutput,
|
||||
{ cause: params.error },
|
||||
);
|
||||
}
|
||||
throw params.error;
|
||||
}
|
||||
|
||||
function drainPendingJobs(vm: QuickJS): void {
|
||||
for (let index = 0; index < 1000; index += 1) {
|
||||
if (vm.executePendingJobs() === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error("code mode pending job limit exceeded");
|
||||
}
|
||||
|
||||
function getResultHandle(vm: QuickJS): JSValueHandle {
|
||||
return vm.global.getProp("__openclawResult");
|
||||
}
|
||||
|
||||
async function readCompletedResult(vm: QuickJS, resultHandle: JSValueHandle): Promise<unknown> {
|
||||
if (!resultHandle.isPromise) {
|
||||
return toJsonSafe(vm.dump(resultHandle));
|
||||
}
|
||||
const settled = await vm.resolvePromise(resultHandle);
|
||||
if ("error" in settled) {
|
||||
try {
|
||||
throw new CodeModeGuestError(errorMessage(vm.dump(settled.error)));
|
||||
} finally {
|
||||
settled.error.dispose();
|
||||
}
|
||||
}
|
||||
try {
|
||||
return toJsonSafe(vm.dump(settled.value));
|
||||
} finally {
|
||||
settled.value.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function waitingResult(params: {
|
||||
vm: QuickJS;
|
||||
pendingRequests: CodeModePendingBridgeRequest[];
|
||||
output: unknown[];
|
||||
config: CodeModeRuntimeConfig;
|
||||
}): CodeModeWorkerResult {
|
||||
const snapshotBytes = QuickJS.serializeSnapshot(params.vm.snapshot());
|
||||
if (snapshotBytes.byteLength > params.config.maxSnapshotBytes) {
|
||||
throw new CodeModeWorkerFailure("snapshot_limit_exceeded", "code mode snapshot limit exceeded");
|
||||
}
|
||||
return {
|
||||
status: "waiting",
|
||||
snapshotBytes,
|
||||
pendingRequests: params.pendingRequests,
|
||||
output: params.output,
|
||||
};
|
||||
}
|
||||
|
||||
async function runExec(input: Extract<CodeModeWorkerInput, { kind: "exec" }>) {
|
||||
const pendingRequests: CodeModePendingBridgeRequest[] = [];
|
||||
const { vm, didTimeout } = await createVm({
|
||||
catalog: input.catalog,
|
||||
config: input.config,
|
||||
pendingRequests,
|
||||
});
|
||||
let output: unknown[] = [];
|
||||
try {
|
||||
vm.evalCode(
|
||||
buildUserSource(input.source),
|
||||
"openclaw-code-mode:user.js",
|
||||
EvalFlags.ASYNC,
|
||||
).dispose();
|
||||
drainPendingJobs(vm);
|
||||
output = takeOutput(vm);
|
||||
const resultHandle = getResultHandle(vm);
|
||||
try {
|
||||
if (pendingRequests.length > 0) {
|
||||
return waitingResult({ vm, pendingRequests, output, config: input.config });
|
||||
}
|
||||
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
|
||||
throw new Error("code mode promise is pending without host work");
|
||||
}
|
||||
return {
|
||||
status: "completed" as const,
|
||||
value: await readCompletedResult(vm, resultHandle),
|
||||
output,
|
||||
};
|
||||
} finally {
|
||||
resultHandle.dispose();
|
||||
}
|
||||
} catch (error) {
|
||||
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
|
||||
} finally {
|
||||
vm.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async function runResume(input: Extract<CodeModeWorkerInput, { kind: "resume" }>) {
|
||||
const pendingRequests: CodeModePendingBridgeRequest[] = [];
|
||||
const { vm, didTimeout } = await restoreVm({
|
||||
snapshotBytes: input.snapshotBytes,
|
||||
config: input.config,
|
||||
pendingRequests,
|
||||
});
|
||||
let output: unknown[] = [];
|
||||
try {
|
||||
const settle = vm.global.getProp("__openclawSettleBridge");
|
||||
try {
|
||||
for (const request of input.settledRequests) {
|
||||
const id = vm.newString(request.id);
|
||||
const payload = vm.newString(JSON.stringify(request.ok ? request.value : request.error));
|
||||
try {
|
||||
vm.callFunction(
|
||||
settle,
|
||||
vm.undefined,
|
||||
id,
|
||||
request.ok ? vm.true : vm.false,
|
||||
payload,
|
||||
).dispose();
|
||||
} finally {
|
||||
id.dispose();
|
||||
payload.dispose();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
settle.dispose();
|
||||
}
|
||||
drainPendingJobs(vm);
|
||||
output = takeOutput(vm);
|
||||
const resultHandle = getResultHandle(vm);
|
||||
try {
|
||||
if (pendingRequests.length > 0) {
|
||||
return waitingResult({ vm, pendingRequests, output, config: input.config });
|
||||
}
|
||||
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
|
||||
throw new Error("code mode promise is pending without host work");
|
||||
}
|
||||
return {
|
||||
status: "completed" as const,
|
||||
value: await readCompletedResult(vm, resultHandle),
|
||||
output,
|
||||
};
|
||||
} finally {
|
||||
resultHandle.dispose();
|
||||
}
|
||||
} catch (error) {
|
||||
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
|
||||
} finally {
|
||||
vm.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<CodeModeWorkerResult> {
|
||||
const input = workerData as unknown;
|
||||
if (!isRecord(input) || !isRecord(input.config)) {
|
||||
return {
|
||||
status: "failed",
|
||||
error: "invalid code mode worker input",
|
||||
code: "invalid_input",
|
||||
output: [],
|
||||
};
|
||||
}
|
||||
try {
|
||||
if (input.kind === "exec" && typeof input.source === "string") {
|
||||
return await runExec({
|
||||
kind: "exec",
|
||||
source: input.source,
|
||||
config: input.config as CodeModeRuntimeConfig,
|
||||
catalog: Array.isArray(input.catalog) ? input.catalog : [],
|
||||
});
|
||||
}
|
||||
if (input.kind === "resume" && input.snapshotBytes instanceof Uint8Array) {
|
||||
return await runResume({
|
||||
kind: "resume",
|
||||
snapshotBytes: input.snapshotBytes,
|
||||
config: input.config as CodeModeRuntimeConfig,
|
||||
settledRequests: Array.isArray(input.settledRequests)
|
||||
? (input.settledRequests as CodeModeSettledBridgeRequest[])
|
||||
: [],
|
||||
});
|
||||
}
|
||||
return {
|
||||
status: "failed",
|
||||
error: "invalid code mode worker input",
|
||||
code: "invalid_input",
|
||||
output: [],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "failed",
|
||||
error: errorMessage(error),
|
||||
code: error instanceof CodeModeWorkerFailure ? error.code : "internal_error",
|
||||
output: error instanceof CodeModeWorkerFailureWithOutput ? error.output : [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line unicorn/require-post-message-target-origin -- Node worker_threads MessagePort, not window.postMessage.
|
||||
parentPort?.postMessage(await main());
|
||||
8
packages/code-mode-runtime/tsconfig.json
Normal file
8
packages/code-mode-runtime/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
7
packages/media-understanding-common/dist/active-model.d.mts
vendored
Normal file
7
packages/media-understanding-common/dist/active-model.d.mts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
//#region packages/media-understanding-common/src/active-model.d.ts
|
||||
type ActiveMediaModel = {
|
||||
provider: string;
|
||||
model?: string;
|
||||
};
|
||||
//#endregion
|
||||
export { ActiveMediaModel };
|
||||
1
packages/media-understanding-common/dist/active-model.mjs
vendored
Normal file
1
packages/media-understanding-common/dist/active-model.mjs
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
14
packages/media-understanding-common/dist/defaults.d.mts
vendored
Normal file
14
packages/media-understanding-common/dist/defaults.d.mts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import { MediaUnderstandingCapability } from "./types.mjs";
|
||||
|
||||
//#region packages/media-understanding-common/src/defaults.d.ts
|
||||
declare const DEFAULT_MAX_CHARS = 500;
|
||||
declare const DEFAULT_MAX_CHARS_BY_CAPABILITY: Record<MediaUnderstandingCapability, number | undefined>;
|
||||
declare const DEFAULT_MAX_BYTES: Record<MediaUnderstandingCapability, number>;
|
||||
declare const DEFAULT_TIMEOUT_SECONDS: Record<MediaUnderstandingCapability, number>;
|
||||
declare const DEFAULT_PROMPT: Record<MediaUnderstandingCapability, string>;
|
||||
declare const DEFAULT_VIDEO_MAX_BASE64_BYTES: number;
|
||||
declare const CLI_OUTPUT_MAX_BUFFER: number;
|
||||
declare const DEFAULT_MEDIA_CONCURRENCY = 2;
|
||||
declare const MIN_AUDIO_FILE_BYTES = 1024;
|
||||
//#endregion
|
||||
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES };
|
||||
29
packages/media-understanding-common/dist/defaults.mjs
vendored
Normal file
29
packages/media-understanding-common/dist/defaults.mjs
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
//#region packages/media-understanding-common/src/defaults.ts
|
||||
const MB = 1024 * 1024;
|
||||
const DEFAULT_MAX_CHARS = 500;
|
||||
const DEFAULT_MAX_CHARS_BY_CAPABILITY = {
|
||||
image: 500,
|
||||
audio: void 0,
|
||||
video: 500
|
||||
};
|
||||
const DEFAULT_MAX_BYTES = {
|
||||
image: 10 * MB,
|
||||
audio: 20 * MB,
|
||||
video: 50 * MB
|
||||
};
|
||||
const DEFAULT_TIMEOUT_SECONDS = {
|
||||
image: 60,
|
||||
audio: 60,
|
||||
video: 120
|
||||
};
|
||||
const DEFAULT_PROMPT = {
|
||||
image: "Describe the image.",
|
||||
audio: "Transcribe the audio.",
|
||||
video: "Describe the video."
|
||||
};
|
||||
const DEFAULT_VIDEO_MAX_BASE64_BYTES = 70 * MB;
|
||||
const CLI_OUTPUT_MAX_BUFFER = 5 * MB;
|
||||
const DEFAULT_MEDIA_CONCURRENCY = 2;
|
||||
const MIN_AUDIO_FILE_BYTES = 1024;
|
||||
//#endregion
|
||||
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES };
|
||||
9
packages/media-understanding-common/dist/errors.d.mts
vendored
Normal file
9
packages/media-understanding-common/dist/errors.d.mts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
//#region packages/media-understanding-common/src/errors.d.ts
|
||||
type MediaUnderstandingSkipReason = "maxBytes" | "timeout" | "unsupported" | "empty" | "blocked" | "tooSmall";
|
||||
declare class MediaUnderstandingSkipError extends Error {
|
||||
readonly reason: MediaUnderstandingSkipReason;
|
||||
constructor(reason: MediaUnderstandingSkipReason, message: string);
|
||||
}
|
||||
declare function isMediaUnderstandingSkipError(err: unknown): err is MediaUnderstandingSkipError;
|
||||
//#endregion
|
||||
export { MediaUnderstandingSkipError, isMediaUnderstandingSkipError };
|
||||
13
packages/media-understanding-common/dist/errors.mjs
vendored
Normal file
13
packages/media-understanding-common/dist/errors.mjs
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
//#region packages/media-understanding-common/src/errors.ts
|
||||
var MediaUnderstandingSkipError = class extends Error {
|
||||
constructor(reason, message) {
|
||||
super(message);
|
||||
this.reason = reason;
|
||||
this.name = "MediaUnderstandingSkipError";
|
||||
}
|
||||
};
|
||||
function isMediaUnderstandingSkipError(err) {
|
||||
return err instanceof MediaUnderstandingSkipError;
|
||||
}
|
||||
//#endregion
|
||||
export { MediaUnderstandingSkipError, isMediaUnderstandingSkipError };
|
||||
11
packages/media-understanding-common/dist/format.d.mts
vendored
Normal file
11
packages/media-understanding-common/dist/format.d.mts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { MediaUnderstandingOutput } from "./types.mjs";
|
||||
|
||||
//#region packages/media-understanding-common/src/format.d.ts
|
||||
declare function extractMediaUserText(body?: string): string | undefined;
|
||||
declare function formatMediaUnderstandingBody(params: {
|
||||
body?: string;
|
||||
outputs: MediaUnderstandingOutput[];
|
||||
}): string;
|
||||
declare function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string;
|
||||
//#endregion
|
||||
export { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody };
|
||||
47
packages/media-understanding-common/dist/format.mjs
vendored
Normal file
47
packages/media-understanding-common/dist/format.mjs
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
//#region packages/media-understanding-common/src/format.ts
|
||||
const MEDIA_PLACEHOLDER_RE = /^<media:[^>]+>(\s*\([^)]*\))?$/i;
|
||||
const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
|
||||
function extractMediaUserText(body) {
|
||||
const trimmed = body?.trim() ?? "";
|
||||
if (!trimmed) return;
|
||||
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) return;
|
||||
return trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim() || void 0;
|
||||
}
|
||||
function formatSection(title, kind, text, userText) {
|
||||
const lines = [`[${title}]`];
|
||||
if (userText) lines.push(`User text:\n${userText}`);
|
||||
lines.push(`${kind}:\n${text}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
function formatMediaUnderstandingBody(params) {
|
||||
const outputs = params.outputs.filter((output) => output.text.trim());
|
||||
if (outputs.length === 0) return params.body ?? "";
|
||||
const userText = extractMediaUserText(params.body);
|
||||
const sections = [];
|
||||
if (userText && outputs.length > 1) sections.push(`User text:\n${userText}`);
|
||||
const counts = /* @__PURE__ */ new Map();
|
||||
for (const output of outputs) counts.set(output.kind, (counts.get(output.kind) ?? 0) + 1);
|
||||
const seen = /* @__PURE__ */ new Map();
|
||||
for (const output of outputs) {
|
||||
const count = counts.get(output.kind) ?? 1;
|
||||
const next = (seen.get(output.kind) ?? 0) + 1;
|
||||
seen.set(output.kind, next);
|
||||
const suffix = count > 1 ? ` ${next}/${count}` : "";
|
||||
if (output.kind === "audio.transcription") {
|
||||
sections.push(formatSection(`Audio${suffix}`, "Transcript", output.text, outputs.length === 1 ? userText : void 0));
|
||||
continue;
|
||||
}
|
||||
if (output.kind === "image.description") {
|
||||
sections.push(formatSection(`Image${suffix}`, "Description", output.text, outputs.length === 1 ? userText : void 0));
|
||||
continue;
|
||||
}
|
||||
sections.push(formatSection(`Video${suffix}`, "Description", output.text, outputs.length === 1 ? userText : void 0));
|
||||
}
|
||||
return sections.join("\n\n").trim();
|
||||
}
|
||||
function formatAudioTranscripts(outputs) {
|
||||
if (outputs.length === 1) return outputs[0].text;
|
||||
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
|
||||
}
|
||||
//#endregion
|
||||
export { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody };
|
||||
11
packages/media-understanding-common/dist/index.d.mts
vendored
Normal file
11
packages/media-understanding-common/dist/index.d.mts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ActiveMediaModel } from "./active-model.mjs";
|
||||
import { MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider } from "./types.mjs";
|
||||
import { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES } from "./defaults.mjs";
|
||||
import { MediaUnderstandingSkipError, isMediaUnderstandingSkipError } from "./errors.mjs";
|
||||
import { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody } from "./format.mjs";
|
||||
import { OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString } from "./openai-compatible-video.mjs";
|
||||
import { extractGeminiResponse } from "./output-extract.mjs";
|
||||
import { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.mjs";
|
||||
import { providerSupportsCapability } from "./provider-supports.mjs";
|
||||
import { estimateBase64Size, resolveVideoMaxBase64Bytes } from "./video.mjs";
|
||||
export { ActiveMediaModel, CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES, MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider, MediaUnderstandingSkipError, OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, estimateBase64Size, extractGeminiResponse, extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody, isMediaUnderstandingSkipError, normalizeMediaExecutionProviderId, normalizeMediaProviderId, providerSupportsCapability, resolveMediaUnderstandingString, resolveVideoMaxBase64Bytes };
|
||||
11
packages/media-understanding-common/dist/index.mjs
vendored
Normal file
11
packages/media-understanding-common/dist/index.mjs
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import "./active-model.mjs";
|
||||
import { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES } from "./defaults.mjs";
|
||||
import { MediaUnderstandingSkipError, isMediaUnderstandingSkipError } from "./errors.mjs";
|
||||
import { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody } from "./format.mjs";
|
||||
import { buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString } from "./openai-compatible-video.mjs";
|
||||
import { extractGeminiResponse } from "./output-extract.mjs";
|
||||
import { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.mjs";
|
||||
import { providerSupportsCapability } from "./provider-supports.mjs";
|
||||
import "./types.mjs";
|
||||
import { estimateBase64Size, resolveVideoMaxBase64Bytes } from "./video.mjs";
|
||||
export { CLI_OUTPUT_MAX_BUFFER, DEFAULT_MAX_BYTES, DEFAULT_MAX_CHARS, DEFAULT_MAX_CHARS_BY_CAPABILITY, DEFAULT_MEDIA_CONCURRENCY, DEFAULT_PROMPT, DEFAULT_TIMEOUT_SECONDS, DEFAULT_VIDEO_MAX_BASE64_BYTES, MIN_AUDIO_FILE_BYTES, MediaUnderstandingSkipError, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, estimateBase64Size, extractGeminiResponse, extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody, isMediaUnderstandingSkipError, normalizeMediaExecutionProviderId, normalizeMediaProviderId, providerSupportsCapability, resolveMediaUnderstandingString, resolveVideoMaxBase64Bytes };
|
||||
37
packages/media-understanding-common/dist/openai-compatible-video.d.mts
vendored
Normal file
37
packages/media-understanding-common/dist/openai-compatible-video.d.mts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
//#region packages/media-understanding-common/src/openai-compatible-video.d.ts
|
||||
type OpenAiCompatibleVideoPayload = {
|
||||
choices?: Array<{
|
||||
message?: {
|
||||
content?: string | Array<{
|
||||
text?: string;
|
||||
}>;
|
||||
reasoning_content?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
declare function resolveMediaUnderstandingString(value: string | undefined, fallback: string): string;
|
||||
declare function coerceOpenAiCompatibleVideoText(payload: OpenAiCompatibleVideoPayload): string | null;
|
||||
declare function buildOpenAiCompatibleVideoRequestBody(params: {
|
||||
model: string;
|
||||
prompt: string;
|
||||
mime: string;
|
||||
buffer: Buffer;
|
||||
}): {
|
||||
model: string;
|
||||
messages: {
|
||||
role: string;
|
||||
content: ({
|
||||
type: string;
|
||||
text: string;
|
||||
video_url?: undefined;
|
||||
} | {
|
||||
type: string;
|
||||
video_url: {
|
||||
url: string;
|
||||
};
|
||||
text?: undefined;
|
||||
})[];
|
||||
}[];
|
||||
};
|
||||
//#endregion
|
||||
export { OpenAiCompatibleVideoPayload, buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString };
|
||||
32
packages/media-understanding-common/dist/openai-compatible-video.mjs
vendored
Normal file
32
packages/media-understanding-common/dist/openai-compatible-video.mjs
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
//#region packages/media-understanding-common/src/openai-compatible-video.ts
|
||||
function resolveMediaUnderstandingString(value, fallback) {
|
||||
return value?.trim() || fallback;
|
||||
}
|
||||
function coerceOpenAiCompatibleVideoText(payload) {
|
||||
const message = payload.choices?.[0]?.message;
|
||||
if (!message) return null;
|
||||
if (typeof message.content === "string" && message.content.trim()) return message.content.trim();
|
||||
if (Array.isArray(message.content)) {
|
||||
const text = message.content.map((part) => part.text?.trim() ?? "").filter(Boolean).join("\n");
|
||||
if (text) return text;
|
||||
}
|
||||
if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) return message.reasoning_content.trim();
|
||||
return null;
|
||||
}
|
||||
function buildOpenAiCompatibleVideoRequestBody(params) {
|
||||
return {
|
||||
model: params.model,
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: [{
|
||||
type: "text",
|
||||
text: params.prompt
|
||||
}, {
|
||||
type: "video_url",
|
||||
video_url: { url: `data:${params.mime};base64,${params.buffer.toString("base64")}` }
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { buildOpenAiCompatibleVideoRequestBody, coerceOpenAiCompatibleVideoText, resolveMediaUnderstandingString };
|
||||
4
packages/media-understanding-common/dist/output-extract.d.mts
vendored
Normal file
4
packages/media-understanding-common/dist/output-extract.d.mts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
//#region packages/media-understanding-common/src/output-extract.d.ts
|
||||
declare function extractGeminiResponse(raw: string): string | null;
|
||||
//#endregion
|
||||
export { extractGeminiResponse };
|
||||
21
packages/media-understanding-common/dist/output-extract.mjs
vendored
Normal file
21
packages/media-understanding-common/dist/output-extract.mjs
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
//#region packages/media-understanding-common/src/output-extract.ts
|
||||
function extractLastJsonObject(raw) {
|
||||
const trimmed = raw.trim();
|
||||
const start = trimmed.lastIndexOf("{");
|
||||
if (start === -1) return null;
|
||||
const slice = trimmed.slice(start);
|
||||
try {
|
||||
return JSON.parse(slice);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function extractGeminiResponse(raw) {
|
||||
const payload = extractLastJsonObject(raw);
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
const response = payload.response;
|
||||
if (typeof response !== "string") return null;
|
||||
return response.trim() || null;
|
||||
}
|
||||
//#endregion
|
||||
export { extractGeminiResponse };
|
||||
5
packages/media-understanding-common/dist/provider-id.d.mts
vendored
Normal file
5
packages/media-understanding-common/dist/provider-id.d.mts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
//#region packages/media-understanding-common/src/provider-id.d.ts
|
||||
declare function normalizeMediaProviderId(id: string): string;
|
||||
declare function normalizeMediaExecutionProviderId(id: string): string;
|
||||
//#endregion
|
||||
export { normalizeMediaExecutionProviderId, normalizeMediaProviderId };
|
||||
18
packages/media-understanding-common/dist/provider-id.mjs
vendored
Normal file
18
packages/media-understanding-common/dist/provider-id.mjs
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
//#region packages/media-understanding-common/src/provider-id.ts
|
||||
function normalizeProviderId(provider) {
|
||||
return provider.trim().toLowerCase();
|
||||
}
|
||||
function normalizeMediaProviderId(id) {
|
||||
const normalized = normalizeProviderId(id);
|
||||
if (normalized === "gemini") return "google";
|
||||
if (normalized === "minimax-cn") return "minimax";
|
||||
if (normalized === "minimax-portal-cn") return "minimax-portal";
|
||||
return normalized;
|
||||
}
|
||||
function normalizeMediaExecutionProviderId(id) {
|
||||
const normalized = normalizeProviderId(id);
|
||||
if (normalized === "minimax-cn" || normalized === "minimax-portal-cn") return normalized;
|
||||
return normalizeMediaProviderId(normalized);
|
||||
}
|
||||
//#endregion
|
||||
export { normalizeMediaExecutionProviderId, normalizeMediaProviderId };
|
||||
6
packages/media-understanding-common/dist/provider-supports.d.mts
vendored
Normal file
6
packages/media-understanding-common/dist/provider-supports.d.mts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import { MediaUnderstandingCapability, MediaUnderstandingProvider } from "./types.mjs";
|
||||
|
||||
//#region packages/media-understanding-common/src/provider-supports.d.ts
|
||||
declare function providerSupportsCapability(provider: MediaUnderstandingProvider | undefined, capability: MediaUnderstandingCapability): boolean;
|
||||
//#endregion
|
||||
export { providerSupportsCapability };
|
||||
9
packages/media-understanding-common/dist/provider-supports.mjs
vendored
Normal file
9
packages/media-understanding-common/dist/provider-supports.mjs
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
//#region packages/media-understanding-common/src/provider-supports.ts
|
||||
function providerSupportsCapability(provider, capability) {
|
||||
if (!provider) return false;
|
||||
if (capability === "audio") return Boolean(provider.transcribeAudio);
|
||||
if (capability === "image") return Boolean(provider.describeImage);
|
||||
return Boolean(provider.describeVideo);
|
||||
}
|
||||
//#endregion
|
||||
export { providerSupportsCapability };
|
||||
31
packages/media-understanding-common/dist/types.d.mts
vendored
Normal file
31
packages/media-understanding-common/dist/types.d.mts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
//#region packages/media-understanding-common/src/types.d.ts
|
||||
type MediaUnderstandingKind = "audio.transcription" | "video.description" | "image.description";
|
||||
type MediaUnderstandingCapability = "image" | "audio" | "video";
|
||||
type MediaUnderstandingCapabilityRegistry = Map<string, {
|
||||
capabilities?: MediaUnderstandingCapability[];
|
||||
}>;
|
||||
type MediaAttachment = {
|
||||
path?: string;
|
||||
url?: string;
|
||||
mime?: string;
|
||||
index: number;
|
||||
alreadyTranscribed?: boolean;
|
||||
};
|
||||
type MediaUnderstandingOutput = {
|
||||
kind: MediaUnderstandingKind;
|
||||
attachmentIndex: number;
|
||||
text: string;
|
||||
provider: string;
|
||||
model?: string;
|
||||
};
|
||||
type MediaUnderstandingProvider = {
|
||||
id: string;
|
||||
capabilities?: MediaUnderstandingCapability[];
|
||||
transcribeAudio?: unknown;
|
||||
describeVideo?: unknown;
|
||||
describeImage?: unknown;
|
||||
describeImages?: unknown;
|
||||
extractStructured?: unknown;
|
||||
};
|
||||
//#endregion
|
||||
export { MediaAttachment, MediaUnderstandingCapability, MediaUnderstandingCapabilityRegistry, MediaUnderstandingKind, MediaUnderstandingOutput, MediaUnderstandingProvider };
|
||||
1
packages/media-understanding-common/dist/types.mjs
vendored
Normal file
1
packages/media-understanding-common/dist/types.mjs
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
5
packages/media-understanding-common/dist/video.d.mts
vendored
Normal file
5
packages/media-understanding-common/dist/video.d.mts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
//#region packages/media-understanding-common/src/video.d.ts
|
||||
declare function estimateBase64Size(bytes: number): number;
|
||||
declare function resolveVideoMaxBase64Bytes(maxBytes: number): number;
|
||||
//#endregion
|
||||
export { estimateBase64Size, resolveVideoMaxBase64Bytes };
|
||||
11
packages/media-understanding-common/dist/video.mjs
vendored
Normal file
11
packages/media-understanding-common/dist/video.mjs
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.mjs";
|
||||
//#region packages/media-understanding-common/src/video.ts
|
||||
function estimateBase64Size(bytes) {
|
||||
return Math.ceil(bytes / 3) * 4;
|
||||
}
|
||||
function resolveVideoMaxBase64Bytes(maxBytes) {
|
||||
const expanded = Math.floor(maxBytes * (4 / 3));
|
||||
return Math.min(expanded, DEFAULT_VIDEO_MAX_BASE64_BYTES);
|
||||
}
|
||||
//#endregion
|
||||
export { estimateBase64Size, resolveVideoMaxBase64Bytes };
|
||||
71
packages/media-understanding-common/package.json
Normal file
71
packages/media-understanding-common/package.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "@openclaw/media-understanding-common",
|
||||
"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"
|
||||
},
|
||||
"./active-model": {
|
||||
"types": "./dist/active-model.d.mts",
|
||||
"import": "./dist/active-model.mjs",
|
||||
"default": "./dist/active-model.mjs"
|
||||
},
|
||||
"./defaults": {
|
||||
"types": "./dist/defaults.d.mts",
|
||||
"import": "./dist/defaults.mjs",
|
||||
"default": "./dist/defaults.mjs"
|
||||
},
|
||||
"./errors": {
|
||||
"types": "./dist/errors.d.mts",
|
||||
"import": "./dist/errors.mjs",
|
||||
"default": "./dist/errors.mjs"
|
||||
},
|
||||
"./format": {
|
||||
"types": "./dist/format.d.mts",
|
||||
"import": "./dist/format.mjs",
|
||||
"default": "./dist/format.mjs"
|
||||
},
|
||||
"./openai-compatible-video": {
|
||||
"types": "./dist/openai-compatible-video.d.mts",
|
||||
"import": "./dist/openai-compatible-video.mjs",
|
||||
"default": "./dist/openai-compatible-video.mjs"
|
||||
},
|
||||
"./output-extract": {
|
||||
"types": "./dist/output-extract.d.mts",
|
||||
"import": "./dist/output-extract.mjs",
|
||||
"default": "./dist/output-extract.mjs"
|
||||
},
|
||||
"./provider-id": {
|
||||
"types": "./dist/provider-id.d.mts",
|
||||
"import": "./dist/provider-id.mjs",
|
||||
"default": "./dist/provider-id.mjs"
|
||||
},
|
||||
"./provider-supports": {
|
||||
"types": "./dist/provider-supports.d.mts",
|
||||
"import": "./dist/provider-supports.mjs",
|
||||
"default": "./dist/provider-supports.mjs"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.mts",
|
||||
"import": "./dist/types.mjs",
|
||||
"default": "./dist/types.mjs"
|
||||
},
|
||||
"./video": {
|
||||
"types": "./dist/video.d.mts",
|
||||
"import": "./dist/video.mjs",
|
||||
"default": "./dist/video.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown src/index.ts src/active-model.ts src/defaults.ts src/errors.ts src/format.ts src/openai-compatible-video.ts src/output-extract.ts src/provider-id.ts src/provider-supports.ts src/types.ts src/video.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||
}
|
||||
}
|
||||
4
packages/media-understanding-common/src/active-model.ts
Normal file
4
packages/media-understanding-common/src/active-model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type ActiveMediaModel = {
|
||||
provider: string;
|
||||
model?: string;
|
||||
};
|
||||
32
packages/media-understanding-common/src/defaults.ts
Normal file
32
packages/media-understanding-common/src/defaults.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { MediaUnderstandingCapability } from "./types.js";
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
export const DEFAULT_MAX_CHARS = 500;
|
||||
export const DEFAULT_MAX_CHARS_BY_CAPABILITY: Record<
|
||||
MediaUnderstandingCapability,
|
||||
number | undefined
|
||||
> = {
|
||||
image: DEFAULT_MAX_CHARS,
|
||||
audio: undefined,
|
||||
video: DEFAULT_MAX_CHARS,
|
||||
};
|
||||
export const DEFAULT_MAX_BYTES: Record<MediaUnderstandingCapability, number> = {
|
||||
image: 10 * MB,
|
||||
audio: 20 * MB,
|
||||
video: 50 * MB,
|
||||
};
|
||||
export const DEFAULT_TIMEOUT_SECONDS: Record<MediaUnderstandingCapability, number> = {
|
||||
image: 60,
|
||||
audio: 60,
|
||||
video: 120,
|
||||
};
|
||||
export const DEFAULT_PROMPT: Record<MediaUnderstandingCapability, string> = {
|
||||
image: "Describe the image.",
|
||||
audio: "Transcribe the audio.",
|
||||
video: "Describe the video.",
|
||||
};
|
||||
export const DEFAULT_VIDEO_MAX_BASE64_BYTES = 70 * MB;
|
||||
export const CLI_OUTPUT_MAX_BUFFER = 5 * MB;
|
||||
export const DEFAULT_MEDIA_CONCURRENCY = 2;
|
||||
export const MIN_AUDIO_FILE_BYTES = 1024;
|
||||
21
packages/media-understanding-common/src/errors.ts
Normal file
21
packages/media-understanding-common/src/errors.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
type MediaUnderstandingSkipReason =
|
||||
| "maxBytes"
|
||||
| "timeout"
|
||||
| "unsupported"
|
||||
| "empty"
|
||||
| "blocked"
|
||||
| "tooSmall";
|
||||
|
||||
export class MediaUnderstandingSkipError extends Error {
|
||||
readonly reason: MediaUnderstandingSkipReason;
|
||||
|
||||
constructor(reason: MediaUnderstandingSkipReason, message: string) {
|
||||
super(message);
|
||||
this.reason = reason;
|
||||
this.name = "MediaUnderstandingSkipError";
|
||||
}
|
||||
}
|
||||
|
||||
export function isMediaUnderstandingSkipError(err: unknown): err is MediaUnderstandingSkipError {
|
||||
return err instanceof MediaUnderstandingSkipError;
|
||||
}
|
||||
98
packages/media-understanding-common/src/format.ts
Normal file
98
packages/media-understanding-common/src/format.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { MediaUnderstandingOutput } from "./types.js";
|
||||
|
||||
const MEDIA_PLACEHOLDER_RE = /^<media:[^>]+>(\s*\([^)]*\))?$/i;
|
||||
const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
|
||||
|
||||
export function extractMediaUserText(body?: string): string | undefined {
|
||||
const trimmed = body?.trim() ?? "";
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
const cleaned = trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim();
|
||||
return cleaned || undefined;
|
||||
}
|
||||
|
||||
function formatSection(
|
||||
title: string,
|
||||
kind: "Transcript" | "Description",
|
||||
text: string,
|
||||
userText?: string,
|
||||
): string {
|
||||
const lines = [`[${title}]`];
|
||||
if (userText) {
|
||||
lines.push(`User text:\n${userText}`);
|
||||
}
|
||||
lines.push(`${kind}:\n${text}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function formatMediaUnderstandingBody(params: {
|
||||
body?: string;
|
||||
outputs: MediaUnderstandingOutput[];
|
||||
}): string {
|
||||
const outputs = params.outputs.filter((output) => output.text.trim());
|
||||
if (outputs.length === 0) {
|
||||
return params.body ?? "";
|
||||
}
|
||||
|
||||
const userText = extractMediaUserText(params.body);
|
||||
const sections: string[] = [];
|
||||
if (userText && outputs.length > 1) {
|
||||
sections.push(`User text:\n${userText}`);
|
||||
}
|
||||
|
||||
const counts = new Map<MediaUnderstandingOutput["kind"], number>();
|
||||
for (const output of outputs) {
|
||||
counts.set(output.kind, (counts.get(output.kind) ?? 0) + 1);
|
||||
}
|
||||
const seen = new Map<MediaUnderstandingOutput["kind"], number>();
|
||||
|
||||
for (const output of outputs) {
|
||||
const count = counts.get(output.kind) ?? 1;
|
||||
const next = (seen.get(output.kind) ?? 0) + 1;
|
||||
seen.set(output.kind, next);
|
||||
const suffix = count > 1 ? ` ${next}/${count}` : "";
|
||||
if (output.kind === "audio.transcription") {
|
||||
sections.push(
|
||||
formatSection(
|
||||
`Audio${suffix}`,
|
||||
"Transcript",
|
||||
output.text,
|
||||
outputs.length === 1 ? userText : undefined,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (output.kind === "image.description") {
|
||||
sections.push(
|
||||
formatSection(
|
||||
`Image${suffix}`,
|
||||
"Description",
|
||||
output.text,
|
||||
outputs.length === 1 ? userText : undefined,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
sections.push(
|
||||
formatSection(
|
||||
`Video${suffix}`,
|
||||
"Description",
|
||||
output.text,
|
||||
outputs.length === 1 ? userText : undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return sections.join("\n\n").trim();
|
||||
}
|
||||
|
||||
export function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string {
|
||||
if (outputs.length === 1) {
|
||||
return outputs[0].text;
|
||||
}
|
||||
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
|
||||
}
|
||||
10
packages/media-understanding-common/src/index.ts
Normal file
10
packages/media-understanding-common/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from "./active-model.js";
|
||||
export * from "./defaults.js";
|
||||
export * from "./errors.js";
|
||||
export * from "./format.js";
|
||||
export * from "./openai-compatible-video.js";
|
||||
export * from "./output-extract.js";
|
||||
export * from "./provider-id.js";
|
||||
export * from "./provider-supports.js";
|
||||
export * from "./types.js";
|
||||
export * from "./video.js";
|
||||
@@ -0,0 +1,66 @@
|
||||
export type OpenAiCompatibleVideoPayload = {
|
||||
choices?: Array<{
|
||||
message?: {
|
||||
content?: string | Array<{ text?: string }>;
|
||||
reasoning_content?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export function resolveMediaUnderstandingString(
|
||||
value: string | undefined,
|
||||
fallback: string,
|
||||
): string {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed || fallback;
|
||||
}
|
||||
|
||||
export function coerceOpenAiCompatibleVideoText(
|
||||
payload: OpenAiCompatibleVideoPayload,
|
||||
): string | null {
|
||||
const message = payload.choices?.[0]?.message;
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
if (typeof message.content === "string" && message.content.trim()) {
|
||||
return message.content.trim();
|
||||
}
|
||||
if (Array.isArray(message.content)) {
|
||||
const text = message.content
|
||||
.map((part) => part.text?.trim() ?? "")
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) {
|
||||
return message.reasoning_content.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildOpenAiCompatibleVideoRequestBody(params: {
|
||||
model: string;
|
||||
prompt: string;
|
||||
mime: string;
|
||||
buffer: Buffer;
|
||||
}) {
|
||||
return {
|
||||
model: params.model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: params.prompt },
|
||||
{
|
||||
type: "video_url",
|
||||
video_url: {
|
||||
url: `data:${params.mime};base64,${params.buffer.toString("base64")}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
26
packages/media-understanding-common/src/output-extract.ts
Normal file
26
packages/media-understanding-common/src/output-extract.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
function extractLastJsonObject(raw: string): unknown {
|
||||
const trimmed = raw.trim();
|
||||
const start = trimmed.lastIndexOf("{");
|
||||
if (start === -1) {
|
||||
return null;
|
||||
}
|
||||
const slice = trimmed.slice(start);
|
||||
try {
|
||||
return JSON.parse(slice);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractGeminiResponse(raw: string): string | null {
|
||||
const payload = extractLastJsonObject(raw);
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return null;
|
||||
}
|
||||
const response = (payload as { response?: unknown }).response;
|
||||
if (typeof response !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = response.trim();
|
||||
return trimmed || null;
|
||||
}
|
||||
25
packages/media-understanding-common/src/provider-id.ts
Normal file
25
packages/media-understanding-common/src/provider-id.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
function normalizeProviderId(provider: string): string {
|
||||
return provider.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function normalizeMediaProviderId(id: string): string {
|
||||
const normalized = normalizeProviderId(id);
|
||||
if (normalized === "gemini") {
|
||||
return "google";
|
||||
}
|
||||
if (normalized === "minimax-cn") {
|
||||
return "minimax";
|
||||
}
|
||||
if (normalized === "minimax-portal-cn") {
|
||||
return "minimax-portal";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function normalizeMediaExecutionProviderId(id: string): string {
|
||||
const normalized = normalizeProviderId(id);
|
||||
if (normalized === "minimax-cn" || normalized === "minimax-portal-cn") {
|
||||
return normalized;
|
||||
}
|
||||
return normalizeMediaProviderId(normalized);
|
||||
}
|
||||
17
packages/media-understanding-common/src/provider-supports.ts
Normal file
17
packages/media-understanding-common/src/provider-supports.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { MediaUnderstandingCapability, MediaUnderstandingProvider } from "./types.js";
|
||||
|
||||
export function providerSupportsCapability(
|
||||
provider: MediaUnderstandingProvider | undefined,
|
||||
capability: MediaUnderstandingCapability,
|
||||
): boolean {
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
if (capability === "audio") {
|
||||
return Boolean(provider.transcribeAudio);
|
||||
}
|
||||
if (capability === "image") {
|
||||
return Boolean(provider.describeImage);
|
||||
}
|
||||
return Boolean(provider.describeVideo);
|
||||
}
|
||||
39
packages/media-understanding-common/src/types.ts
Normal file
39
packages/media-understanding-common/src/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export type MediaUnderstandingKind =
|
||||
| "audio.transcription"
|
||||
| "video.description"
|
||||
| "image.description";
|
||||
|
||||
export type MediaUnderstandingCapability = "image" | "audio" | "video";
|
||||
|
||||
export type MediaUnderstandingCapabilityRegistry = Map<
|
||||
string,
|
||||
{
|
||||
capabilities?: MediaUnderstandingCapability[];
|
||||
}
|
||||
>;
|
||||
|
||||
export type MediaAttachment = {
|
||||
path?: string;
|
||||
url?: string;
|
||||
mime?: string;
|
||||
index: number;
|
||||
alreadyTranscribed?: boolean;
|
||||
};
|
||||
|
||||
export type MediaUnderstandingOutput = {
|
||||
kind: MediaUnderstandingKind;
|
||||
attachmentIndex: number;
|
||||
text: string;
|
||||
provider: string;
|
||||
model?: string;
|
||||
};
|
||||
|
||||
export type MediaUnderstandingProvider = {
|
||||
id: string;
|
||||
capabilities?: MediaUnderstandingCapability[];
|
||||
transcribeAudio?: unknown;
|
||||
describeVideo?: unknown;
|
||||
describeImage?: unknown;
|
||||
describeImages?: unknown;
|
||||
extractStructured?: unknown;
|
||||
};
|
||||
10
packages/media-understanding-common/src/video.ts
Normal file
10
packages/media-understanding-common/src/video.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.js";
|
||||
|
||||
export function estimateBase64Size(bytes: number): number {
|
||||
return Math.ceil(bytes / 3) * 4;
|
||||
}
|
||||
|
||||
export function resolveVideoMaxBase64Bytes(maxBytes: number): number {
|
||||
const expanded = Math.floor(maxBytes * (4 / 3));
|
||||
return Math.min(expanded, DEFAULT_VIDEO_MAX_BASE64_BYTES);
|
||||
}
|
||||
302
pnpm-lock.yaml
generated
302
pnpm-lock.yaml
generated
@@ -304,9 +304,6 @@ importers:
|
||||
specifier: 4.1.7
|
||||
version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/browser-playwright@4.1.7)(@vitest/coverage-v8@4.1.7)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.3)(yaml@2.9.0))
|
||||
optionalDependencies:
|
||||
sharp:
|
||||
specifier: 0.34.5
|
||||
version: 0.34.5
|
||||
sqlite-vec:
|
||||
specifier: 0.1.9
|
||||
version: 0.1.9
|
||||
@@ -1694,7 +1691,7 @@ importers:
|
||||
version: 2.2.3
|
||||
baileys:
|
||||
specifier: 7.0.0-rc13
|
||||
version: 7.0.0-rc13(audio-decode@2.2.3)(sharp@0.34.5)
|
||||
version: 7.0.0-rc13(audio-decode@2.2.3)
|
||||
typebox:
|
||||
specifier: 1.1.38
|
||||
version: 1.1.38
|
||||
@@ -1791,6 +1788,12 @@ importers:
|
||||
specifier: 2.9.0
|
||||
version: 2.9.0
|
||||
|
||||
packages/code-mode-runtime:
|
||||
dependencies:
|
||||
quickjs-wasi:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
|
||||
packages/gateway-client:
|
||||
dependencies:
|
||||
'@openclaw/gateway-protocol':
|
||||
@@ -1832,6 +1835,8 @@ importers:
|
||||
|
||||
packages/media-generation-core: {}
|
||||
|
||||
packages/media-understanding-common: {}
|
||||
|
||||
packages/memory-host-sdk: {}
|
||||
|
||||
packages/net-policy:
|
||||
@@ -2734,159 +2739,6 @@ packages:
|
||||
peerDependencies:
|
||||
hono: 4.12.18
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -6598,10 +6450,6 @@ packages:
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -8358,103 +8206,6 @@ snapshots:
|
||||
dependencies:
|
||||
hono: 4.12.18
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.10.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
dependencies:
|
||||
minipass: 7.1.3
|
||||
@@ -10105,7 +9856,7 @@ snapshots:
|
||||
|
||||
bail@2.0.2: {}
|
||||
|
||||
baileys@7.0.0-rc13(audio-decode@2.2.3)(sharp@0.34.5):
|
||||
baileys@7.0.0-rc13(audio-decode@2.2.3):
|
||||
dependencies:
|
||||
'@cacheable/node-cache': 1.7.6
|
||||
'@hapi/boom': 9.1.4
|
||||
@@ -10120,7 +9871,6 @@ snapshots:
|
||||
ws: 8.21.0
|
||||
optionalDependencies:
|
||||
audio-decode: 2.2.3
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
@@ -12556,38 +12306,6 @@ snapshots:
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.0
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
optional: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
@@ -106,7 +106,6 @@ allowBuilds:
|
||||
koffi: false
|
||||
node-llama-cpp: true
|
||||
protobufjs: true
|
||||
sharp: true
|
||||
tree-sitter-bash: false
|
||||
openclaw: true
|
||||
"@openclaw/proxyline": true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user