mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
6 Commits
codex/pr-8
...
v2026.4.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd13188f79 | ||
|
|
c2f6ad3876 | ||
|
|
bca479cefb | ||
|
|
6d082070a2 | ||
|
|
86f65ef1e3 | ||
|
|
cdd91edd5e |
@@ -97,6 +97,11 @@ Use this skill for release and publish-time workflow. Keep ordinary development
|
||||
|
||||
## Build changelog-backed release notes
|
||||
|
||||
- Before release branching or tagging, rewrite the target `CHANGELOG.md`
|
||||
section from commit history, not just from existing notes: scan commits since
|
||||
the last reachable release tag, add missed user-facing changes, dedupe
|
||||
overlapping entries, and sort each section from most to least interesting for
|
||||
users.
|
||||
- Changelog entries should be user-facing, not internal release-process notes.
|
||||
- GitHub release and prerelease bodies must use the full matching
|
||||
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
|
||||
|
||||
2805
CHANGELOG.md
2805
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
56ccee3ef8ff3b0ba7e2e765ae631b59254464585d5fef9db7e905f2c4c34ded plugin-sdk-api-baseline.json
|
||||
39184cf8afaec691f0352d1a113e30a7099b87c0748237a3c7307e903ba24eee plugin-sdk-api-baseline.jsonl
|
||||
97bbfec2665d3ea7b100eba09b1ebe3c2a678b79d3badfa5537cbe10ffb8c087 plugin-sdk-api-baseline.json
|
||||
b9dc740d631641121696f5348a6cf34b82f7a7a3afae7ccbb0333fa0812bc61c plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
|
||||
@@ -35,6 +35,7 @@ import { DEFAULT_UPLOAD_DIR } from "./paths.js";
|
||||
|
||||
export {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
|
||||
beforeEach(async () => {
|
||||
const page = {
|
||||
on: vi.fn(),
|
||||
goto,
|
||||
title: pageTitle,
|
||||
url: pageUrl,
|
||||
|
||||
@@ -5,6 +5,12 @@ type QaWebSession = {
|
||||
browser: Browser;
|
||||
context: BrowserContext;
|
||||
page: Page;
|
||||
diagnostics: QaWebDiagnosticEntry[];
|
||||
};
|
||||
|
||||
type QaWebDiagnosticEntry = {
|
||||
kind: "console" | "pageerror" | "requestfailed";
|
||||
text: string;
|
||||
};
|
||||
|
||||
type QaWebOpenPageParams = {
|
||||
@@ -44,6 +50,18 @@ type QaWebEvaluateParams = {
|
||||
|
||||
const sessions = new Map<string, QaWebSession>();
|
||||
const DEFAULT_WEB_TIMEOUT_MS = 20_000;
|
||||
const MAX_DIAGNOSTIC_ENTRIES = 50;
|
||||
const MAX_DIAGNOSTIC_TEXT_CHARS = 2_000;
|
||||
|
||||
function appendDiagnostic(diagnostics: QaWebDiagnosticEntry[], entry: QaWebDiagnosticEntry): void {
|
||||
diagnostics.push({
|
||||
kind: entry.kind,
|
||||
text: entry.text.slice(0, MAX_DIAGNOSTIC_TEXT_CHARS),
|
||||
});
|
||||
if (diagnostics.length > MAX_DIAGNOSTIC_ENTRIES) {
|
||||
diagnostics.splice(0, diagnostics.length - MAX_DIAGNOSTIC_ENTRIES);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTimeoutMs(timeoutMs: number | undefined, fallbackMs = DEFAULT_WEB_TIMEOUT_MS) {
|
||||
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
|
||||
@@ -71,12 +89,31 @@ export async function qaWebOpenPage(params: QaWebOpenPageParams) {
|
||||
viewport: params.viewport ?? { width: 1440, height: 1080 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const diagnostics: QaWebDiagnosticEntry[] = [];
|
||||
page.on("console", (message) => {
|
||||
appendDiagnostic(diagnostics, {
|
||||
kind: "console",
|
||||
text: `[${message.type()}] ${message.text()}`,
|
||||
});
|
||||
});
|
||||
page.on("pageerror", (error) => {
|
||||
appendDiagnostic(diagnostics, {
|
||||
kind: "pageerror",
|
||||
text: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
||||
});
|
||||
});
|
||||
page.on("requestfailed", (request) => {
|
||||
appendDiagnostic(diagnostics, {
|
||||
kind: "requestfailed",
|
||||
text: `${request.method()} ${request.url()} ${request.failure()?.errorText ?? "failed"}`,
|
||||
});
|
||||
});
|
||||
await page.goto(params.url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
const pageId = randomUUID();
|
||||
sessions.set(pageId, { browser, context, page });
|
||||
sessions.set(pageId, { browser, context, page, diagnostics });
|
||||
return {
|
||||
pageId,
|
||||
url: page.url(),
|
||||
@@ -128,6 +165,7 @@ export async function qaWebSnapshot(params: QaWebSnapshotParams) {
|
||||
url: session.page.url(),
|
||||
title: await session.page.title().catch(() => ""),
|
||||
text: maxChars ? text.slice(0, maxChars) : text,
|
||||
diagnostics: [...session.diagnostics],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw",
|
||||
"version": "2026.4.24",
|
||||
"version": "2026.4.24-beta.1",
|
||||
"description": "Multi-channel AI gateway with extensible messaging integrations",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/openclaw/openclaw#readme",
|
||||
@@ -1129,14 +1129,14 @@
|
||||
"types": "./dist/plugin-sdk/provider-usage.d.ts",
|
||||
"default": "./dist/plugin-sdk/provider-usage.js"
|
||||
},
|
||||
"./plugin-sdk/web-content-extractor": {
|
||||
"types": "./dist/plugin-sdk/web-content-extractor.d.ts",
|
||||
"default": "./dist/plugin-sdk/web-content-extractor.js"
|
||||
},
|
||||
"./plugin-sdk/document-extractor": {
|
||||
"types": "./dist/plugin-sdk/document-extractor.d.ts",
|
||||
"default": "./dist/plugin-sdk/document-extractor.js"
|
||||
},
|
||||
"./plugin-sdk/web-content-extractor": {
|
||||
"types": "./dist/plugin-sdk/web-content-extractor.d.ts",
|
||||
"default": "./dist/plugin-sdk/web-content-extractor.js"
|
||||
},
|
||||
"./plugin-sdk/provider-web-fetch-contract": {
|
||||
"types": "./dist/plugin-sdk/provider-web-fetch-contract.d.ts",
|
||||
"default": "./dist/plugin-sdk/provider-web-fetch-contract.js"
|
||||
|
||||
@@ -65,7 +65,7 @@ steps:
|
||||
expr: "buildAgentSessionKey({ agentId: env.cfg.agents?.list?.find((agent) => agent.default)?.id ?? env.cfg.agents?.list?.[0]?.id ?? 'main', channel: 'qa-channel', accountId: 'default', peer: { kind: 'direct', id: config.conversationId }, dmScope: env.cfg.session?.dmScope, identityLinks: env.cfg.session?.identityLinks })"
|
||||
- set: controlUiChatUrl
|
||||
value:
|
||||
expr: "(() => { const url = new URL(String(bootstrap.controlUiEmbeddedUrl)); url.pathname = `${url.pathname.replace(/\\/$/, '')}/chat`; url.searchParams.set('session', uiSessionKey); return url.toString(); })()"
|
||||
expr: "(() => { const url = new URL(`${env.gateway.baseUrl}/`); url.searchParams.set('session', uiSessionKey); url.hash = `token=${encodeURIComponent(env.gateway.token ?? '')}`; return url.toString(); })()"
|
||||
- call: webOpenPage
|
||||
saveAs: uiTab
|
||||
args:
|
||||
@@ -80,17 +80,38 @@ steps:
|
||||
args:
|
||||
- pageId:
|
||||
ref: uiPageId
|
||||
selector: textarea
|
||||
selector: openclaw-app
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- call: waitForCondition
|
||||
saveAs: uiReadySnapshot
|
||||
args:
|
||||
- lambda:
|
||||
async: true
|
||||
expr: "await (async () => { const snapshot = await webSnapshot({ pageId: uiPageId, maxChars: 12000, timeoutMs: liveTurnTimeoutMs(env, 30000) }); const text = normalizeLowercaseStringOrEmpty(snapshot.text); return text.includes('ready to chat') ? snapshot : undefined; })()"
|
||||
- expr: liveTurnTimeoutMs(env, 45000)
|
||||
- 500
|
||||
- try:
|
||||
actions:
|
||||
- call: waitForCondition
|
||||
saveAs: uiReadySnapshot
|
||||
args:
|
||||
- lambda:
|
||||
async: true
|
||||
expr: "await (async () => { const snapshot = await webSnapshot({ pageId: uiPageId, maxChars: 12000, timeoutMs: liveTurnTimeoutMs(env, 30000) }); const text = normalizeLowercaseStringOrEmpty(snapshot.text); return text.includes('ready to chat') ? snapshot : undefined; })()"
|
||||
- expr: liveTurnTimeoutMs(env, 45000)
|
||||
- 500
|
||||
catch:
|
||||
- call: webSnapshot
|
||||
saveAs: uiReadyFailureSnapshot
|
||||
args:
|
||||
- pageId:
|
||||
ref: uiPageId
|
||||
maxChars: 12000
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 15000)
|
||||
- call: webEvaluate
|
||||
saveAs: uiReadyFailureState
|
||||
args:
|
||||
- pageId:
|
||||
ref: uiPageId
|
||||
expression: "(() => { const app = document.querySelector('openclaw-app'); const resources = performance.getEntriesByType('resource').map((entry) => ({ name: entry.name, type: entry.initiatorType, duration: Math.round(entry.duration), transferSize: entry.transferSize, decodedBodySize: entry.decodedBodySize })); return { url: location.href, readyState: document.readyState, appDefined: Boolean(customElements.get('openclaw-app')), appState: app ? { sessionKey: app.sessionKey, settingsSessionKey: app.settings?.sessionKey, lastActiveSessionKey: app.settings?.lastActiveSessionKey, chatMessages: Array.isArray(app.chatMessages) ? app.chatMessages.length : null, chatLoading: app.chatLoading, lastError: app.lastError, connected: app.connected, tab: app.tab } : null, scripts: Array.from(document.scripts).map((script) => script.src || script.textContent?.slice(0, 80)), links: Array.from(document.querySelectorAll('link')).map((link) => link.href), resources, bodyHtml: document.body.innerHTML.slice(0, 400) }; })()"
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 15000)
|
||||
- throw:
|
||||
expr: "`control ui did not become ready. state=${JSON.stringify(uiReadyFailureState)} diagnostics=${JSON.stringify(uiReadyFailureSnapshot.diagnostics ?? [])} snapshot: ${uiReadyFailureSnapshot.text}`"
|
||||
- assert:
|
||||
expr: "Boolean(uiPageId)"
|
||||
message: control ui page was not available
|
||||
@@ -149,7 +170,7 @@ steps:
|
||||
args:
|
||||
- pageId:
|
||||
ref: uiAckPageId
|
||||
selector: textarea
|
||||
selector: openclaw-app
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- try:
|
||||
@@ -240,7 +261,7 @@ steps:
|
||||
args:
|
||||
- pageId:
|
||||
ref: uiImagePageId
|
||||
selector: textarea
|
||||
selector: openclaw-app
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- try:
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
realpathSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
@@ -17,6 +18,10 @@ import {
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "../src/infra/package-dist-inventory.ts";
|
||||
import {
|
||||
resolveBundledRuntimeDependencyInstallRoot,
|
||||
resolveBundledRuntimeDependencyPackageInstallRoot,
|
||||
} from "../src/plugins/bundled-runtime-deps.ts";
|
||||
import {
|
||||
collectBundledExtensionManifestErrors,
|
||||
type BundledExtension,
|
||||
@@ -317,28 +322,48 @@ function bundledRuntimeDependencySentinelPath(
|
||||
);
|
||||
}
|
||||
|
||||
function bundledRuntimeDependencySentinelCandidates(
|
||||
export function bundledRuntimeDependencySentinelCandidates(
|
||||
packageRoot: string,
|
||||
pluginId: string,
|
||||
dependencyName: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const dependencyParts = dependencyName.split("/");
|
||||
const packageRoots = [
|
||||
packageRoot,
|
||||
(() => {
|
||||
try {
|
||||
return realpathSync(packageRoot);
|
||||
} catch {
|
||||
return packageRoot;
|
||||
}
|
||||
})(),
|
||||
];
|
||||
const runtimeRoots = packageRoots.flatMap((root) => [
|
||||
resolveBundledRuntimeDependencyPackageInstallRoot(root, { env }),
|
||||
resolveBundledRuntimeDependencyInstallRoot(join(root, "dist", "extensions", pluginId), {
|
||||
env,
|
||||
}),
|
||||
]);
|
||||
return [
|
||||
bundledRuntimeDependencySentinelPath(packageRoot, pluginId, dependencyName),
|
||||
join(packageRoot, "dist", "extensions", "node_modules", ...dependencyParts, "package.json"),
|
||||
join(packageRoot, "node_modules", ...dependencyParts, "package.json"),
|
||||
];
|
||||
...runtimeRoots.map((root) => join(root, "node_modules", ...dependencyParts, "package.json")),
|
||||
].filter((candidate, index, candidates) => candidates.indexOf(candidate) === index);
|
||||
}
|
||||
|
||||
function assertBundledRuntimeDependencyAbsent(params: {
|
||||
packageRoot: string;
|
||||
pluginId: string;
|
||||
dependencyName: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
const sentinelPath = bundledRuntimeDependencySentinelCandidates(
|
||||
params.packageRoot,
|
||||
params.pluginId,
|
||||
params.dependencyName,
|
||||
params.env,
|
||||
).find((candidate) => existsSync(candidate));
|
||||
if (sentinelPath) {
|
||||
throw new Error(
|
||||
@@ -351,11 +376,13 @@ function assertBundledRuntimeDependencyPresent(params: {
|
||||
packageRoot: string;
|
||||
pluginId: string;
|
||||
dependencyName: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
const sentinelPath = bundledRuntimeDependencySentinelCandidates(
|
||||
params.packageRoot,
|
||||
params.pluginId,
|
||||
params.dependencyName,
|
||||
params.env,
|
||||
).find((candidate) => existsSync(candidate));
|
||||
if (sentinelPath) {
|
||||
return;
|
||||
@@ -413,24 +440,25 @@ function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: str
|
||||
{ pluginId: "feishu", dependencyName: "@larksuiteoapi/node-sdk" },
|
||||
] as const;
|
||||
|
||||
for (const dep of lazyDeps) {
|
||||
assertBundledRuntimeDependencyAbsent({ packageRoot, ...dep });
|
||||
}
|
||||
|
||||
const homeDir = join(tmpRoot, "activation-home");
|
||||
mkdirSync(homeDir, { recursive: true });
|
||||
const env = createPackedCliSmokeEnv(process.env, {
|
||||
HOME: homeDir,
|
||||
OPENAI_API_KEY: "sk-openclaw-release-check",
|
||||
});
|
||||
for (const dep of lazyDeps) {
|
||||
assertBundledRuntimeDependencyAbsent({ packageRoot, env, ...dep });
|
||||
}
|
||||
|
||||
writePackedBundledPluginActivationConfig(homeDir);
|
||||
execFileSync(process.execPath, [join(packageRoot, "openclaw.mjs"), "plugins", "doctor"], {
|
||||
cwd: packageRoot,
|
||||
stdio: "inherit",
|
||||
env: createPackedCliSmokeEnv(process.env, {
|
||||
HOME: homeDir,
|
||||
OPENAI_API_KEY: "sk-openclaw-release-check",
|
||||
}),
|
||||
env,
|
||||
});
|
||||
|
||||
for (const dep of lazyDeps) {
|
||||
assertBundledRuntimeDependencyPresent({ packageRoot, ...dep });
|
||||
assertBundledRuntimeDependencyPresent({ packageRoot, env, ...dep });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27959,6 +27959,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
tags: ["advanced", "url-secret"],
|
||||
},
|
||||
},
|
||||
version: "2026.4.24",
|
||||
version: "2026.4.24-beta.1",
|
||||
generatedAt: "2026-03-22T21:17:33.302Z",
|
||||
};
|
||||
|
||||
@@ -128,6 +128,7 @@ const BROWSER_HELPER_EXPORT_PARITY_CONTRACTS: readonly BrowserHelperExportParity
|
||||
extensionPath: "extensions/browser/browser-profiles.ts",
|
||||
expectedExports: [
|
||||
"DEFAULT_AI_SNAPSHOT_MAX_CHARS",
|
||||
"DEFAULT_BROWSER_ACTION_TIMEOUT_MS",
|
||||
"DEFAULT_BROWSER_DEFAULT_PROFILE_NAME",
|
||||
"DEFAULT_BROWSER_EVALUATE_ENABLED",
|
||||
"DEFAULT_OPENCLAW_BROWSER_COLOR",
|
||||
|
||||
@@ -1504,6 +1504,60 @@ module.exports = {
|
||||
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("loads bundled plugins from symlinked package roots with an external stage dir", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
const aliasRoot = path.join(makeTempDir(), "openclaw-alias");
|
||||
const bundledDir = path.join(packageRoot, "dist", "extensions");
|
||||
const plugin = writePlugin({
|
||||
id: "alpha",
|
||||
dir: path.join(bundledDir, "alpha"),
|
||||
filename: "index.cjs",
|
||||
body: `module.exports = { id: "alpha", register(api) { api.registerCommand({ name: "alpha", handler: () => "ok" }); } };`,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.4.24", type: "module" }),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/alpha",
|
||||
version: "1.0.0",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "alpha",
|
||||
enabledByDefault: true,
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.symlinkSync(packageRoot, aliasRoot, "dir");
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(aliasRoot, "dist", "extensions");
|
||||
process.env.OPENCLAW_PLUGIN_STAGE_DIR = stageDir;
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: { plugins: { enabled: true } },
|
||||
});
|
||||
|
||||
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("loads bundled plugins with plugin-sdk imports from an external stage dir", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
|
||||
@@ -2276,8 +2276,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
};
|
||||
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
|
||||
let runtimePluginRoot = pluginRoot;
|
||||
let runtimeCandidateSource = candidate.source;
|
||||
let runtimeSetupSource = manifestRecord.setupSource;
|
||||
let runtimeCandidateSource =
|
||||
candidate.origin === "bundled" ? safeRealpathOrResolve(candidate.source) : candidate.source;
|
||||
let runtimeSetupSource =
|
||||
candidate.origin === "bundled" && manifestRecord.setupSource
|
||||
? safeRealpathOrResolve(manifestRecord.setupSource)
|
||||
: manifestRecord.setupSource;
|
||||
|
||||
const scopedSetupOnlyChannelPluginRequested =
|
||||
includeSetupOnlyChannelPlugins &&
|
||||
@@ -2381,12 +2385,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
});
|
||||
runtimeCandidateSource =
|
||||
remapBundledPluginRuntimePath({
|
||||
source: candidate.source,
|
||||
source: runtimeCandidateSource,
|
||||
pluginRoot,
|
||||
mirroredRoot: runtimePluginRoot,
|
||||
}) ?? candidate.source;
|
||||
}) ?? runtimeCandidateSource;
|
||||
runtimeSetupSource = remapBundledPluginRuntimePath({
|
||||
source: manifestRecord.setupSource,
|
||||
source: runtimeSetupSource,
|
||||
pluginRoot,
|
||||
mirroredRoot: runtimePluginRoot,
|
||||
});
|
||||
@@ -3186,7 +3190,11 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
|
||||
const cliMetadataSource = resolveCliMetadataEntrySource(candidate.rootDir);
|
||||
const sourceForCliMetadata =
|
||||
candidate.origin === "bundled" ? cliMetadataSource : (cliMetadataSource ?? candidate.source);
|
||||
candidate.origin === "bundled"
|
||||
? cliMetadataSource
|
||||
? safeRealpathOrResolve(cliMetadataSource)
|
||||
: null
|
||||
: (cliMetadataSource ?? candidate.source);
|
||||
if (!sourceForCliMetadata) {
|
||||
record.status = "loaded";
|
||||
registry.plugins.push(record);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
collectBundledPluginRootRuntimeMirrorErrors,
|
||||
collectForbiddenPackContentPaths,
|
||||
collectInstalledBundledPluginRuntimeDepErrors,
|
||||
bundledRuntimeDependencySentinelCandidates,
|
||||
collectRootDistBundledRuntimeMirrors,
|
||||
collectForbiddenPackPaths,
|
||||
collectMissingPackPaths,
|
||||
@@ -673,3 +674,36 @@ describe("collectInstalledBundledPluginRuntimeDepErrors", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("bundledRuntimeDependencySentinelCandidates", () => {
|
||||
it("checks canonical external runtime-deps roots for packed installs", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "release-check-runtime-candidates-"));
|
||||
const packageRoot = join(root, "package");
|
||||
const aliasRoot = join(root, "package-alias");
|
||||
const homeRoot = join(root, "home");
|
||||
try {
|
||||
mkdirSync(join(packageRoot, "dist", "extensions", "browser"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.4.24-beta.1" }, null, 2),
|
||||
);
|
||||
symlinkSync(packageRoot, aliasRoot, "dir");
|
||||
|
||||
const candidates = bundledRuntimeDependencySentinelCandidates(
|
||||
aliasRoot,
|
||||
"browser",
|
||||
"playwright-core",
|
||||
{ HOME: homeRoot } as NodeJS.ProcessEnv,
|
||||
);
|
||||
const externalCandidates = candidates.filter(
|
||||
(candidate) =>
|
||||
candidate.startsWith(join(homeRoot, ".openclaw", "plugin-runtime-deps")) &&
|
||||
candidate.endsWith(join("node_modules", "playwright-core", "package.json")),
|
||||
);
|
||||
|
||||
expect(externalCandidates.length).toBeGreaterThanOrEqual(2);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { applyMergePatch } from "../../../src/config/merge-patch.ts";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
parseAgentSessionKey,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "../../../src/routing/session-key.js";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { getSafeLocalStorage } from "../local-storage.ts";
|
||||
import { refreshChat } from "./app-chat.ts";
|
||||
@@ -120,9 +115,14 @@ import {
|
||||
} from "./controllers/skills.ts";
|
||||
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
|
||||
import { icons } from "./icons.ts";
|
||||
import "./components/dashboard-header.ts";
|
||||
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
|
||||
import "./components/dashboard-header.ts";
|
||||
import { isPluginEnabledInConfigSnapshot } from "./plugin-activation.ts";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
parseAgentSessionKey,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "./session-key.ts";
|
||||
import { isRenderableControlUiAvatarUrl } from "./views/agents-utils.ts";
|
||||
import { agentLogoUrl } from "./views/agents-utils.ts";
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { resolveAgentIdFromSessionKey } from "../../../src/routing/session-key.js";
|
||||
import { i18n, I18nController, isSupportedLocale } from "../i18n/index.ts";
|
||||
import {
|
||||
handleChannelConfigReload as handleChannelConfigReloadInternal,
|
||||
@@ -80,6 +79,7 @@ import type {
|
||||
import { importCustomThemeFromUrl } from "./custom-theme.ts";
|
||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
|
||||
import type { Tab } from "./navigation.ts";
|
||||
import { resolveAgentIdFromSessionKey } from "./session-key.ts";
|
||||
import type { SidebarContent } from "./sidebar-content.ts";
|
||||
import { loadLocalUserIdentity, loadSettings, type UiSettings } from "./storage.ts";
|
||||
import { VALID_THEME_NAMES, type ResolvedTheme, type ThemeMode, type ThemeName } from "./theme.ts";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isHeartbeatOkResponse } from "../../../../src/auto-reply/heartbeat-filter.js";
|
||||
import { resetToolStream } from "../app-tool-stream.ts";
|
||||
import { extractText } from "../chat/message-extract.ts";
|
||||
import { formatConnectError } from "../connect-error.ts";
|
||||
@@ -12,6 +11,8 @@ import {
|
||||
} from "./scope-errors.ts";
|
||||
|
||||
const SILENT_REPLY_PATTERN = /^\s*NO_REPLY\s*$/;
|
||||
const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
|
||||
const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300;
|
||||
const SYNTHETIC_TRANSCRIPT_REPAIR_RESULT =
|
||||
"[openclaw] missing tool result in session history; inserted synthetic error result for transcript repair.";
|
||||
const STARTUP_CHAT_HISTORY_RETRY_TIMEOUT_MS = 60_000;
|
||||
@@ -41,6 +42,97 @@ function shouldApplyChatHistoryResult(
|
||||
function isSilentReplyStream(text: string): boolean {
|
||||
return SILENT_REPLY_PATTERN.test(text);
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function stripHeartbeatTokenForDisplay(
|
||||
raw: string,
|
||||
maxAckChars = DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
): { shouldSkip: boolean } {
|
||||
let text = raw.trim();
|
||||
if (!text) {
|
||||
return { shouldSkip: true };
|
||||
}
|
||||
const strippedMarkup = text
|
||||
.replace(/<[^>]*>/g, " ")
|
||||
.replace(/ /gi, " ")
|
||||
.replace(/^[*`~_]+/, "")
|
||||
.replace(/[*`~_]+$/, "");
|
||||
if (!text.includes(HEARTBEAT_TOKEN) && !strippedMarkup.includes(HEARTBEAT_TOKEN)) {
|
||||
return { shouldSkip: false };
|
||||
}
|
||||
|
||||
const tokenAtEnd = new RegExp(`${escapeRegExp(HEARTBEAT_TOKEN)}[^\\w]{0,4}$`);
|
||||
let changed = true;
|
||||
let didStrip = false;
|
||||
text = strippedMarkup.trim();
|
||||
while (changed) {
|
||||
changed = false;
|
||||
const next = text.trim();
|
||||
if (next.startsWith(HEARTBEAT_TOKEN)) {
|
||||
text = next.slice(HEARTBEAT_TOKEN.length).trimStart();
|
||||
didStrip = true;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if (tokenAtEnd.test(next)) {
|
||||
const index = next.lastIndexOf(HEARTBEAT_TOKEN);
|
||||
const before = next.slice(0, index).trimEnd();
|
||||
const after = next.slice(index + HEARTBEAT_TOKEN.length).trimStart();
|
||||
text = before ? `${before}${after}`.trimEnd() : "";
|
||||
didStrip = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStrip) {
|
||||
return { shouldSkip: false };
|
||||
}
|
||||
return { shouldSkip: !text || text.length <= maxAckChars };
|
||||
}
|
||||
|
||||
function isHeartbeatOkResponse(message: { role: string; content?: unknown }): boolean {
|
||||
if (message.role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
const { text, hasNonTextContent } = resolveMessageText(message.content);
|
||||
if (hasNonTextContent) {
|
||||
return false;
|
||||
}
|
||||
return stripHeartbeatTokenForDisplay(text).shouldSkip;
|
||||
}
|
||||
|
||||
function resolveMessageText(content: unknown): { text: string; hasNonTextContent: boolean } {
|
||||
if (typeof content === "string") {
|
||||
return { text: content, hasNonTextContent: false };
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
return { text: "", hasNonTextContent: content != null };
|
||||
}
|
||||
let hasNonTextContent = false;
|
||||
const text = content
|
||||
.filter((block): block is { type: "text"; text: string } => {
|
||||
if (!block || typeof block !== "object" || !("type" in block)) {
|
||||
hasNonTextContent = true;
|
||||
return false;
|
||||
}
|
||||
if ((block as { type?: unknown }).type !== "text") {
|
||||
hasNonTextContent = true;
|
||||
return false;
|
||||
}
|
||||
if (typeof (block as { text?: unknown }).text !== "string") {
|
||||
hasNonTextContent = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((block) => block.text)
|
||||
.join("");
|
||||
return { text, hasNonTextContent };
|
||||
}
|
||||
|
||||
/** Client-side defense-in-depth: detect assistant messages whose text is purely NO_REPLY. */
|
||||
function isAssistantSilentReply(message: unknown): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
|
||||
Reference in New Issue
Block a user