mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
9 Commits
codex/pr-8
...
v2026.4.22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e96087892e | ||
|
|
aef4fc9178 | ||
|
|
c9bb56998a | ||
|
|
fdfc901e42 | ||
|
|
5cd79da5b1 | ||
|
|
0ec75a6ab4 | ||
|
|
435136de8f | ||
|
|
579f00313b | ||
|
|
bef298d97f |
@@ -25,9 +25,11 @@ Use this skill for release and publish-time workflow. Keep ordinary development
|
||||
- Before release branching, commit any dirty files in coherent groups, push,
|
||||
pull/rebase, then run `/changelog` on `main` and commit/push/pull that
|
||||
changelog rewrite immediately before creating the release branch.
|
||||
- Do not delete or rewrite beta tags after they leave the machine. If a
|
||||
published or pushed beta needs a fix, commit the fix on the release branch and
|
||||
increment to the next `-beta.N`.
|
||||
- Do not delete or rewrite beta tags after npm publish has completed for that
|
||||
exact beta version. If a beta tag was only pushed to GitHub and no npm package
|
||||
was published for it yet, it may be moved/recreated to include late release
|
||||
fixes when the operator approves that. If npm publish already happened, commit
|
||||
the fix on the release branch and increment to the next `-beta.N`.
|
||||
- For a beta release train, run the full pre-npm test roster before publishing
|
||||
each beta. After a beta is published, run the smaller published-install roster
|
||||
focused on install/update/Docker/Parallels. If anything fails, fix it on the
|
||||
|
||||
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Status: show `Fast` in `/status` when fast mode is enabled, including config/default-derived fast mode, and omit it when disabled.
|
||||
- Models/auth: merge provider-owned default-model additions from `openclaw models auth login` instead of replacing `agents.defaults.models`, so re-authenticating an OAuth provider such as OpenAI Codex no longer wipes other providers' aliases and per-model params. Migrations that must rename keys (Anthropic -> Claude CLI) opt in with `replaceDefaultModels`. Fixes #69414. (#70435) Thanks @neeravmakwana.
|
||||
- Media understanding/audio: prefer configured or key-backed STT providers before auto-detected local Whisper CLIs, so installed local transcription tools no longer shadow API providers such as Groq/OpenAI in `tools.media.audio` auto mode. Fixes #68727.
|
||||
- Providers/OpenAI: lock the auth picker wording for OpenAI API key, Codex browser login, and Codex device pairing so the setup choices no longer imply a mixed Codex/API-key auth path. (#67848) Thanks @tmlxrd.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
1d08257f068365d84ea1163baf2bca00484bb2689cd1ad2f80e97d3269b8a318 config-baseline.json
|
||||
ea6681d3c14934385bda669a2b1de6ca0155a914217bb7cd96d894081bf1dce8 config-baseline.json
|
||||
a4e167f169db58d71c385a31fa2b980772f9fee963e70dd9553f63536cae5aed config-baseline.core.json
|
||||
22d7cd6d8279146b2d79c9531a55b80b52a2c99c81338c508104729154fdd02d config-baseline.channel.json
|
||||
8580cad7a65a9dc04a3e8f98b1e9252992aea2dedff16d5483934e4bc2841d57 config-baseline.channel.json
|
||||
5b4d18610693d9c4f3cbac51d011b4eb47b0fb11772ba3d2aa3e3499d474260d config-baseline.plugin.json
|
||||
|
||||
@@ -582,10 +582,10 @@ plugin on older hosts.
|
||||
Exact npm version pinning already lives in `npmSpec`, for example
|
||||
`"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3"`. Pair that with
|
||||
`expectedIntegrity` when you want update flows to fail closed if the fetched
|
||||
npm artifact no longer matches the pinned release. Interactive onboarding only
|
||||
offers npm install choices from trusted catalog metadata when `npmSpec` is an
|
||||
exact version and `expectedIntegrity` is present; otherwise it falls back to a
|
||||
local source or skip.
|
||||
npm artifact no longer matches the pinned release. Interactive onboarding
|
||||
offers trusted registry npm specs, including bare package names and dist-tags.
|
||||
When `expectedIntegrity` is present, install/update flows enforce it; when it
|
||||
is omitted, the registry resolution is recorded without an integrity pin.
|
||||
|
||||
Channel plugins should provide `openclaw.setupEntry` when status, channel list,
|
||||
or SecretRef scans need to identify configured accounts without loading the full
|
||||
|
||||
@@ -162,11 +162,11 @@ Interactive onboarding also uses `openclaw.install` for install-on-demand
|
||||
surfaces. If your plugin exposes provider auth choices or channel setup/catalog
|
||||
metadata before runtime loads, onboarding can show that choice, prompt for npm
|
||||
vs local install, install or enable the plugin, then continue the selected
|
||||
flow. Npm onboarding choices require trusted catalog metadata with an exact
|
||||
`npmSpec` version and `expectedIntegrity`; unpinned package names and dist-tags
|
||||
are not offered for automatic onboarding installs. Keep the "what to show"
|
||||
metadata in `openclaw.plugin.json` and the "how to install it" metadata in
|
||||
`package.json`.
|
||||
flow. Npm onboarding choices require trusted catalog metadata with a registry
|
||||
`npmSpec`; exact versions and `expectedIntegrity` are optional pins. If
|
||||
`expectedIntegrity` is present, install/update flows enforce it. Keep the "what
|
||||
to show" metadata in `openclaw.plugin.json` and the "how to install it"
|
||||
metadata in `package.json`.
|
||||
|
||||
If `minHostVersion` is set, install and manifest-registry loading both enforce
|
||||
it. Older hosts skip the plugin; invalid version strings are rejected.
|
||||
|
||||
@@ -68,6 +68,7 @@ title: "Thinking Levels"
|
||||
- For direct public `anthropic/*` requests, including OAuth-authenticated traffic sent to `api.anthropic.com`, fast mode maps to Anthropic service tiers: `/fast on` sets `service_tier=auto`, `/fast off` sets `service_tier=standard_only`.
|
||||
- For `minimax/*` on the Anthropic-compatible path, `/fast on` (or `params.fastMode: true`) rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
|
||||
- Explicit Anthropic `serviceTier` / `service_tier` model params override the fast-mode default when both are set. OpenClaw still skips Anthropic service-tier injection for non-Anthropic proxy base URLs.
|
||||
- `/status` shows `Fast` only when fast mode is enabled.
|
||||
|
||||
## Verbose directives (/verbose or /v)
|
||||
|
||||
|
||||
@@ -175,15 +175,15 @@ vi.spyOn(conversationRuntimeModule, "recordInboundSession").mockImplementation(
|
||||
recordInboundSession(params) as never) as never,
|
||||
);
|
||||
|
||||
const configRuntimeModule = await import("openclaw/plugin-sdk/config-runtime");
|
||||
vi.spyOn(configRuntimeModule, "readSessionUpdatedAt").mockImplementation(
|
||||
((params: Parameters<typeof configRuntimeModule.readSessionUpdatedAt>[0]) =>
|
||||
const sessionStoreRuntimeModule = await import("openclaw/plugin-sdk/session-store-runtime");
|
||||
vi.spyOn(sessionStoreRuntimeModule, "readSessionUpdatedAt").mockImplementation(
|
||||
((params: Parameters<typeof sessionStoreRuntimeModule.readSessionUpdatedAt>[0]) =>
|
||||
configSessionsMocks.readSessionUpdatedAt(params) as never) as never,
|
||||
);
|
||||
vi.spyOn(configRuntimeModule, "resolveStorePath").mockImplementation(
|
||||
vi.spyOn(sessionStoreRuntimeModule, "resolveStorePath").mockImplementation(
|
||||
((
|
||||
path: Parameters<typeof configRuntimeModule.resolveStorePath>[0],
|
||||
opts: Parameters<typeof configRuntimeModule.resolveStorePath>[1],
|
||||
path: Parameters<typeof sessionStoreRuntimeModule.resolveStorePath>[0],
|
||||
opts: Parameters<typeof sessionStoreRuntimeModule.resolveStorePath>[1],
|
||||
) => configSessionsMocks.resolveStorePath(path, opts) as never) as never,
|
||||
);
|
||||
|
||||
|
||||
@@ -21,14 +21,10 @@ import {
|
||||
resolveChannelStreamingBlockEnabled,
|
||||
resolveChannelStreamingPreviewToolProgress,
|
||||
} from "openclaw/plugin-sdk/channel-streaming";
|
||||
import {
|
||||
isDangerousNameMatchingEnabled,
|
||||
readSessionUpdatedAt,
|
||||
resolveChannelContextVisibilityMode,
|
||||
resolveMarkdownTableMode,
|
||||
resolveStorePath,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
|
||||
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
||||
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { resolveChunkMode } from "openclaw/plugin-sdk/reply-chunking";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
||||
@@ -41,6 +37,7 @@ import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-pay
|
||||
import { buildAgentSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { evaluateSupplementalContextVisibility } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { readSessionUpdatedAt, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
convertMarkdownTables,
|
||||
stripInlineDirectiveTagsForDelivery,
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
createChannelInboundDebouncer,
|
||||
shouldDebounceTextInbound,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import {
|
||||
buildDiscordInboundReplayKey,
|
||||
claimDiscordInboundReplay,
|
||||
|
||||
@@ -2,12 +2,12 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { ChannelType } from "discord-api-types/v10";
|
||||
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
} from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
|
||||
@@ -25,9 +25,3 @@ export function registerDiscordSubagentHooks(api: OpenClawPluginApi): void {
|
||||
return handleDiscordSubagentDeliveryTarget(event);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
handleDiscordSubagentDeliveryTarget,
|
||||
handleDiscordSubagentEnded,
|
||||
handleDiscordSubagentSpawning,
|
||||
} from "./src/subagent-hooks.js";
|
||||
|
||||
@@ -7,10 +7,11 @@ import type { VoiceCallProvider } from "../providers/base.js";
|
||||
import type { HangupCallInput, NormalizedEvent } from "../types.js";
|
||||
import type { CallManagerContext } from "./context.js";
|
||||
import { processEvent } from "./events.js";
|
||||
import { flushPendingCallRecordWritesForTest } from "./store.js";
|
||||
|
||||
const contexts: CallManagerContext[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
for (const ctx of contexts.splice(0)) {
|
||||
for (const timer of ctx.maxDurationTimers.values()) {
|
||||
clearTimeout(timer);
|
||||
@@ -20,6 +21,7 @@ afterEach(() => {
|
||||
clearTimeout(waiter.timeout);
|
||||
}
|
||||
ctx.transcriptWaiters.clear();
|
||||
await flushPendingCallRecordWritesForTest();
|
||||
fs.rmSync(ctx.storePath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,13 +3,25 @@ import fsp from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { CallRecordSchema, TerminalStates, type CallId, type CallRecord } from "../types.js";
|
||||
|
||||
const pendingPersistWrites = new Set<Promise<void>>();
|
||||
|
||||
export function persistCallRecord(storePath: string, call: CallRecord): void {
|
||||
const logPath = path.join(storePath, "calls.jsonl");
|
||||
const line = `${JSON.stringify(call)}\n`;
|
||||
// Fire-and-forget async write to avoid blocking event loop.
|
||||
fsp.appendFile(logPath, line).catch((err) => {
|
||||
console.error("[voice-call] Failed to persist call record:", err);
|
||||
});
|
||||
const write = fsp
|
||||
.appendFile(logPath, line)
|
||||
.catch((err) => {
|
||||
console.error("[voice-call] Failed to persist call record:", err);
|
||||
})
|
||||
.finally(() => {
|
||||
pendingPersistWrites.delete(write);
|
||||
});
|
||||
pendingPersistWrites.add(write);
|
||||
}
|
||||
|
||||
export async function flushPendingCallRecordWritesForTest(): Promise<void> {
|
||||
await Promise.allSettled(pendingPersistWrites);
|
||||
}
|
||||
|
||||
export function loadActiveCallsFromStore(storePath: string): {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw",
|
||||
"version": "2026.4.22",
|
||||
"version": "2026.4.22-beta.1",
|
||||
"description": "Multi-channel AI gateway with extensible messaging integrations",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/openclaw/openclaw#readme",
|
||||
|
||||
@@ -53,7 +53,7 @@ prepare_package_tgz() {
|
||||
prepare_package_tgz
|
||||
|
||||
DOCKER_PACKAGE_TGZ="/tmp/openclaw-current.tgz"
|
||||
run_log="$(mktemp "${TMPDIR:-/tmp}/openclaw-npm-onboard-channel-agent.XXXXXX.log")"
|
||||
run_log="$(mktemp "${TMPDIR:-/tmp}/openclaw-npm-onboard-channel-agent.XXXXXX")"
|
||||
|
||||
echo "Running npm tarball onboard/channel/agent Docker E2E ($CHANNEL)..."
|
||||
if ! docker run --rm \
|
||||
|
||||
@@ -16,7 +16,7 @@ if [[ -n "${OPENAI_BASE_URL:-}" && "${OPENAI_BASE_URL:-}" != "undefined" && "${O
|
||||
fi
|
||||
|
||||
echo "Running plugins Docker E2E..."
|
||||
RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-plugins-run.XXXXXX.log")"
|
||||
RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-plugins-run.XXXXXX")"
|
||||
if ! docker run --rm "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" bash -s >"$RUN_LOG" 2>&1 <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ run_logged() {
|
||||
local label="$1"
|
||||
shift
|
||||
local log_file
|
||||
log_file="$(mktemp "${TMPDIR:-/tmp}/openclaw-${label}.XXXXXX.log")"
|
||||
local tmp_dir="${TMPDIR:-/tmp}"
|
||||
tmp_dir="${tmp_dir%/}"
|
||||
log_file="$(mktemp "$tmp_dir/openclaw-${label}.XXXXXX")"
|
||||
if ! "$@" >"$log_file" 2>&1; then
|
||||
cat "$log_file"
|
||||
rm -f "$log_file"
|
||||
|
||||
27
scripts/lib/official-external-channel-catalog.json
Normal file
27
scripts/lib/official-external-channel-catalog.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"name": "@wecom/wecom-openclaw-plugin",
|
||||
"description": "OpenClaw WeCom channel plugin by the Tencent WeCom team.",
|
||||
"source": "external",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "wecom",
|
||||
"label": "WeCom",
|
||||
"selectionLabel": "WeCom (Enterprise WeChat)",
|
||||
"detailLabel": "WeCom",
|
||||
"docsPath": "/plugins/community#wecom",
|
||||
"docsLabel": "wecom",
|
||||
"blurb": "Enterprise WeChat bot and conversation channel.",
|
||||
"aliases": ["qywx", "wework", "enterprise-wechat"],
|
||||
"order": 45
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@wecom/wecom-openclaw-plugin",
|
||||
"defaultChoice": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -144,7 +144,15 @@ function assertEntryFileExists(entry) {
|
||||
|
||||
async function smokeChannelEntry(entryFile) {
|
||||
assertEntryFileExists(entryFile);
|
||||
const entry = (await importBuiltModule(entryFile.path)).default;
|
||||
let entry;
|
||||
try {
|
||||
entry = (await importBuiltModule(entryFile.path)).default;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`${entryFile.id} ${entryFile.kind} entry failed to import ${entryFile.path}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
assert.equal(entry.kind, "bundled-channel-entry", `${entryFile.id} channel entry kind mismatch`);
|
||||
assert.equal(
|
||||
typeof entry.loadChannelPlugin,
|
||||
@@ -163,7 +171,15 @@ async function smokeChannelEntry(entryFile) {
|
||||
|
||||
async function smokeSetupEntry(entryFile) {
|
||||
assertEntryFileExists(entryFile);
|
||||
const entry = (await importBuiltModule(entryFile.path)).default;
|
||||
let entry;
|
||||
try {
|
||||
entry = (await importBuiltModule(entryFile.path)).default;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`${entryFile.id} ${entryFile.kind} entry failed to import ${entryFile.path}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
if (entry?.kind !== "bundled-channel-setup-entry") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import officialExternalChannelCatalog from "./lib/official-external-channel-catalog.json" with { type: "json" };
|
||||
import { isRecord, trimString } from "./lib/record-shared.mjs";
|
||||
import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
|
||||
|
||||
@@ -13,9 +14,14 @@ function toCatalogInstall(value, packageName) {
|
||||
return null;
|
||||
}
|
||||
const defaultChoice = trimString(install.defaultChoice);
|
||||
const minHostVersion = trimString(install.minHostVersion);
|
||||
const expectedIntegrity = trimString(install.expectedIntegrity);
|
||||
return {
|
||||
npmSpec,
|
||||
...(defaultChoice === "npm" || defaultChoice === "local" ? { defaultChoice } : {}),
|
||||
...(minHostVersion ? { minHostVersion } : {}),
|
||||
...(expectedIntegrity ? { expectedIntegrity } : {}),
|
||||
...(install.allowInvalidConfigRecovery === true ? { allowInvalidConfigRecovery: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,7 +56,9 @@ function buildCatalogEntry(packageJson) {
|
||||
export function buildOfficialChannelCatalog(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const extensionsRoot = path.join(repoRoot, "extensions");
|
||||
const entries = [];
|
||||
const entries = Array.isArray(officialExternalChannelCatalog.entries)
|
||||
? [...officialExternalChannelCatalog.entries]
|
||||
: [];
|
||||
if (!fs.existsSync(extensionsRoot)) {
|
||||
return { entries };
|
||||
}
|
||||
|
||||
@@ -467,6 +467,55 @@ describe("session_status tool", () => {
|
||||
expect(details.sessionKey).toBe("main");
|
||||
});
|
||||
|
||||
it("falls back from implicit default-account direct policy keys to persisted direct sessions", async () => {
|
||||
resetSessionStore({
|
||||
"agent:main:telegram:direct:1053274893": {
|
||||
sessionId: "s-direct",
|
||||
updatedAt: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = getSessionStatusTool("agent:main:telegram:default:direct:1053274893");
|
||||
|
||||
const result = await tool.execute("call-default-direct", {});
|
||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||
expect(details.ok).toBe(true);
|
||||
expect(details.sessionKey).toBe("agent:main:telegram:direct:1053274893");
|
||||
});
|
||||
|
||||
it("falls back from implicit default-account direct policy keys to main sessions", async () => {
|
||||
resetSessionStore({
|
||||
"agent:main:main": {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = getSessionStatusTool("agent:main:telegram:default:direct:1053274893");
|
||||
|
||||
const result = await tool.execute("call-default-direct-main", {});
|
||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||
expect(details.ok).toBe(true);
|
||||
expect(details.sessionKey).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("keeps explicit default-account direct session lookups strict", async () => {
|
||||
resetSessionStore({
|
||||
"agent:main:main": {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = getSessionStatusTool("agent:main:telegram:default:direct:1053274893");
|
||||
|
||||
await expect(
|
||||
tool.execute("call-default-direct-explicit", {
|
||||
sessionKey: "agent:main:telegram:default:direct:1053274893",
|
||||
}),
|
||||
).rejects.toThrow("Unknown sessionKey: agent:main:telegram:default:direct:1053274893");
|
||||
});
|
||||
|
||||
it("prefers a literal current session key in session_status", async () => {
|
||||
resetSessionStore({
|
||||
main: {
|
||||
|
||||
@@ -133,6 +133,33 @@ function resolveStoreScopedRequesterKey(params: {
|
||||
return parsed.rest === params.mainKey ? params.mainKey : params.requesterKey;
|
||||
}
|
||||
|
||||
function listImplicitDefaultDirectFallbackKeys(params: {
|
||||
keyRaw: string;
|
||||
mainKey: string;
|
||||
}): string[] {
|
||||
const parsed = parseAgentSessionKey(params.keyRaw.trim());
|
||||
if (!parsed) {
|
||||
return [];
|
||||
}
|
||||
const parts = parsed.rest.split(":");
|
||||
if (parts.length < 4 || parts[1] !== "default" || parts[2] !== "direct") {
|
||||
return [];
|
||||
}
|
||||
const [channel, , , ...peerParts] = parts;
|
||||
if (!channel || peerParts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const candidates = [
|
||||
`agent:${parsed.agentId}:${channel}:direct:${peerParts.join(":")}`,
|
||||
buildAgentMainSessionKey({
|
||||
agentId: parsed.agentId,
|
||||
mainKey: params.mainKey,
|
||||
}),
|
||||
params.mainKey,
|
||||
];
|
||||
return [...new Set(candidates)];
|
||||
}
|
||||
|
||||
function formatSessionTaskLine(params: {
|
||||
relatedSessionKey: string;
|
||||
callerOwnerKey: string;
|
||||
@@ -295,6 +322,7 @@ export function createSessionStatusTool(opts?: {
|
||||
let requestedKeyRaw = requestedKeyParam ?? opts?.agentSessionKey;
|
||||
const requestedKeyInput = requestedKeyRaw?.trim() ?? "";
|
||||
let resolvedViaSessionId = false;
|
||||
let resolvedViaImplicitCurrentFallback = false;
|
||||
if (!requestedKeyRaw?.trim()) {
|
||||
throw new Error("sessionKey required");
|
||||
}
|
||||
@@ -402,17 +430,39 @@ export function createSessionStatusTool(opts?: {
|
||||
});
|
||||
}
|
||||
|
||||
if (!resolved && requestedKeyParam === undefined) {
|
||||
for (const fallbackKey of listImplicitDefaultDirectFallbackKeys({
|
||||
keyRaw: requestedKeyRaw,
|
||||
mainKey,
|
||||
})) {
|
||||
resolved = resolveSessionEntry({
|
||||
store,
|
||||
keyRaw: fallbackKey,
|
||||
alias,
|
||||
mainKey,
|
||||
requesterInternalKey: storeScopedRequesterKey,
|
||||
includeAliasFallback: true,
|
||||
});
|
||||
if (resolved) {
|
||||
resolvedViaImplicitCurrentFallback = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
const kind = shouldResolveSessionIdInput(requestedKeyRaw) ? "sessionId" : "sessionKey";
|
||||
throw new Error(`Unknown ${kind}: ${requestedKeyRaw}`);
|
||||
}
|
||||
|
||||
// Preserve caller-scoped raw-key/current lookups as "self" for visibility checks.
|
||||
const visibilityTargetKey =
|
||||
!resolvedViaSessionId &&
|
||||
(requestedKeyInput === "current" || resolved.key === requestedKeyInput)
|
||||
? visibilityRequesterKey
|
||||
: normalizeVisibilityTargetSessionKey(resolved.key, agentId);
|
||||
const shouldTreatVisibilityTargetAsSelf =
|
||||
resolvedViaImplicitCurrentFallback ||
|
||||
(!resolvedViaSessionId &&
|
||||
(requestedKeyInput === "current" || resolved.key === requestedKeyInput));
|
||||
const visibilityTargetKey = shouldTreatVisibilityTargetAsSelf
|
||||
? visibilityRequesterKey
|
||||
: normalizeVisibilityTargetSessionKey(resolved.key, agentId);
|
||||
const access = visibilityGuard.check(visibilityTargetKey);
|
||||
if (!access.allowed) {
|
||||
throw new Error(access.error);
|
||||
|
||||
@@ -268,6 +268,23 @@ describe("info command handlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards resolved fast mode to /status", async () => {
|
||||
const params = buildInfoParams("/status", {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as OpenClawConfig);
|
||||
params.resolvedFastMode = true;
|
||||
|
||||
const statusResult = await handleStatusCommand(params, true);
|
||||
|
||||
expect(statusResult?.shouldContinue).toBe(false);
|
||||
expect(vi.mocked(buildStatusReply)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resolvedFastMode: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the canonical target session agent when listing /commands", async () => {
|
||||
const { handleCommandsListCommand } = await import("./commands-info.js");
|
||||
const params = buildInfoParams("/commands", {
|
||||
|
||||
@@ -204,6 +204,7 @@ export const handleStatusCommand: CommandHandler = async (params, allowTextComma
|
||||
model: params.model,
|
||||
contextTokens: params.contextTokens,
|
||||
resolvedThinkLevel: params.resolvedThinkLevel,
|
||||
resolvedFastMode: params.resolvedFastMode,
|
||||
resolvedVerboseLevel: params.resolvedVerboseLevel,
|
||||
resolvedReasoningLevel: params.resolvedReasoningLevel,
|
||||
resolvedElevatedLevel: params.resolvedElevatedLevel,
|
||||
|
||||
@@ -53,6 +53,7 @@ export type HandleCommandsParams = {
|
||||
opts?: GetReplyOptions;
|
||||
defaultGroupActivation: () => "always" | "mention";
|
||||
resolvedThinkLevel?: ThinkLevel;
|
||||
resolvedFastMode?: boolean;
|
||||
resolvedVerboseLevel: VerboseLevel;
|
||||
resolvedReasoningLevel: ReasoningLevel;
|
||||
resolvedElevatedLevel?: ElevatedLevel;
|
||||
|
||||
@@ -276,7 +276,24 @@ describe("buildStatusMessage", () => {
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
});
|
||||
|
||||
expect(normalizeTestText(text)).toContain("Fast: on");
|
||||
expect(normalizeTestText(text)).toContain("Fast");
|
||||
});
|
||||
|
||||
it("hides fast mode when disabled", () => {
|
||||
const text = buildStatusMessage({
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
},
|
||||
sessionEntry: {
|
||||
sessionId: "fast-off",
|
||||
updatedAt: 0,
|
||||
fastMode: false,
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
});
|
||||
|
||||
expect(normalizeTestText(text)).not.toContain("Fast");
|
||||
});
|
||||
|
||||
it("shows configured text verbosity for the active model", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import officialExternalChannelCatalog from "../../../scripts/lib/official-external-channel-catalog.json" with { type: "json" };
|
||||
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
|
||||
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
|
||||
@@ -162,7 +163,9 @@ function resolveOfficialCatalogPaths(options: CatalogOptions): string[] {
|
||||
}
|
||||
|
||||
function loadOfficialCatalogEntries(options: CatalogOptions): ChannelPluginCatalogEntry[] {
|
||||
return loadCatalogEntriesFromPaths(resolveOfficialCatalogPaths(options))
|
||||
const builtInEntries = parseCatalogEntries(officialExternalChannelCatalog);
|
||||
const fileEntries = loadCatalogEntriesFromPaths(resolveOfficialCatalogPaths(options));
|
||||
return [...builtInEntries, ...fileEntries]
|
||||
.map((entry) => buildExternalCatalogEntry(entry))
|
||||
.filter((entry): entry is ChannelPluginCatalogEntry => Boolean(entry));
|
||||
}
|
||||
|
||||
@@ -36,3 +36,9 @@ describeOfficialFallbackChannelCatalogContract({
|
||||
externalNpmSpec: "@vendor/whatsapp-fork",
|
||||
externalLabel: "WhatsApp Fork",
|
||||
});
|
||||
|
||||
describeChannelCatalogEntryContract({
|
||||
channelId: "wecom",
|
||||
npmSpec: "@wecom/wecom-openclaw-plugin",
|
||||
alias: "wework",
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
withTimeout.mockImplementation(async <T>(promise: Promise<T>) => await promise);
|
||||
});
|
||||
|
||||
it("passes pinned npm specs and expected integrity to npm installs with progress", async () => {
|
||||
it("passes npm specs and optional expected integrity to npm installs with progress", async () => {
|
||||
installPluginFromNpmSpec.mockImplementation(async (params) => {
|
||||
params.logger?.info?.("Downloading demo-plugin…");
|
||||
return {
|
||||
@@ -137,7 +137,7 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not offer npm installs without an exact version and integrity pin", async () => {
|
||||
it("offers registry npm specs without requiring an exact version or integrity pin", async () => {
|
||||
let captured:
|
||||
| {
|
||||
options: Array<{ value: "npm" | "local" | "skip"; label: string; hint?: string }>;
|
||||
@@ -163,8 +163,11 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
runtime: {} as never,
|
||||
});
|
||||
|
||||
expect(captured?.options).toEqual([{ value: "skip", label: "Skip for now" }]);
|
||||
expect(captured?.initialValue).toBe("skip");
|
||||
expect(captured?.options).toEqual([
|
||||
{ value: "npm", label: "Download from npm (@demo/plugin)" },
|
||||
{ value: "skip", label: "Skip for now" },
|
||||
]);
|
||||
expect(captured?.initialValue).toBe("npm");
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -191,14 +191,13 @@ function resolveBundledLocalPath(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePinnedNpmSpecForOnboarding(install: PluginPackageInstall): string | null {
|
||||
function resolveNpmSpecForOnboarding(install: PluginPackageInstall): string | null {
|
||||
const npmSpec = install.npmSpec?.trim();
|
||||
const expectedIntegrity = install.expectedIntegrity?.trim();
|
||||
if (!npmSpec || !expectedIntegrity) {
|
||||
if (!npmSpec) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseRegistryNpmSpec(npmSpec);
|
||||
return parsed?.selectorKind === "exact-version" ? npmSpec : null;
|
||||
return parsed ? npmSpec : null;
|
||||
}
|
||||
|
||||
function resolveInstallDefaultChoice(params: {
|
||||
@@ -241,7 +240,7 @@ async function promptInstallChoice(params: {
|
||||
defaultChoice: InstallChoice;
|
||||
prompter: WizardPrompter;
|
||||
}): Promise<InstallChoice> {
|
||||
const npmSpec = resolvePinnedNpmSpecForOnboarding(params.entry.install);
|
||||
const npmSpec = resolveNpmSpecForOnboarding(params.entry.install);
|
||||
const safeLabel = sanitizeTerminalText(params.entry.label);
|
||||
const safeNpmSpec = npmSpec ? sanitizeTerminalText(npmSpec) : null;
|
||||
const safeLocalPath = params.localPath ? sanitizeTerminalText(params.localPath) : null;
|
||||
@@ -399,7 +398,7 @@ export async function ensureOnboardingPluginInstalled(params: {
|
||||
workspaceDir,
|
||||
allowLocal,
|
||||
});
|
||||
const npmSpec = resolvePinnedNpmSpecForOnboarding(entry.install);
|
||||
const npmSpec = resolveNpmSpecForOnboarding(entry.install);
|
||||
const defaultChoice = resolveInstallDefaultChoice({
|
||||
cfg: next,
|
||||
entry,
|
||||
|
||||
@@ -893,6 +893,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -2066,6 +2069,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -3081,6 +3087,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Discord Draft Chunk Break Preference",
|
||||
help: "Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph.",
|
||||
},
|
||||
"streaming.preview.toolProgress": {
|
||||
label: "Discord Draft Tool Progress",
|
||||
help: "Show tool/progress activity in the live draft preview message (default: true). Set false to keep tool updates as separate messages.",
|
||||
},
|
||||
"retry.attempts": {
|
||||
label: "Discord Retry Attempts",
|
||||
help: "Max retry attempts for outbound Discord API calls (default: 3).",
|
||||
@@ -9417,6 +9427,27 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
],
|
||||
},
|
||||
},
|
||||
groupAllowFrom: {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dmPolicy: {
|
||||
type: "string",
|
||||
enum: ["open", "allowlist", "disabled"],
|
||||
},
|
||||
groupPolicy: {
|
||||
type: "string",
|
||||
enum: ["open", "allowlist", "disabled"],
|
||||
},
|
||||
systemPrompt: {
|
||||
type: "string",
|
||||
},
|
||||
@@ -9479,42 +9510,41 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
],
|
||||
},
|
||||
tts: {
|
||||
execApprovals: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
anyOf: [
|
||||
{
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
},
|
||||
baseUrl: {
|
||||
type: "string",
|
||||
},
|
||||
apiKey: {
|
||||
type: "string",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
},
|
||||
voice: {
|
||||
type: "string",
|
||||
},
|
||||
authStyle: {
|
||||
type: "string",
|
||||
enum: ["bearer", "api-key"],
|
||||
},
|
||||
queryParams: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
approvers: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
speed: {
|
||||
type: "number",
|
||||
agentFilter: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
sessionFilter: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
target: {
|
||||
type: "string",
|
||||
enum: ["dm", "channel", "both"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
@@ -9637,6 +9667,27 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
],
|
||||
},
|
||||
},
|
||||
groupAllowFrom: {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dmPolicy: {
|
||||
type: "string",
|
||||
enum: ["open", "allowlist", "disabled"],
|
||||
},
|
||||
groupPolicy: {
|
||||
type: "string",
|
||||
enum: ["open", "allowlist", "disabled"],
|
||||
},
|
||||
systemPrompt: {
|
||||
type: "string",
|
||||
},
|
||||
@@ -9699,6 +9750,45 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
],
|
||||
},
|
||||
execApprovals: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
approvers: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
agentFilter: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
sessionFilter: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
target: {
|
||||
type: "string",
|
||||
enum: ["dm", "channel", "both"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
@@ -10866,6 +10956,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -11775,6 +11868,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -12311,6 +12407,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Slack Native Streaming",
|
||||
help: "Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Requires a reply thread target; top-level DMs stay on the non-thread fallback path.",
|
||||
},
|
||||
"streaming.preview.toolProgress": {
|
||||
label: "Slack Draft Tool Progress",
|
||||
help: "Show tool/progress activity in the live draft preview message (default: true). Set false to keep tool updates as separate messages.",
|
||||
},
|
||||
"thread.historyScope": {
|
||||
label: "Slack Thread History Scope",
|
||||
help: 'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||
@@ -13058,6 +13158,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -14096,6 +14199,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
toolProgress: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -14498,6 +14604,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Telegram Draft Chunk Break Preference",
|
||||
help: "Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence).",
|
||||
},
|
||||
"streaming.preview.toolProgress": {
|
||||
label: "Telegram Draft Tool Progress",
|
||||
help: "Show tool/progress activity in the live draft preview message (default: true). Set false to keep tool updates as separate messages.",
|
||||
},
|
||||
"retry.attempts": {
|
||||
label: "Telegram Retry Attempts",
|
||||
help: "Max retry attempts for outbound Telegram API calls (default: 3).",
|
||||
|
||||
@@ -27728,6 +27728,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
tags: ["advanced", "url-secret"],
|
||||
},
|
||||
},
|
||||
version: "2026.4.22",
|
||||
version: "2026.4.22-beta.1",
|
||||
generatedAt: "2026-03-22T21:17:33.302Z",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
const LEGACY_QA_LAB_DIR = ["qa", "lab"].join("-");
|
||||
const LEGACY_QA_CHANNEL_DIR = ["qa", "channel"].join("-");
|
||||
export const LEGACY_QA_CHANNEL_RUNTIME_API_PATH = [
|
||||
"dist",
|
||||
"extensions",
|
||||
LEGACY_QA_CHANNEL_DIR,
|
||||
"runtime-api.js",
|
||||
].join("/");
|
||||
|
||||
type NpmUpdateCompatSidecar = {
|
||||
path: string;
|
||||
@@ -9,7 +16,7 @@ const EMPTY_RUNTIME_SIDECAR = "export {};\n";
|
||||
|
||||
export const NPM_UPDATE_COMPAT_SIDECARS = [
|
||||
{
|
||||
path: "dist/extensions/qa-channel/runtime-api.js",
|
||||
path: LEGACY_QA_CHANNEL_RUNTIME_API_PATH,
|
||||
content: EMPTY_RUNTIME_SIDECAR,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "./npm-update-compat-sidecars.js";
|
||||
import {
|
||||
LEGACY_QA_CHANNEL_RUNTIME_API_PATH,
|
||||
NPM_UPDATE_COMPAT_SIDECAR_PATHS,
|
||||
} from "./npm-update-compat-sidecars.js";
|
||||
|
||||
export const PACKAGE_DIST_INVENTORY_RELATIVE_PATH = "dist/postinstall-inventory.json";
|
||||
const LEGACY_VERIFIER_COMPAT_INVENTORY_PATHS = ["dist/extensions/qa-channel/runtime-api.js"];
|
||||
const LEGACY_VERIFIER_COMPAT_INVENTORY_PATHS = [LEGACY_QA_CHANNEL_RUNTIME_API_PATH];
|
||||
const LEGACY_QA_LAB_DIR = ["qa", "lab"].join("-");
|
||||
const OMITTED_QA_EXTENSION_PREFIXES = [
|
||||
"dist/extensions/qa-channel/",
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
export { getRuntimeConfigSnapshot } from "../config/runtime-snapshot.js";
|
||||
export {
|
||||
clearRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
} from "../config/runtime-snapshot.js";
|
||||
export type { OpenClawConfig } from "../config/types.js";
|
||||
|
||||
@@ -219,6 +219,60 @@ describe("provider install catalog", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes trusted registry npm specs without requiring an exact version or integrity pin", () => {
|
||||
discoverOpenClawPlugins.mockReturnValue({
|
||||
candidates: [
|
||||
{
|
||||
idHint: "vllm",
|
||||
origin: "config",
|
||||
rootDir: "/Users/test/.openclaw/extensions/vllm",
|
||||
source: "/Users/test/.openclaw/extensions/vllm/index.js",
|
||||
packageName: "@openclaw/vllm",
|
||||
packageDir: "/Users/test/.openclaw/extensions/vllm",
|
||||
packageManifest: {
|
||||
install: {
|
||||
npmSpec: "@openclaw/vllm",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
loadPluginManifest.mockReturnValue({
|
||||
ok: true,
|
||||
manifestPath: "/Users/test/.openclaw/extensions/vllm/openclaw.plugin.json",
|
||||
manifest: {
|
||||
id: "vllm",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
});
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
pluginId: "vllm",
|
||||
providerId: "vllm",
|
||||
methodId: "server",
|
||||
choiceId: "vllm",
|
||||
choiceLabel: "vLLM",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(resolveProviderInstallCatalogEntry("vllm")).toEqual({
|
||||
pluginId: "vllm",
|
||||
providerId: "vllm",
|
||||
methodId: "server",
|
||||
choiceId: "vllm",
|
||||
choiceLabel: "vLLM",
|
||||
label: "vLLM",
|
||||
origin: "config",
|
||||
install: {
|
||||
npmSpec: "@openclaw/vllm",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not expose npm install specs from untrusted package metadata", () => {
|
||||
discoverOpenClawPlugins.mockReturnValue({
|
||||
candidates: [
|
||||
|
||||
@@ -53,7 +53,7 @@ function resolvePluginManifest(
|
||||
return manifest.ok ? manifest : null;
|
||||
}
|
||||
|
||||
function resolveTrustedPinnedNpmSpec(params: {
|
||||
function resolveTrustedNpmSpec(params: {
|
||||
origin: PluginOrigin;
|
||||
install?: PluginPackageInstall;
|
||||
}): string | undefined {
|
||||
@@ -61,12 +61,11 @@ function resolveTrustedPinnedNpmSpec(params: {
|
||||
return undefined;
|
||||
}
|
||||
const npmSpec = params.install?.npmSpec?.trim();
|
||||
const expectedIntegrity = params.install?.expectedIntegrity?.trim();
|
||||
if (!npmSpec || !expectedIntegrity) {
|
||||
if (!npmSpec) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseRegistryNpmSpec(npmSpec);
|
||||
return parsed?.selectorKind === "exact-version" ? npmSpec : undefined;
|
||||
return parsed ? npmSpec : undefined;
|
||||
}
|
||||
|
||||
function resolveInstallInfo(params: {
|
||||
@@ -75,7 +74,7 @@ function resolveInstallInfo(params: {
|
||||
packageDir?: string;
|
||||
workspaceDir?: string;
|
||||
}): PluginPackageInstall | null {
|
||||
const npmSpec = resolveTrustedPinnedNpmSpec({
|
||||
const npmSpec = resolveTrustedNpmSpec({
|
||||
origin: params.origin,
|
||||
install: params.install,
|
||||
});
|
||||
|
||||
29
src/status/status-message.test.ts
Normal file
29
src/status/status-message.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
||||
import { buildStatusMessage } from "./status-message.js";
|
||||
|
||||
const buildFastStatus = (model: string, fastMode: boolean) =>
|
||||
normalizeTestText(
|
||||
buildStatusMessage({
|
||||
modelAuth: "api-key",
|
||||
activeModelAuth: "api-key",
|
||||
agent: { model },
|
||||
sessionEntry: {
|
||||
sessionId: "fast-status",
|
||||
updatedAt: 0,
|
||||
fastMode,
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
}),
|
||||
);
|
||||
|
||||
describe("buildStatusMessage fast mode labels", () => {
|
||||
it("shows fast mode when enabled", () => {
|
||||
expect(buildFastStatus("openai/gpt-5.4", true)).toContain("Fast");
|
||||
});
|
||||
|
||||
it("hides fast mode when disabled", () => {
|
||||
expect(buildFastStatus("anthropic/claude-opus-4-6", false)).not.toContain("Fast");
|
||||
});
|
||||
});
|
||||
@@ -237,6 +237,13 @@ const formatQueueDetails = (queue?: QueueStatus) => {
|
||||
return detailParts.length ? ` (${detailParts.join(" · ")})` : "";
|
||||
};
|
||||
|
||||
const formatFastModeLabel = (enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
return "Fast";
|
||||
};
|
||||
|
||||
const readUsageFromSessionLog = (
|
||||
sessionId?: string,
|
||||
sessionEntry?: SessionEntry,
|
||||
@@ -705,7 +712,7 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
const optionParts = [
|
||||
`Runtime: ${runtime.label}`,
|
||||
`Think: ${thinkLevel}`,
|
||||
fastMode ? "Fast: on" : null,
|
||||
formatFastModeLabel(fastMode),
|
||||
textVerbosity ? `Text: ${textVerbosity}` : null,
|
||||
verboseLabel,
|
||||
traceLabel,
|
||||
|
||||
@@ -68,8 +68,21 @@ describe("buildOfficialChannelCatalog", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(buildOfficialChannelCatalog({ repoRoot })).toEqual({
|
||||
entries: [
|
||||
expect(buildOfficialChannelCatalog({ repoRoot }).entries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "@wecom/wecom-openclaw-plugin",
|
||||
openclaw: expect.objectContaining({
|
||||
channel: expect.objectContaining({
|
||||
id: "wecom",
|
||||
label: "WeCom",
|
||||
}),
|
||||
install: {
|
||||
npmSpec: "@wecom/wecom-openclaw-plugin",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "@openclaw/whatsapp",
|
||||
version: "2026.3.23",
|
||||
@@ -89,8 +102,8 @@ describe("buildOfficialChannelCatalog", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("writes the official catalog under dist", () => {
|
||||
@@ -118,8 +131,11 @@ describe("buildOfficialChannelCatalog", () => {
|
||||
|
||||
const outputPath = path.join(repoRoot, OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
expect(JSON.parse(fs.readFileSync(outputPath, "utf8"))).toEqual({
|
||||
entries: [
|
||||
expect(JSON.parse(fs.readFileSync(outputPath, "utf8")).entries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "@wecom/wecom-openclaw-plugin",
|
||||
}),
|
||||
{
|
||||
name: "@openclaw/whatsapp",
|
||||
openclaw: {
|
||||
@@ -135,7 +151,7 @@ describe("buildOfficialChannelCatalog", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user