mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-17 11:38:44 +08:00
Compare commits
1 Commits
codex/fron
...
codex/capa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91625aa9f3 |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
89
CHANGELOG.md
89
CHANGELOG.md
@@ -6,57 +6,68 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- CLI/capabilities: add a first-class `openclaw capability ...` hub for provider-backed inference workflows across model, media, web, and embedding tasks, with capability inspection, provider discovery, and consistent JSON output. Thanks @Takhoffman.
|
||||
- Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, and doctor flows again, and keep the Docker Claude CLI live lane aligned with the restored guidance.
|
||||
- Plugins/webhooks: add a bundled webhook ingress plugin so external automation can create and drive bound TaskFlows through per-route shared-secret endpoints. (#61892) Thanks @mbelinky.
|
||||
- Tools/media generation: preserve intent across auth-backed image, music, and video provider fallback, remap size, aspect ratio, resolution, and duration hints to the closest supported option, and surface explicit provider capabilities plus mode-aware video-to-video support.
|
||||
- Memory/wiki: restore the bundled `memory-wiki` stack with plugin, CLI, sync/query/apply tooling, and memory-host integration for wiki-backed memory workflows.
|
||||
- Providers/Arcee AI: add a bundled Arcee AI provider plugin with Trinity catalog entries, OpenRouter support, and updated onboarding/auth guidance. (#62068) Thanks @arthurbr11.
|
||||
- Providers/Google: add Gemma 4 model support and keep Google fallback resolution on the requested provider path so native Google Gemma routes work again. (#61507) Thanks @eyjohn.
|
||||
- Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, doctor flows, and Docker Claude CLI live lanes again.
|
||||
- ACP/ACPX plugin: bump the bundled `acpx` pin to `0.5.1` so plugin-local installs and strict version checks pick up the latest published runtime release. (#62148) Thanks @onutc.
|
||||
- Tools/media generation: auto-fallback across auth-backed image, music, and video providers by default, and remap fallback size, aspect ratio, resolution, and duration hints to the closest supported option instead of dropping intent on provider switches.
|
||||
- Tools/media generation: report applied fallback geometry and duration settings consistently in tool results, add a shared normalization contract for image/music/video runtimes, and simplify the bundled image-generation-core runtime test to only verify the plugin-sdk re-export seam.
|
||||
- Gateway/sessions: add persisted compaction checkpoints plus Sessions UI branch/restore actions so operators can inspect and recover pre-compaction session state. (#62146) Thanks @scoootscooob.
|
||||
- Providers/Ollama: detect vision capability from the `/api/show` response and set image input on models that support it so Ollama vision models accept image attachments. (#62193) Thanks @BruceMacD.
|
||||
- Memory/dreaming: ingest redacted session transcripts into the dreaming corpus with per-day session-corpus notes, cursor checkpointing, and promotion/doctor support. (#62227) Thanks @vignesh07.
|
||||
- Tools/media: document per-provider music and video generation capabilities, and add shared live video-to-video sweep coverage for providers that support local reference clips.
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI/capabilities: keep provider-backed capability behavior aligned with actual runtime execution by fixing explicit TTS override handling, profile-aware gateway TTS prefs resolution, per-request transcription `prompt`/`language` overrides, image output MIME/extension mismatches, configured web-search fallback behavior, and agent-vs-CLI web-search execution drift.
|
||||
- Channels/secrets: keep bundled channel artifact and secret-contract loading stable under lazy loading so bundled channel secrets continue to appear in `openclaw secret`, status, and security-audit surfaces.
|
||||
- Providers/xAI: recognize `api.grok.x.ai` as an xAI-native endpoint again so native xAI web-search attribution keeps working on Grok-hosted base URLs. (#61377) Thanks @jjjojoj.
|
||||
- Providers/Anthropic/cache: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so Anthropic prompt-cache prefixes keep matching after thinking turns. (#61793)
|
||||
- Auth/OpenAI Codex OAuth: reload fresh on-disk credentials inside the locked refresh path and retry once after `refresh_token_reused` rotates only the stored refresh token, so relogin/restart recovery stops getting stuck on stale cached auth state. Thanks @owen-ever.
|
||||
- Agents/history and replies: buffer phaseless OpenAI WS text until a real assistant phase arrives, keep replay and SSE history sequence tracking aligned, hide commentary and leaked tool XML from user-visible history, and keep history-based follow-up replies on `final_answer` text only. (#61729, #61747, #61829, #61855, #61954) Thanks @100yenadmin, @afurm, and @openperf.
|
||||
- Plugins/channels: keep bundled channel artifact and secret-contract loading stable under lazy loading, preserve plugin-schema defaults during install, and fix Windows `file://` plus native-Jiti plugin loader paths so onboarding, doctor, `openclaw secret`, and bundled plugin installs work again. (#61832, #61836, #61853, #61856) Thanks @Zeesejo and @SuperMarioYL.
|
||||
- Auto-reply/media: allow managed generated-media `MEDIA:` paths from normal reply text again while still blocking arbitrary host-local media and document paths, so generated media keep delivering without reopening host-path injection holes.
|
||||
- Runtime event trust: mark background `notifyOnExit` summaries, ACP parent-stream relays, and wake-hook payloads as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted `System:` text. (#62003)
|
||||
- Providers/Anthropic: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so prompt-cache prefixes keep matching, and skip `service_tier` injection on OAuth-authenticated stream wrapper requests so Claude OAuth streaming stops failing with HTTP 401. (#60356, #61793)
|
||||
- Control UI: show `/tts` audio replies in webchat, detect mistaken `?token=` auth links with the correct `#token=` hint, and keep Copy, Canvas, and mobile exec-approval UI from covering chat content on narrow screens. (#54842, #61514, #61598) Thanks @neeravmakwana.
|
||||
- TUI: route `/status` through the shared session-status command, keep commentary hidden in history, strip raw envelope metadata from async command notices, preserve fallback streaming before per-attempt failures finalize, and restore Kitty keyboard state on exit or fatal crashes. (#49130, #59985, #60043, #61463) Thanks @biefan, @MoerAI, @jwchmodx, and @100yenadmin.
|
||||
- Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.
|
||||
- iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including reconnect recovery, pending approval persistence, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.
|
||||
- Agents/context overflow: combine oversized and aggregate tool-result recovery in one pass and restore a total-context overflow backstop so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.
|
||||
- Agents/exec: preserve explicit `host=node` routing under elevated defaults when `tools.exec.host=auto`, fail loud on invalid elevated cross-host overrides, and keep `strictInlineEval` commands blocked after approval timeouts instead of falling through to automatic execution. (#61739) Thanks @obviyus.
|
||||
- Providers/Ollama: honor the selected provider's `baseUrl` during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678)
|
||||
- Browser/remote CDP: retry the DevTools websocket once after remote browser restarts so healthy remote browser profiles do not fail availability checks during CDP warm-up. (#57397) Thanks @ThanhNguyxn07.
|
||||
- Gateway/status and containers: auto-bind to `0.0.0.0` inside Docker and Podman environments, and probe local TLS gateways over `wss://` with self-signed fingerprint forwarding so container startup and loopback TLS status checks work again. (#61818, #61935) Thanks @openperf and @ThanhNguyxn07.
|
||||
- macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like `OpenClaw 2026.4.2 (d74a122)` again. (#61111) Thanks @oliviareid-svg.
|
||||
- Discord: recover forwarded referenced message text and attachments when snapshots are missing, use `ws://` again for gateway monitor sockets, stop forcing a hardcoded temperature for Codex-backed auto-thread titles, and harden voice receive recovery so rapid speaker restarts keep their next utterance. (#41536, #61670) Thanks @artwalker and @wit-oc.
|
||||
- Slack/threading: keep legacy thread stickiness for real replies when older callers omit `isThreadReply`, while still honoring `replyToMode` for Slack's auto-created top-level `thread_ts`. (#61835) Thanks @kaonash.
|
||||
- Providers/xAI: recognize `api.grok.x.ai` as an xAI-native endpoint again and keep legacy `x_search` auth resolution working so older xAI web-search configs continue to load. (#61377) Thanks @jjjojoj.
|
||||
- Memory/vector recall: surface explicit warnings when `sqlite-vec` is unavailable or vector writes are degraded, and strip managed Light Sleep and REM blocks before daily-note ingestion so memory indexing and dreaming stop reporting false-success or re-ingesting staged output. (#61720) Thanks @MonkeyLeeT.
|
||||
- Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps their content attached to the correct list item. (#60997) Thanks @gucasbrg.
|
||||
- Memory/vector recall: surface explicit warnings when `sqlite-vec` is unavailable or vector writes are degraded so memory indexing no longer reports false-success while semantic recall is impaired.
|
||||
- MS Teams/security: validate file-consent upload URLs against HTTPS, Microsoft/SharePoint host allowlists, and private-IP DNS checks before uploading attachments, blocking SSRF-style consent-upload abuse. (#23596)
|
||||
- QQ Bot/media: route gateway-side attachment and fallback downloads through guarded QQ/Tencent HTTPS fetches so QQ media handling no longer follows arbitrary remote hosts.
|
||||
- Discord/gateway monitor: use `ws://` again for gateway monitor sockets so Discord monitor connections recover reliably after recent gateway socket changes.
|
||||
- Control UI/auth URLs: detect mistaken `?token=` links, show the correct `#token=` fragment hint only on real auth failures, and stop masking the real problem behind a generic device-identity error. (#54842)
|
||||
- Control UI/chat layout: keep Copy and Canvas actions plus mobile exec-approval overlays from covering chat text or command previews on narrow screens. (#61514)
|
||||
- Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps its content attached to the correct list item. (#60997) Thanks @gucasbrg.
|
||||
- Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.
|
||||
- Secrets/x_search: keep legacy `x_search` auth resolution working so older xAI web-search configs continue to load after the plugin-owned auth move.
|
||||
- iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including background-safe reconnects, persisted pending approvals, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.
|
||||
- Discord/forwarding: recover forwarded referenced message text and attachments when Discord omits snapshot payloads, so forwarded-message relays keep the original content. (#61670) Thanks @artwalker.
|
||||
- TUI/status: route `/status` through the shared session-status command and move the old gateway-wide diagnostic summary to `/gateway-status` (`/gwstatus`). Thanks @vincentkoc.
|
||||
- TUI/history and heartbeat: keep assistant commentary hidden on both streamed and reloaded TUI history views, preserve the phase-sanitized REST history contract, and stop forced heartbeat runs from targeting subagent sessions. (#61463) Thanks @100yenadmin.
|
||||
- TUI/command messages: strip inbound envelope metadata before rendering command/system messages so async completion notices stop leaking raw wrappers into the operator terminal. (#59985) Thanks @MoerAI.
|
||||
- TUI/terminal: restore Kitty keyboard protocol and `modifyOtherKeys` state on TUI exit and fatal CLI crashes so parent shells stop inheriting broken keyboard input after `openclaw tui` exits. (#49130) Thanks @biefan.
|
||||
- Plugins/Windows: load plugin entrypoints through `file://` import specifiers on Windows without breaking plugin SDK alias resolution, fixing `ERR_UNSUPPORTED_ESM_URL_SCHEME` for absolute plugin paths. (#61832) Thanks @Zeesejo.
|
||||
- Plugins/Windows: disable native Jiti loading for setup and doctor contract registries on Windows so onboarding and config-doctor plugin probes stop crashing with `ERR_UNSUPPORTED_ESM_URL_SCHEME`. (#61836, #61853)
|
||||
- Plugins/install: preserve plugin-schema defaults during fresh-install raw config validation so bundled plugin installs stop failing when required fields rely on schema defaults. (#61856) Thanks @SuperMarioYL.
|
||||
- macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like `OpenClaw 2026.4.2 (d74a122)` again. (#61111) Thanks @oliviareid-svg.
|
||||
- Gateway/containers: auto-bind to `0.0.0.0` during container startup for Docker and Podman compatibility, while keeping host-side status and doctor checks on the hardened loopback default when `gateway.bind` is unset. (#61818) Thanks @openperf.
|
||||
- Gateway/status: probe local TLS gateways over `wss://`, forward the local cert fingerprint for self-signed loopback probes, and warn when the local TLS runtime cannot load the configured cert. (#61935) Thanks @ThanhNguyxn07.
|
||||
- Slack/threading: keep legacy thread stickiness for real replies when older callers omit `isThreadReply`, while still honoring `replyToMode` for Slack's auto-created top-level `thread_ts`. (#61835) Thanks @kaonash.
|
||||
- Discord/voice: re-arm DAVE receive passthrough without suppressing decrypt-failure rejoin recovery, and clear capture state before finalize teardown so rapid speaker restarts keep their next utterance. (#41536) Thanks @wit-oc.
|
||||
- Providers/Google: recognize Gemma model ids in native Google forward-compat resolution, keep the requested provider when cloning fallback templates, and force Gemma reasoning off so Gemma 4 routes stop failing through the Google catalog fallback. (#61507) Thanks @eyjohn.
|
||||
- Providers/Anthropic: skip `service_tier` injection for OAuth-authenticated stream wrapper requests so Claude OAuth requests stop failing with HTTP 401. (#60356) thanks @openperf.
|
||||
- Providers/OpenAI: keep WebSocket text buffered until a real assistant phase arrives, even when text deltas land before a phaseless `output_item.added` announcement. (#61954) Thanks @100yenadmin.
|
||||
- Providers/OpenAI: accept case-insensitive `plugins.entries.openai.config.personality` values, keep unknown overrides on the friendly overlay path, and add `on` as an alias for `friendly`. Thanks @vincentkoc.
|
||||
- Discord/thread titles: stop forcing a hardcoded temperature for generated auto-thread names so Codex-backed thread title generation works on `openai-codex/*` models again. (#59525)
|
||||
- Agents/message tool: add a `read` plus `threadId` discoverability hint when the configured channel actions support threaded message reads.
|
||||
- Agents/context overflow: combine oversized and aggregate tool-result recovery in one repair pass, and restore a total-context overflow backstop during tool loops so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.
|
||||
- Agents/exec: preserve explicit `host=node` routing under elevated defaults when `tools.exec.host=auto`, and fail loud on invalid elevated cross-host overrides. (#61739) Thanks @obviyus.
|
||||
- Agents/heartbeat: stop truncating live session transcripts after no-op heartbeat acks, move heartbeat cleanup to prompt assembly and compaction, and keep post-filter context-engine ingestion aligned with the real session baseline. (#60998) Thanks @nxmxbbd.
|
||||
- Gateway/TUI: defer terminal chat finalization for per-attempt lifecycle errors so fallback retries keep streaming before the run is marked failed. (#60043) Thanks @jwchmodx.
|
||||
- Gateway/history: seed SSE startup history and raw transcript sequence tracking from one initial transcript snapshot so first history events cannot diverge from subsequent message sequence numbering. (#61855) Thanks @100yenadmin.
|
||||
- Agents/history: keep history-based reply reads and subagent completion summaries on `final_answer` text only so internal commentary stops leaking into user-visible follow-up replies. (#61747) Thanks @afurm.
|
||||
- Agents/history: suppress commentary-only visible-text leaks in streaming and chat history views, and keep sanitized SSE history sequence numbers monotonic after transcript-only refreshes. (#61829) Thanks @100yenadmin.
|
||||
- Agents/history: use one shared assistant-visible sanitizer across embedded delivery and chat-history extraction so leaked `<tool_call>` and `<tool_result>` XML blocks stay hidden from user-facing replies. (#61729) Thanks @openperf.
|
||||
- Agents/history: keep truly legacy unsigned replay text unphased when mixed with phased OpenAI WS assistant blocks, while still inheriting message phase for id-only replay signatures. (#61529) Thanks @100yenadmin.
|
||||
- Memory/dreaming: strip managed Light Sleep and REM blocks before daily-note ingestion so dreaming summaries stop re-ingesting their own staged output into new candidates. (#61720) Thanks @MonkeyLeeT.
|
||||
- Docs/i18n: relocalize final localized-page links after translation so generated locale pages stop keeping stale English-root links when targets appear later in the same run. (#61796) thanks @hxy91819.
|
||||
- Docs/i18n: remove the zh-CN homepage redirect override so Mintlify can resolve the localized Chinese homepage without self-redirecting `/zh-CN/index`.
|
||||
- Tools/web_fetch and web_search: fix `TypeError: fetch failed` caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set `allowH2: false` to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.
|
||||
- Tools/web search/Exa: show Exa Search in onboarding and configure provider pickers again by marking the bundled Exa provider as setup-visible. Thanks @vincentkoc.
|
||||
- Docs/i18n: relocalize final localized-page links after translation and remove the zh-CN homepage redirect override so localized Mintlify pages resolve to the correct language roots again. (#61796) Thanks @hxy91819.
|
||||
- Agents/session keys: backfill `sessionKey` from `sessionId` in the embedded PI runner when callers omit it, so hooks, LCM, and compaction receive a valid key; also normalize whitespace-only session keys to `undefined` before downstream consumers see them. (#60555) Thanks @100yenadmin.
|
||||
- Plugins/provider hooks: stop recursive provider snapshot loads from overflowing the stack during plugin initialization, while still preserving cached nested provider-hook results. (#61922, #61938, #61946, #61951)
|
||||
- Discord/voice: re-arm DAVE receive passthrough without suppressing decrypt-failure rejoin recovery, and clear capture state before finalize teardown so rapid speaker restarts keep their next utterance. (#41536) Thanks @wit-oc.
|
||||
- Agents/exec: keep `strictInlineEval` commands blocked after approval timeouts on both gateway and node exec hosts, so timeout fallback no longer turns timed-out inline interpreter prompts into automatic execution.
|
||||
- QQ Bot/media: route gateway-side attachment and fallback downloads through guarded QQ/Tencent HTTPS fetches so QQ media handling no longer follows arbitrary remote hosts.
|
||||
- Exec/runtime events: mark background `notifyOnExit` summaries and ACP parent-stream relays as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted `System:` text.
|
||||
- Hooks/wake: queue direct and mapped wake-hook payloads as untrusted system events so external wake content no longer enters the main session as trusted input. (#62003)
|
||||
- Slack/thread mentions: add `channels.slack.thread.requireExplicitMention` so Slack channels that already require mentions can also require explicit `@bot` mentions inside bot-participated threads. (#58276) Thanks @praktika-engineer.
|
||||
- UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue.
|
||||
- Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras.
|
||||
- Telegram/doctor: keep top-level access-control fallback in place during multi-account normalization while still promoting legacy default auth into `accounts.default`, so existing named bots keep inherited allowlists without dropping the legacy default bot. (#62263) Thanks @obviyus.
|
||||
- Agents/subagents: honor `sessions_spawn(lightContext: true)` for spawned subagent runs by preserving lightweight bootstrap context through the gateway and embedded runner instead of silently falling back to full workspace bootstrap injection. (#62264) Thanks @theSamPadilla.
|
||||
- Slack/media: keep attachment downloads on the SSRF-guarded dispatcher path so Slack media fetching works on Node 22 without dropping pinned transport enforcement. (#62239) Thanks @openperf.
|
||||
- Docker/plugins: stop forcing bundled plugin discovery to `/app/extensions` in runtime images so packaged installs use compiled `dist/extensions` artifacts again and Node 24 containers do not boot through source-only plugin entry paths. Fixes #62044. (#62316) Thanks @gumadeiras.
|
||||
|
||||
## 2026.4.5
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ RUN corepack enable
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
||||
COPY openclaw.mjs ./
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
COPY scripts/postinstall-bundled-plugins.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
|
||||
@@ -160,6 +159,10 @@ COPY --from=runtime-assets --chown=node:node /app/skills ./skills
|
||||
COPY --from=runtime-assets --chown=node:node /app/docs ./docs
|
||||
COPY --from=runtime-assets --chown=node:node /app/qa ./qa
|
||||
|
||||
# In npm-installed Docker images, prefer the copied source extension tree for
|
||||
# bundled discovery so package metadata that points at source entries stays valid.
|
||||
ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/${OPENCLAW_BUNDLED_PLUGIN_DIR}
|
||||
|
||||
# Keep pnpm available in the runtime image for container-local workflows.
|
||||
# Use a shared Corepack home so the non-root `node` user does not need a
|
||||
# first-run network fetch when invoking pnpm.
|
||||
|
||||
@@ -1327,236 +1327,6 @@ public struct SessionsResolveParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionCompactionCheckpoint: Codable, Sendable {
|
||||
public let checkpointid: String
|
||||
public let sessionkey: String
|
||||
public let sessionid: String
|
||||
public let createdat: Int
|
||||
public let reason: AnyCodable
|
||||
public let tokensbefore: Int?
|
||||
public let tokensafter: Int?
|
||||
public let summary: String?
|
||||
public let firstkeptentryid: String?
|
||||
public let precompaction: [String: AnyCodable]
|
||||
public let postcompaction: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
checkpointid: String,
|
||||
sessionkey: String,
|
||||
sessionid: String,
|
||||
createdat: Int,
|
||||
reason: AnyCodable,
|
||||
tokensbefore: Int?,
|
||||
tokensafter: Int?,
|
||||
summary: String?,
|
||||
firstkeptentryid: String?,
|
||||
precompaction: [String: AnyCodable],
|
||||
postcompaction: [String: AnyCodable])
|
||||
{
|
||||
self.checkpointid = checkpointid
|
||||
self.sessionkey = sessionkey
|
||||
self.sessionid = sessionid
|
||||
self.createdat = createdat
|
||||
self.reason = reason
|
||||
self.tokensbefore = tokensbefore
|
||||
self.tokensafter = tokensafter
|
||||
self.summary = summary
|
||||
self.firstkeptentryid = firstkeptentryid
|
||||
self.precompaction = precompaction
|
||||
self.postcompaction = postcompaction
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case checkpointid = "checkpointId"
|
||||
case sessionkey = "sessionKey"
|
||||
case sessionid = "sessionId"
|
||||
case createdat = "createdAt"
|
||||
case reason
|
||||
case tokensbefore = "tokensBefore"
|
||||
case tokensafter = "tokensAfter"
|
||||
case summary
|
||||
case firstkeptentryid = "firstKeptEntryId"
|
||||
case precompaction = "preCompaction"
|
||||
case postcompaction = "postCompaction"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionListParams: Codable, Sendable {
|
||||
public let key: String
|
||||
|
||||
public init(
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionGetParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionBranchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionRestoreParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionListResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let key: String
|
||||
public let checkpoints: [SessionCompactionCheckpoint]
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
key: String,
|
||||
checkpoints: [SessionCompactionCheckpoint])
|
||||
{
|
||||
self.ok = ok
|
||||
self.key = key
|
||||
self.checkpoints = checkpoints
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case key
|
||||
case checkpoints
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionGetResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let key: String
|
||||
public let checkpoint: SessionCompactionCheckpoint
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
key: String,
|
||||
checkpoint: SessionCompactionCheckpoint)
|
||||
{
|
||||
self.ok = ok
|
||||
self.key = key
|
||||
self.checkpoint = checkpoint
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case key
|
||||
case checkpoint
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionBranchResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let sourcekey: String
|
||||
public let key: String
|
||||
public let sessionid: String
|
||||
public let checkpoint: SessionCompactionCheckpoint
|
||||
public let entry: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
sourcekey: String,
|
||||
key: String,
|
||||
sessionid: String,
|
||||
checkpoint: SessionCompactionCheckpoint,
|
||||
entry: [String: AnyCodable])
|
||||
{
|
||||
self.ok = ok
|
||||
self.sourcekey = sourcekey
|
||||
self.key = key
|
||||
self.sessionid = sessionid
|
||||
self.checkpoint = checkpoint
|
||||
self.entry = entry
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case sourcekey = "sourceKey"
|
||||
case key
|
||||
case sessionid = "sessionId"
|
||||
case checkpoint
|
||||
case entry
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionRestoreResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let key: String
|
||||
public let sessionid: String
|
||||
public let checkpoint: SessionCompactionCheckpoint
|
||||
public let entry: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
key: String,
|
||||
sessionid: String,
|
||||
checkpoint: SessionCompactionCheckpoint,
|
||||
entry: [String: AnyCodable])
|
||||
{
|
||||
self.ok = ok
|
||||
self.key = key
|
||||
self.sessionid = sessionid
|
||||
self.checkpoint = checkpoint
|
||||
self.entry = entry
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case key
|
||||
case sessionid = "sessionId"
|
||||
case checkpoint
|
||||
case entry
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCreateParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let agentid: String?
|
||||
|
||||
@@ -361,14 +361,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"update_plan": {
|
||||
"emoji": "🗺️",
|
||||
"title": "Update Plan",
|
||||
"detailKeys": [
|
||||
"explanation",
|
||||
"plan.0.step"
|
||||
]
|
||||
},
|
||||
"gateway": {
|
||||
"emoji": "🔌",
|
||||
"title": "Gateway",
|
||||
|
||||
@@ -1327,236 +1327,6 @@ public struct SessionsResolveParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionCompactionCheckpoint: Codable, Sendable {
|
||||
public let checkpointid: String
|
||||
public let sessionkey: String
|
||||
public let sessionid: String
|
||||
public let createdat: Int
|
||||
public let reason: AnyCodable
|
||||
public let tokensbefore: Int?
|
||||
public let tokensafter: Int?
|
||||
public let summary: String?
|
||||
public let firstkeptentryid: String?
|
||||
public let precompaction: [String: AnyCodable]
|
||||
public let postcompaction: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
checkpointid: String,
|
||||
sessionkey: String,
|
||||
sessionid: String,
|
||||
createdat: Int,
|
||||
reason: AnyCodable,
|
||||
tokensbefore: Int?,
|
||||
tokensafter: Int?,
|
||||
summary: String?,
|
||||
firstkeptentryid: String?,
|
||||
precompaction: [String: AnyCodable],
|
||||
postcompaction: [String: AnyCodable])
|
||||
{
|
||||
self.checkpointid = checkpointid
|
||||
self.sessionkey = sessionkey
|
||||
self.sessionid = sessionid
|
||||
self.createdat = createdat
|
||||
self.reason = reason
|
||||
self.tokensbefore = tokensbefore
|
||||
self.tokensafter = tokensafter
|
||||
self.summary = summary
|
||||
self.firstkeptentryid = firstkeptentryid
|
||||
self.precompaction = precompaction
|
||||
self.postcompaction = postcompaction
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case checkpointid = "checkpointId"
|
||||
case sessionkey = "sessionKey"
|
||||
case sessionid = "sessionId"
|
||||
case createdat = "createdAt"
|
||||
case reason
|
||||
case tokensbefore = "tokensBefore"
|
||||
case tokensafter = "tokensAfter"
|
||||
case summary
|
||||
case firstkeptentryid = "firstKeptEntryId"
|
||||
case precompaction = "preCompaction"
|
||||
case postcompaction = "postCompaction"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionListParams: Codable, Sendable {
|
||||
public let key: String
|
||||
|
||||
public init(
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionGetParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionBranchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionRestoreParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionListResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let key: String
|
||||
public let checkpoints: [SessionCompactionCheckpoint]
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
key: String,
|
||||
checkpoints: [SessionCompactionCheckpoint])
|
||||
{
|
||||
self.ok = ok
|
||||
self.key = key
|
||||
self.checkpoints = checkpoints
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case key
|
||||
case checkpoints
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionGetResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let key: String
|
||||
public let checkpoint: SessionCompactionCheckpoint
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
key: String,
|
||||
checkpoint: SessionCompactionCheckpoint)
|
||||
{
|
||||
self.ok = ok
|
||||
self.key = key
|
||||
self.checkpoint = checkpoint
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case key
|
||||
case checkpoint
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionBranchResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let sourcekey: String
|
||||
public let key: String
|
||||
public let sessionid: String
|
||||
public let checkpoint: SessionCompactionCheckpoint
|
||||
public let entry: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
sourcekey: String,
|
||||
key: String,
|
||||
sessionid: String,
|
||||
checkpoint: SessionCompactionCheckpoint,
|
||||
entry: [String: AnyCodable])
|
||||
{
|
||||
self.ok = ok
|
||||
self.sourcekey = sourcekey
|
||||
self.key = key
|
||||
self.sessionid = sessionid
|
||||
self.checkpoint = checkpoint
|
||||
self.entry = entry
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case sourcekey = "sourceKey"
|
||||
case key
|
||||
case sessionid = "sessionId"
|
||||
case checkpoint
|
||||
case entry
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionRestoreResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let key: String
|
||||
public let sessionid: String
|
||||
public let checkpoint: SessionCompactionCheckpoint
|
||||
public let entry: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
key: String,
|
||||
sessionid: String,
|
||||
checkpoint: SessionCompactionCheckpoint,
|
||||
entry: [String: AnyCodable])
|
||||
{
|
||||
self.ok = ok
|
||||
self.key = key
|
||||
self.sessionid = sessionid
|
||||
self.checkpoint = checkpoint
|
||||
self.entry = entry
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case key
|
||||
case sessionid = "sessionId"
|
||||
case checkpoint
|
||||
case entry
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCreateParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let agentid: String?
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
64ff922efc6146d867f3858141772094a8a72cba99a8fd61878551175dd8c822 config-baseline.json
|
||||
5d0ce975352ff2b03077f6d71e9fe99ab0f0b118da0f72d47dc989c83f13d668 config-baseline.core.json
|
||||
d22f4414b79ee03d896e58d875c80523bcc12303cbacb1700261e6ec73945187 config-baseline.channel.json
|
||||
1891bcb68d80ab8b7546a2946b5a9d82b18c3e92ffd2c834d15928e73fa11564 config-baseline.plugin.json
|
||||
1c74540dd152c55dbda3e5dee1e37008ee3e6aabb0608e571292832c7a1c012c config-baseline.json
|
||||
7e30316f2326b7d07b71d7b8a96049a74b81428921299b5c4b5aa3d080e03305 config-baseline.core.json
|
||||
66edc86a9d16db1b9e9e7dd99b7032e2d9bcfb9ff210256a21f4b4f088cb3dc1 config-baseline.channel.json
|
||||
d6ebc4948499b997c4a3727cf31849d4a598de9f1a4c197417dcc0b0ec1b734f config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
2375f50cfc8f29df7de7ec84d72ea654ef6eb203204f295fc287a11d63ed2cdb plugin-sdk-api-baseline.json
|
||||
103ee7cf995fe6a0a3d109ad4bb785d342fa1eba83e3295dee3a5ee0bb315b4a plugin-sdk-api-baseline.jsonl
|
||||
08615a28ed3deb20a96c9cd8fd7237a4cbb209ceec93dca03b543979304459e4 plugin-sdk-api-baseline.json
|
||||
683c1249dc15529d8e79bc75e9c00484551cb74126befee507fffcf786e01833 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -227,10 +227,7 @@ Quick mental model (evaluation order for group messages):
|
||||
|
||||
Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`.
|
||||
|
||||
Replying to a bot message counts as an implicit mention when the channel
|
||||
supports reply metadata. Quoting a bot message can also count as an implicit
|
||||
mention on channels that expose quote metadata. Current built-in cases include
|
||||
Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser.
|
||||
Replying to a bot message counts as an implicit mention (when the channel supports reply metadata). This applies to Telegram, WhatsApp, Slack, Discord, and Microsoft Teams.
|
||||
|
||||
```json5
|
||||
{
|
||||
|
||||
@@ -61,17 +61,13 @@ What the Matrix wizard actually asks for:
|
||||
- optional device name
|
||||
- whether to enable E2EE
|
||||
- whether to configure Matrix room access now
|
||||
- whether to configure Matrix invite auto-join now
|
||||
- when invite auto-join is enabled, whether it should be `allowlist`, `always`, or `off`
|
||||
|
||||
Wizard behavior that matters:
|
||||
|
||||
- If Matrix auth env vars already exist for the selected account, and that account does not already have auth saved in config, the wizard offers an env shortcut so setup can keep auth in env vars instead of copying secrets into config.
|
||||
- If Matrix auth env vars already exist for the selected account, and that account does not already have auth saved in config, the wizard offers an env shortcut and only writes `enabled: true` for that account.
|
||||
- When you add another Matrix account interactively, the entered account name is normalized into the account ID used in config and env vars. For example, `Ops Bot` becomes `ops-bot`.
|
||||
- DM allowlist prompts accept full `@user:server` values immediately. Display names only work when live directory lookup finds one exact match; otherwise the wizard asks you to retry with a full Matrix ID.
|
||||
- Room allowlist prompts accept room IDs and aliases directly. They can also resolve joined-room names live, but unresolved names are only kept as typed during setup and are ignored later by runtime allowlist resolution. Prefer `!room:server` or `#alias:server`.
|
||||
- The wizard now shows an explicit warning before the invite auto-join step because `channels.matrix.autoJoin` defaults to `off`; agents will not join invited rooms or fresh DM-style invites unless you set it.
|
||||
- In invite auto-join allowlist mode, use only stable invite targets: `!roomId:server`, `#alias:server`, or `*`. Plain room names are rejected.
|
||||
- Runtime room/session identity uses the stable Matrix room ID. Room-declared aliases are only used as lookup inputs, not as the long-term session key or stable group identity.
|
||||
- To resolve room names before saving them, use `openclaw channels resolve --channel matrix "Project Room"`.
|
||||
|
||||
@@ -81,8 +77,6 @@ Wizard behavior that matters:
|
||||
If you leave it unset, the bot will not join invited rooms or fresh DM-style invites, so it will not appear in new groups or invited DMs unless you join manually first.
|
||||
|
||||
Set `autoJoin: "allowlist"` together with `autoJoinAllowlist` to restrict which invites it accepts, or set `autoJoin: "always"` if you want it to join every invite.
|
||||
|
||||
In `allowlist` mode, `autoJoinAllowlist` only accepts `!roomId:server`, `#alias:server`, or `*`.
|
||||
</Warning>
|
||||
|
||||
Allowlist example:
|
||||
|
||||
@@ -399,7 +399,7 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
|
||||
|
||||
- explicit app mention (`<@botId>`)
|
||||
- mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`)
|
||||
- implicit reply-to-bot thread behavior (disabled when `thread.requireExplicitMention` is `true`)
|
||||
- implicit reply-to-bot thread behavior
|
||||
|
||||
Per-channel controls (`channels.slack.channels.<id>`; names only via startup resolution or `dangerouslyAllowNameMatching`):
|
||||
|
||||
@@ -423,7 +423,6 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
|
||||
- Thread replies can create thread session suffixes (`:thread:<threadTs>`) when applicable.
|
||||
- `channels.slack.thread.historyScope` default is `thread`; `thread.inheritParent` default is `false`.
|
||||
- `channels.slack.thread.initialHistoryLimit` controls how many existing thread messages are fetched when a new thread session starts (default `20`; set `0` to disable).
|
||||
- `channels.slack.thread.requireExplicitMention` (default `false`): when `true`, suppress implicit thread mentions so the bot only responds to explicit `@bot` mentions inside threads, even when the bot already participated in the thread. Without this, replies in a bot-participated thread bypass `requireMention` gating.
|
||||
|
||||
Reply threading controls:
|
||||
|
||||
|
||||
@@ -124,7 +124,6 @@ Example:
|
||||
- `channels.zalouser.groups.<group>.requireMention` controls whether group replies require a mention.
|
||||
- Resolution order: exact group id/name -> normalized group slug -> `*` -> default (`true`).
|
||||
- This applies both to allowlisted groups and open group mode.
|
||||
- Quoting a bot message counts as an implicit mention for group activation.
|
||||
- Authorized control commands (for example `/new`) can bypass mention gating.
|
||||
- When a group message is skipped because mention is required, OpenClaw stores it as pending group history and includes it on the next processed group message.
|
||||
- Group history limit defaults to `messages.groupChat.historyLimit` (fallback `50`). You can override per account with `channels.zalouser.historyLimit`.
|
||||
|
||||
116
docs/cli/capability.md
Normal file
116
docs/cli/capability.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
summary: "Capability-first CLI for provider-backed model, media, web, and embedding workflows"
|
||||
read_when:
|
||||
- Adding or modifying `openclaw capability` commands
|
||||
- Designing stable headless capability automation
|
||||
title: "Capability CLI"
|
||||
---
|
||||
|
||||
# Capability CLI
|
||||
|
||||
`openclaw capability` is the canonical headless surface for provider-backed capabilities.
|
||||
|
||||
It intentionally exposes capability families, not raw gateway RPC names and not raw agent tool ids.
|
||||
|
||||
## Command tree
|
||||
|
||||
```text
|
||||
openclaw capability
|
||||
list
|
||||
inspect
|
||||
|
||||
model
|
||||
run
|
||||
list
|
||||
inspect
|
||||
providers
|
||||
auth login
|
||||
auth logout
|
||||
auth status
|
||||
|
||||
media
|
||||
image
|
||||
generate
|
||||
edit
|
||||
describe
|
||||
describe-many
|
||||
providers
|
||||
audio
|
||||
transcribe
|
||||
providers
|
||||
tts
|
||||
convert
|
||||
voices
|
||||
providers
|
||||
status
|
||||
enable
|
||||
disable
|
||||
set-provider
|
||||
video
|
||||
generate
|
||||
describe
|
||||
providers
|
||||
|
||||
web
|
||||
search
|
||||
fetch
|
||||
providers
|
||||
|
||||
memory
|
||||
embedding
|
||||
create
|
||||
providers
|
||||
```
|
||||
|
||||
## Transport
|
||||
|
||||
Supported transport flags:
|
||||
|
||||
- `--local`
|
||||
- `--gateway`
|
||||
|
||||
Default transport is implicit auto at the command-family level:
|
||||
|
||||
- Stateless execution commands default to local.
|
||||
- Gateway-managed state commands default to gateway.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
openclaw capability model run --prompt "hello" --json
|
||||
openclaw capability media image generate --prompt "friendly lobster" --json
|
||||
openclaw capability media tts status --json
|
||||
openclaw capability embedding create --text "hello world" --json
|
||||
```
|
||||
|
||||
## JSON output
|
||||
|
||||
Capability commands normalize JSON output under a shared envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"capability": "media.image.generate",
|
||||
"transport": "local",
|
||||
"provider": "openai",
|
||||
"model": "gpt-image-1",
|
||||
"attempts": [],
|
||||
"outputs": []
|
||||
}
|
||||
```
|
||||
|
||||
Top-level fields are stable:
|
||||
|
||||
- `ok`
|
||||
- `capability`
|
||||
- `transport`
|
||||
- `provider`
|
||||
- `model`
|
||||
- `attempts`
|
||||
- `outputs`
|
||||
- `error`
|
||||
|
||||
## Notes
|
||||
|
||||
- `model run` reuses the agent runtime so provider/model overrides behave like normal agent execution.
|
||||
- `media tts status` defaults to gateway because it reflects gateway-managed TTS state.
|
||||
@@ -35,6 +35,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
- [`logs`](/cli/logs)
|
||||
- [`system`](/cli/system)
|
||||
- [`models`](/cli/models)
|
||||
- [`capability`](/cli/capability)
|
||||
- [`memory`](/cli/memory)
|
||||
- [`directory`](/cli/directory)
|
||||
- [`nodes`](/cli/nodes)
|
||||
@@ -248,6 +249,16 @@ openclaw [--dev] [--profile <name>] <command>
|
||||
fallbacks list|add|remove|clear
|
||||
image-fallbacks list|add|remove|clear
|
||||
scan
|
||||
capability
|
||||
list
|
||||
inspect
|
||||
model run|list|inspect|providers|auth login|logout|status
|
||||
media image generate|edit|describe|describe-many|providers
|
||||
media audio transcribe|providers
|
||||
media tts convert|voices|providers|status|enable|disable|set-provider
|
||||
media video generate|describe|providers
|
||||
web search|fetch|providers
|
||||
embedding create|providers
|
||||
auth add|login|login-github-copilot|setup-token|paste-token
|
||||
auth order get|set|clear
|
||||
sandbox
|
||||
|
||||
@@ -47,7 +47,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
||||
### Unit / integration (default)
|
||||
|
||||
- Command: `pnpm test`
|
||||
- Config: ten sequential shard runs (`vitest.full-*.config.ts`) over the existing scoped Vitest projects
|
||||
- Config: five sequential shard runs (`vitest.full-*.config.ts`) over the existing scoped Vitest projects
|
||||
- Files: core/unit inventories under `src/**/*.test.ts`, `packages/**/*.test.ts`, `test/**/*.test.ts`, and the whitelisted `ui` node tests covered by `vitest.unit.config.ts`
|
||||
- Scope:
|
||||
- Pure unit tests
|
||||
@@ -58,7 +58,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
||||
- No real keys required
|
||||
- Should be fast and stable
|
||||
- Projects note:
|
||||
- Untargeted `pnpm test` now runs ten smaller shard configs (`core-unit-src`, `core-unit-security`, `core-unit-ui`, `core-unit-support`, `core-contracts`, `core-bundled`, `core-runtime`, `agentic`, `auto-reply`, `extensions`) instead of one giant native root-project process. This cuts peak RSS on loaded machines and avoids auto-reply/extension work starving unrelated suites.
|
||||
- Untargeted `pnpm test` now runs eight smaller shard configs (`core-unit-src`, `core-unit-security`, `core-unit-support`, `core-contracts`, `core-runtime`, `agentic`, `auto-reply`, `extensions`) instead of one giant native root-project process. This cuts peak RSS on loaded machines and avoids auto-reply/extension work starving unrelated suites.
|
||||
- `pnpm test --watch` still uses the native root `vitest.config.ts` project graph, because a multi-shard watch loop is not practical.
|
||||
- `pnpm test`, `pnpm test:watch`, and `pnpm test:perf:imports` route explicit file/directory targets through scoped lanes first, so `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` avoids paying the full root project startup tax.
|
||||
- `pnpm test:changed` expands changed git paths into the same scoped lanes when the diff only touches routable source/test files; config/setup edits still fall back to the broad root-project rerun.
|
||||
@@ -305,15 +305,12 @@ Notes:
|
||||
- `pnpm test:live src/gateway/gateway-acp-bind.live.test.ts`
|
||||
- `OPENCLAW_LIVE_ACP_BIND=1`
|
||||
- Defaults:
|
||||
- ACP agents in Docker: `claude,codex,gemini`
|
||||
- ACP agent for direct `pnpm test:live ...`: `claude`
|
||||
- ACP agent: `claude`
|
||||
- Synthetic channel: Slack DM-style conversation context
|
||||
- ACP backend: `acpx`
|
||||
- Overrides:
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=claude`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=codex`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=gemini`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND='npx -y @agentclientprotocol/claude-agent-acp@<version>'`
|
||||
- Notes:
|
||||
- This lane uses the gateway `chat.send` surface with admin-only synthetic originating-route fields so tests can attach message-channel context without pretending to deliver externally.
|
||||
@@ -333,20 +330,10 @@ Docker recipe:
|
||||
pnpm test:docker:live-acp-bind
|
||||
```
|
||||
|
||||
Single-agent Docker recipes:
|
||||
|
||||
```bash
|
||||
pnpm test:docker:live-acp-bind:claude
|
||||
pnpm test:docker:live-acp-bind:codex
|
||||
pnpm test:docker:live-acp-bind:gemini
|
||||
```
|
||||
|
||||
Docker notes:
|
||||
|
||||
- The Docker runner lives at `scripts/test-live-acp-bind-docker.sh`.
|
||||
- By default, it runs the ACP bind smoke against all supported live CLI agents in sequence: `claude`, `codex`, then `gemini`.
|
||||
- Use `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=codex`, or `OPENCLAW_LIVE_ACP_BIND_AGENTS=gemini` to narrow the matrix.
|
||||
- It sources `~/.profile`, stages the matching CLI auth material into the container, installs `acpx` into a writable npm prefix, then installs the requested live CLI (`@anthropic-ai/claude-code`, `@openai/codex`, or `@google/gemini-cli`) if missing.
|
||||
- It sources `~/.profile`, stages the matching CLI auth material into the container, installs `acpx` into a writable npm prefix, then installs the requested live CLI (`@anthropic-ai/claude-code` or `@openai/codex`) if missing.
|
||||
- Inside Docker, the runner sets `OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND=$HOME/.npm-global/bin/acpx` so acpx keeps provider env vars from the sourced profile available to the child harness CLI.
|
||||
|
||||
### Recommended live recipes
|
||||
|
||||
@@ -1120,8 +1120,7 @@ authoring plugins:
|
||||
`openclaw/plugin-sdk/secret-input`, and
|
||||
`openclaw/plugin-sdk/webhook-ingress` for shared setup/auth/reply/webhook
|
||||
wiring. `channel-inbound` is the shared home for debounce, mention matching,
|
||||
inbound mention-policy helpers, envelope formatting, and inbound envelope
|
||||
context helpers.
|
||||
envelope formatting, and inbound envelope context helpers.
|
||||
`channel-setup` is the narrow optional-install setup seam.
|
||||
`setup-runtime` is the runtime-safe setup surface used by `setupEntry` /
|
||||
deferred startup, including the import-safe setup patch adapters.
|
||||
|
||||
@@ -152,87 +152,6 @@ surfaces:
|
||||
|
||||
Auth-only channels can usually stop at the default path: core handles approvals and the plugin just exposes outbound/auth capabilities. Native approval channels such as Matrix, Slack, Telegram, and custom chat transports should use the shared native helpers instead of rolling their own approval lifecycle.
|
||||
|
||||
## Inbound mention policy
|
||||
|
||||
Keep inbound mention handling split in two layers:
|
||||
|
||||
- plugin-owned evidence gathering
|
||||
- shared policy evaluation
|
||||
|
||||
Use `openclaw/plugin-sdk/channel-inbound` for the shared layer.
|
||||
|
||||
Good fit for plugin-local logic:
|
||||
|
||||
- reply-to-bot detection
|
||||
- quoted-bot detection
|
||||
- thread-participation checks
|
||||
- service/system-message exclusions
|
||||
- platform-native caches needed to prove bot participation
|
||||
|
||||
Good fit for the shared helper:
|
||||
|
||||
- `requireMention`
|
||||
- explicit mention result
|
||||
- implicit mention allowlist
|
||||
- command bypass
|
||||
- final skip decision
|
||||
|
||||
Preferred flow:
|
||||
|
||||
1. Compute local mention facts.
|
||||
2. Pass those facts into `resolveInboundMentionDecision({ facts, policy })`.
|
||||
3. Use `decision.effectiveWasMentioned`, `decision.shouldBypassMention`, and `decision.shouldSkip` in your inbound gate.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
implicitMentionKindWhen,
|
||||
matchesMentionWithExplicit,
|
||||
resolveInboundMentionDecision,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
|
||||
const mentionMatch = matchesMentionWithExplicit(text, {
|
||||
mentionRegexes,
|
||||
mentionPatterns,
|
||||
});
|
||||
|
||||
const facts = {
|
||||
canDetectMention: true,
|
||||
wasMentioned: mentionMatch.matched,
|
||||
hasAnyMention: mentionMatch.hasExplicitMention,
|
||||
implicitMentionKinds: [
|
||||
...implicitMentionKindWhen("reply_to_bot", isReplyToBot),
|
||||
...implicitMentionKindWhen("quoted_bot", isQuoteOfBot),
|
||||
],
|
||||
};
|
||||
|
||||
const decision = resolveInboundMentionDecision({
|
||||
facts,
|
||||
policy: {
|
||||
isGroup,
|
||||
requireMention,
|
||||
allowedImplicitMentionKinds: requireExplicitMention ? [] : ["reply_to_bot", "quoted_bot"],
|
||||
allowTextCommands,
|
||||
hasControlCommand,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
|
||||
if (decision.shouldSkip) return;
|
||||
```
|
||||
|
||||
`api.runtime.channel.mentions` exposes the same shared mention helpers for
|
||||
bundled channel plugins that already depend on runtime injection:
|
||||
|
||||
- `buildMentionRegexes`
|
||||
- `matchesMentionPatterns`
|
||||
- `matchesMentionWithExplicit`
|
||||
- `implicitMentionKindWhen`
|
||||
- `resolveInboundMentionDecision`
|
||||
|
||||
The older `resolveMentionGating*` helpers remain on
|
||||
`openclaw/plugin-sdk/channel-inbound` as compatibility exports only. New code
|
||||
should use `resolveInboundMentionDecision({ facts, policy })`.
|
||||
|
||||
## Walkthrough
|
||||
|
||||
<Steps>
|
||||
|
||||
@@ -249,8 +249,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/provider-onboard` | Provider onboarding patches | Onboarding config helpers |
|
||||
| `plugin-sdk/provider-http` | Provider HTTP helpers | Generic provider HTTP/endpoint capability helpers |
|
||||
| `plugin-sdk/provider-web-fetch` | Provider web-fetch helpers | Web-fetch provider registration/cache helpers |
|
||||
| `plugin-sdk/provider-web-search-contract` | Provider web-search contract helpers | Narrow web-search config/credential contract helpers such as `enablePluginInConfig`, `resolveProviderWebSearchPluginConfig`, and scoped credential setters/getters |
|
||||
| `plugin-sdk/provider-web-search` | Provider web-search helpers | Web-search provider registration/cache/runtime helpers |
|
||||
| `plugin-sdk/provider-web-search` | Provider web-search helpers | Web-search provider registration/cache/config helpers |
|
||||
| `plugin-sdk/provider-tools` | Provider tool/schema compat helpers | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, Gemini schema cleanup + diagnostics, and xAI compat helpers such as `resolveXaiModelCompatPatch` / `applyXaiModelCompat` |
|
||||
| `plugin-sdk/provider-usage` | Provider usage helpers | `fetchClaudeUsage`, `fetchGeminiUsage`, `fetchGithubCopilotUsage`, and other provider usage helpers |
|
||||
| `plugin-sdk/provider-stream` | Provider stream wrapper helpers | `ProviderStreamFamily`, `buildProviderStreamFamilyHooks`, `composeProviderStreamWrappers`, stream wrapper types, and shared Anthropic/Bedrock/Google/Kilocode/Moonshot/OpenAI/OpenRouter/Z.A.I/MiniMax/Copilot wrapper helpers |
|
||||
|
||||
@@ -108,7 +108,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/group-access` | Shared group-access decision helpers |
|
||||
| `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers |
|
||||
| `plugin-sdk/interactive-runtime` | Interactive reply payload normalization/reduction helpers |
|
||||
| `plugin-sdk/channel-inbound` | Inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
|
||||
| `plugin-sdk/channel-inbound` | Debounce, mention matching, envelope helpers |
|
||||
| `plugin-sdk/channel-send-result` | Reply result types |
|
||||
| `plugin-sdk/channel-actions` | `createMessageToolButtonsSchema`, `createMessageToolCardSchema` |
|
||||
| `plugin-sdk/channel-targets` | Target parsing/matching helpers |
|
||||
@@ -134,8 +134,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/provider-catalog-shared` | `findCatalogTemplate`, `buildSingleProviderApiKeyCatalog`, `supportsNativeStreamingUsageCompat`, `applyProviderNativeStreamingUsageCompat` |
|
||||
| `plugin-sdk/provider-http` | Generic provider HTTP/endpoint capability helpers |
|
||||
| `plugin-sdk/provider-web-fetch` | Web-fetch provider registration/cache helpers |
|
||||
| `plugin-sdk/provider-web-search-contract` | Narrow web-search config/credential contract helpers such as `enablePluginInConfig`, `resolveProviderWebSearchPluginConfig`, and scoped credential setters/getters |
|
||||
| `plugin-sdk/provider-web-search` | Web-search provider registration/cache/runtime helpers |
|
||||
| `plugin-sdk/provider-web-search` | Web-search provider registration/cache/config helpers |
|
||||
| `plugin-sdk/provider-tools` | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, Gemini schema cleanup + diagnostics, and xAI compat helpers such as `resolveXaiModelCompatPatch` / `applyXaiModelCompat` |
|
||||
| `plugin-sdk/provider-usage` | `fetchClaudeUsage` and similar |
|
||||
| `plugin-sdk/provider-stream` | `ProviderStreamFamily`, `buildProviderStreamFamilyHooks`, `composeProviderStreamWrappers`, stream wrapper types, and shared Anthropic/Bedrock/Google/Kilocode/Moonshot/OpenAI/OpenRouter/Z.A.I/MiniMax/Copilot wrapper helpers |
|
||||
@@ -157,7 +156,6 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/command-surface` | Command-body normalization and command-surface helpers |
|
||||
| `plugin-sdk/allow-from` | `formatAllowFromLowercase` |
|
||||
| `plugin-sdk/channel-secret-runtime` | Narrow secret-contract collection helpers for channel/plugin secret surfaces |
|
||||
| `plugin-sdk/secret-ref-runtime` | Narrow `coerceSecretRef` and SecretRef typing helpers for secret-contract/config parsing |
|
||||
| `plugin-sdk/security-runtime` | Shared trust, DM gating, external-content, and secret-collection helpers |
|
||||
| `plugin-sdk/ssrf-policy` | Host allowlist and private-network SSRF policy helpers |
|
||||
| `plugin-sdk/ssrf-runtime` | Pinned-dispatcher, SSRF-guarded fetch, and SSRF policy helpers |
|
||||
|
||||
@@ -330,46 +330,6 @@ api.runtime.tools.registerMemoryCli(/* ... */);
|
||||
|
||||
Channel-specific runtime helpers (available when a channel plugin is loaded).
|
||||
|
||||
`api.runtime.channel.mentions` is the shared inbound mention-policy surface for
|
||||
bundled channel plugins that use runtime injection:
|
||||
|
||||
```typescript
|
||||
const mentionMatch = api.runtime.channel.mentions.matchesMentionWithExplicit(text, {
|
||||
mentionRegexes,
|
||||
mentionPatterns,
|
||||
});
|
||||
|
||||
const decision = api.runtime.channel.mentions.resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: mentionMatch.matched,
|
||||
implicitMentionKinds: api.runtime.channel.mentions.implicitMentionKindWhen(
|
||||
"reply_to_bot",
|
||||
isReplyToBot,
|
||||
),
|
||||
},
|
||||
policy: {
|
||||
isGroup,
|
||||
requireMention,
|
||||
allowTextCommands,
|
||||
hasControlCommand,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Available mention helpers:
|
||||
|
||||
- `buildMentionRegexes`
|
||||
- `matchesMentionPatterns`
|
||||
- `matchesMentionWithExplicit`
|
||||
- `implicitMentionKindWhen`
|
||||
- `resolveInboundMentionDecision`
|
||||
|
||||
`api.runtime.channel.mentions` intentionally does not expose the older
|
||||
`resolveMentionGating*` compatibility helpers. Prefer the normalized
|
||||
`{ facts, policy }` path.
|
||||
|
||||
## Storing runtime references
|
||||
|
||||
Use `createPluginRuntimeStore` to store the runtime reference for use outside
|
||||
|
||||
@@ -13,7 +13,7 @@ title: "Tests"
|
||||
- `pnpm test:coverage`: Runs the unit suite with V8 coverage (via `vitest.unit.config.ts`). Global thresholds are 70% lines/branches/functions/statements. Coverage excludes integration-heavy entrypoints (CLI wiring, gateway/telegram bridges, webchat static server) to keep the target focused on unit-testable logic.
|
||||
- `pnpm test:coverage:changed`: Runs unit coverage only for files changed since `origin/main`.
|
||||
- `pnpm test:changed`: expands changed git paths into scoped Vitest lanes when the diff only touches routable source/test files. Config/setup changes still fall back to the native root projects run so wiring edits rerun broadly when needed.
|
||||
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs now execute ten sequential shard configs (`vitest.full-core-unit-src.config.ts`, `vitest.full-core-unit-security.config.ts`, `vitest.full-core-unit-ui.config.ts`, `vitest.full-core-unit-support.config.ts`, `vitest.full-core-contracts.config.ts`, `vitest.full-core-bundled.config.ts`, `vitest.full-core-runtime.config.ts`, `vitest.full-agentic.config.ts`, `vitest.full-auto-reply.config.ts`, `vitest.full-extensions.config.ts`) instead of one giant root-project process.
|
||||
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs now execute eight sequential shard configs (`vitest.full-core-unit-src.config.ts`, `vitest.full-core-unit-security.config.ts`, `vitest.full-core-unit-support.config.ts`, `vitest.full-core-contracts.config.ts`, `vitest.full-core-runtime.config.ts`, `vitest.full-agentic.config.ts`, `vitest.full-auto-reply.config.ts`, `vitest.full-extensions.config.ts`) instead of one giant root-project process.
|
||||
- Selected `plugin-sdk` and `commands` test files now route through dedicated light lanes that keep only `test/setup.ts`, leaving runtime-heavy cases on their existing lanes.
|
||||
- Selected `plugin-sdk` and `commands` helper source files also map `pnpm test:changed` to explicit sibling tests in those light lanes, so small helper edits avoid rerunning the heavy runtime-backed suites.
|
||||
- `auto-reply` now also splits into three dedicated configs (`core`, `top-level`, `reply`) so the reply harness does not dominate the lighter top-level status/token/helper tests.
|
||||
|
||||
@@ -68,9 +68,7 @@ Use `action: "list"` to inspect available providers and models at runtime:
|
||||
| `count` | number | Number of images to generate (1–4) |
|
||||
| `filename` | string | Output filename hint |
|
||||
|
||||
Not all providers support all parameters. When a fallback provider supports a nearby geometry option instead of the exact requested one, OpenClaw remaps to the closest supported size, aspect ratio, or resolution before submission. Truly unsupported overrides are still reported in the tool result.
|
||||
|
||||
Tool results report the applied settings. When OpenClaw remaps geometry during provider fallback, the returned `size`, `aspectRatio`, and `resolution` values reflect what was actually sent, and `details.normalization` captures the requested-to-applied translation.
|
||||
Not all providers support all parameters. The tool passes what each provider supports, ignores the rest, and reports dropped overrides in the tool result.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -106,10 +104,6 @@ Notes:
|
||||
|
||||
- Auto-detection is auth-aware. A provider default only enters the candidate list
|
||||
when OpenClaw can actually authenticate that provider.
|
||||
- Auto-detection is enabled by default. Set
|
||||
`agents.defaults.mediaGenerationAutoProviderFallback: false` if you want image
|
||||
generation to use only the explicit `model`, `primary`, and `fallbacks`
|
||||
entries.
|
||||
- Use `action: "list"` to inspect the currently registered providers, their
|
||||
default models, and auth env-var hints.
|
||||
|
||||
|
||||
@@ -131,12 +131,8 @@ Direct generation example:
|
||||
| `filename` | string | Output filename hint |
|
||||
|
||||
Not all providers support all parameters. OpenClaw still validates hard limits
|
||||
such as input counts before submission. When a provider supports duration but
|
||||
uses a shorter maximum than the requested value, OpenClaw automatically clamps
|
||||
to the closest supported duration. Truly unsupported optional hints are ignored
|
||||
with a warning when the selected provider or model cannot honor them.
|
||||
|
||||
Tool results report the applied settings. When OpenClaw clamps duration during provider fallback, the returned `durationSeconds` reflects the submitted value and `details.normalization.durationSeconds` shows the requested-to-applied mapping.
|
||||
such as input counts before submission, but unsupported optional hints are
|
||||
ignored with a warning when the selected provider or model cannot honor them.
|
||||
|
||||
## Async behavior for the shared provider-backed path
|
||||
|
||||
@@ -198,10 +194,6 @@ When generating music, OpenClaw tries providers in this order:
|
||||
If a provider fails, the next candidate is tried automatically. If all fail, the
|
||||
error includes details from each attempt.
|
||||
|
||||
Set `agents.defaults.mediaGenerationAutoProviderFallback: false` if you want
|
||||
music generation to use only the explicit `model`, `primary`, and `fallbacks`
|
||||
entries.
|
||||
|
||||
## Provider notes
|
||||
|
||||
- Google uses Lyria 3 batch generation. The current bundled flow supports
|
||||
|
||||
@@ -154,9 +154,7 @@ and the shared live sweep.
|
||||
| `model` | string | Provider/model override (e.g. `runway/gen4.5`) |
|
||||
| `filename` | string | Output filename hint |
|
||||
|
||||
Not all providers support all parameters. OpenClaw already normalizes duration to the closest provider-supported value, and it also remaps translated geometry hints such as size-to-aspect-ratio when a fallback provider exposes a different control surface. Truly unsupported overrides are ignored on a best-effort basis and reported as warnings in the tool result. Hard capability limits (such as too many reference inputs) fail before submission.
|
||||
|
||||
Tool results report the applied settings. When OpenClaw remaps duration or geometry during provider fallback, the returned `durationSeconds`, `size`, `aspectRatio`, and `resolution` values reflect what was submitted, and `details.normalization` captures the requested-to-applied translation.
|
||||
Not all providers support all parameters. Unsupported overrides are ignored on a best-effort basis and reported as warnings in the tool result. Hard capability limits (such as too many reference inputs) fail before submission.
|
||||
|
||||
Reference inputs also select the runtime mode:
|
||||
|
||||
@@ -184,10 +182,6 @@ When generating a video, OpenClaw resolves the model in this order:
|
||||
|
||||
If a provider fails, the next candidate is tried automatically. If all candidates fail, the error includes details from each attempt.
|
||||
|
||||
Set `agents.defaults.mediaGenerationAutoProviderFallback: false` if you want
|
||||
video generation to use only the explicit `model`, `primary`, and `fallbacks`
|
||||
entries.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "OpenClaw ACP runtime backend",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"acpx": "0.5.1"
|
||||
"acpx": "0.5.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
||||
56
extensions/acpx/src/acpx-runtime-compat.d.ts
vendored
56
extensions/acpx/src/acpx-runtime-compat.d.ts
vendored
@@ -1,56 +0,0 @@
|
||||
declare module "acpx/runtime" {
|
||||
export const ACPX_BACKEND_ID: string;
|
||||
|
||||
export type AcpRuntimeDoctorReport = import("../runtime-api.js").AcpRuntimeDoctorReport;
|
||||
export type AcpRuntimeEnsureInput = import("../runtime-api.js").AcpRuntimeEnsureInput;
|
||||
export type AcpRuntimeEvent = import("../runtime-api.js").AcpRuntimeEvent;
|
||||
export type AcpRuntimeHandle = import("../runtime-api.js").AcpRuntimeHandle;
|
||||
export type AcpRuntimeCapabilities = import("../runtime-api.js").AcpRuntimeCapabilities;
|
||||
export type AcpRuntimeStatus = import("../runtime-api.js").AcpRuntimeStatus;
|
||||
export type AcpRuntimeTurnInput = import("../runtime-api.js").AcpRuntimeTurnInput;
|
||||
|
||||
export type AcpAgentRegistry = {
|
||||
resolve(agent: string): string | undefined;
|
||||
list(): string[];
|
||||
};
|
||||
|
||||
export type AcpSessionRecord = Record<string, unknown>;
|
||||
|
||||
export type AcpSessionStore = {
|
||||
load(sessionId: string): Promise<AcpSessionRecord | undefined>;
|
||||
save(record: AcpSessionRecord): Promise<void>;
|
||||
};
|
||||
|
||||
export type AcpRuntimeOptions = {
|
||||
cwd: string;
|
||||
sessionStore: AcpSessionStore;
|
||||
agentRegistry: AcpAgentRegistry;
|
||||
mcpServers?: unknown;
|
||||
permissionMode?: unknown;
|
||||
nonInteractivePermissions?: unknown;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export class AcpxRuntime {
|
||||
constructor(options: AcpRuntimeOptions, testOptions?: unknown);
|
||||
isHealthy(): boolean;
|
||||
probeAvailability(): Promise<void>;
|
||||
doctor(): Promise<AcpRuntimeDoctorReport>;
|
||||
ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
|
||||
runTurn(input: AcpRuntimeTurnInput): AsyncIterable<AcpRuntimeEvent>;
|
||||
getCapabilities(input?: {
|
||||
handle?: AcpRuntimeHandle;
|
||||
}): AcpRuntimeCapabilities | Promise<AcpRuntimeCapabilities>;
|
||||
getStatus(input: { handle: AcpRuntimeHandle; signal?: AbortSignal }): Promise<AcpRuntimeStatus>;
|
||||
setMode(input: { handle: AcpRuntimeHandle; mode: string }): Promise<void>;
|
||||
setConfigOption(input: { handle: AcpRuntimeHandle; key: string; value: string }): Promise<void>;
|
||||
cancel(input: { handle: AcpRuntimeHandle; reason?: string }): Promise<void>;
|
||||
close(input: { handle: AcpRuntimeHandle; reason?: string }): Promise<void>;
|
||||
}
|
||||
|
||||
export function createAcpRuntime(...args: unknown[]): AcpxRuntime;
|
||||
export function createAgentRegistry(params: { overrides?: unknown }): AcpAgentRegistry;
|
||||
export function createFileSessionStore(params: { stateDir: string }): AcpSessionStore;
|
||||
export function decodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
|
||||
export function encodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
|
||||
}
|
||||
55
extensions/acpx/src/acpx-runtime.d.ts
vendored
Normal file
55
extensions/acpx/src/acpx-runtime.d.ts
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
declare module "acpx/runtime" {
|
||||
export const ACPX_BACKEND_ID: string;
|
||||
|
||||
export type AcpRuntimeDoctorReport =
|
||||
import("../../../src/acp/runtime/types.js").AcpRuntimeDoctorReport;
|
||||
export type AcpRuntimeEnsureInput =
|
||||
import("../../../src/acp/runtime/types.js").AcpRuntimeEnsureInput;
|
||||
export type AcpRuntimeEvent = import("../../../src/acp/runtime/types.js").AcpRuntimeEvent;
|
||||
export type AcpRuntimeHandle = import("../../../src/acp/runtime/types.js").AcpRuntimeHandle;
|
||||
export type AcpRuntimeTurnInput = import("../../../src/acp/runtime/types.js").AcpRuntimeTurnInput;
|
||||
export type AcpRuntimeStatus = import("../../../src/acp/runtime/types.js").AcpRuntimeStatus;
|
||||
export type AcpRuntimeCapabilities =
|
||||
import("../../../src/acp/runtime/types.js").AcpRuntimeCapabilities;
|
||||
|
||||
export type AcpSessionStore = {
|
||||
load(sessionId: string): Promise<unknown>;
|
||||
save(record: unknown): Promise<void>;
|
||||
};
|
||||
|
||||
export type AcpAgentRegistry = {
|
||||
resolve(agentId: string): string;
|
||||
list(): string[];
|
||||
};
|
||||
|
||||
export type AcpRuntimeOptions = {
|
||||
cwd: string;
|
||||
sessionStore: AcpSessionStore;
|
||||
agentRegistry: AcpAgentRegistry;
|
||||
permissionMode: string;
|
||||
mcpServers?: unknown[];
|
||||
nonInteractivePermissions?: unknown;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export class AcpxRuntime {
|
||||
constructor(options: AcpRuntimeOptions, testOptions?: unknown);
|
||||
isHealthy(): boolean;
|
||||
probeAvailability(): Promise<void>;
|
||||
doctor(): Promise<AcpRuntimeDoctorReport>;
|
||||
ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
|
||||
runTurn(input: AcpRuntimeTurnInput): AsyncIterable<AcpRuntimeEvent>;
|
||||
getCapabilities(input?: { handle?: AcpRuntimeHandle }): AcpRuntimeCapabilities;
|
||||
getStatus(input: { handle: AcpRuntimeHandle; signal?: AbortSignal }): Promise<AcpRuntimeStatus>;
|
||||
setMode(input: { handle: AcpRuntimeHandle; mode: string }): Promise<void>;
|
||||
setConfigOption(input: { handle: AcpRuntimeHandle; key: string; value: string }): Promise<void>;
|
||||
cancel(input: { handle: AcpRuntimeHandle; reason?: string }): Promise<void>;
|
||||
close(input: { handle: AcpRuntimeHandle; reason: string }): Promise<void>;
|
||||
}
|
||||
|
||||
export function createAcpRuntime(...args: unknown[]): unknown;
|
||||
export function createAgentRegistry(...args: unknown[]): AcpAgentRegistry;
|
||||
export function createFileSessionStore(...args: unknown[]): AcpSessionStore;
|
||||
export function decodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
|
||||
export function encodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export function formatErrorMessage(error) {
|
||||
if (error instanceof Error) {
|
||||
return error.message || error.name || "Error";
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { formatErrorMessage } from "./error-format.mjs";
|
||||
import { splitCommandLine } from "./mcp-command-line.mjs";
|
||||
|
||||
function decodePayload(argv) {
|
||||
@@ -95,7 +94,7 @@ function main() {
|
||||
child.stdout.pipe(process.stdout);
|
||||
|
||||
child.on("error", (error) => {
|
||||
process.stderr.write(`${formatErrorMessage(error)}\n`);
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ async function makeTempDir(): Promise<string> {
|
||||
|
||||
afterEach(async () => {
|
||||
runtimeRegistry.clear();
|
||||
delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME;
|
||||
delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE;
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -142,50 +140,4 @@ describe("createAcpxRuntimeService", () => {
|
||||
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
it("can skip the embedded runtime probe via env", async () => {
|
||||
process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE = "1";
|
||||
const workspaceDir = await makeTempDir();
|
||||
const ctx = createServiceContext(workspaceDir);
|
||||
const probeAvailability = vi.fn(async () => {});
|
||||
const service = createAcpxRuntimeService({
|
||||
runtimeFactory: () =>
|
||||
({
|
||||
ensureSession: vi.fn(),
|
||||
runTurn: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
close: vi.fn(),
|
||||
probeAvailability,
|
||||
isHealthy: () => false,
|
||||
doctor: async () => ({ ok: false, message: "nope" }),
|
||||
}) as never,
|
||||
});
|
||||
|
||||
await service.start(ctx);
|
||||
|
||||
expect(probeAvailability).not.toHaveBeenCalled();
|
||||
expect(getAcpRuntimeBackend("acpx")).toBeTruthy();
|
||||
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
it("can skip the embedded runtime backend via env", async () => {
|
||||
process.env.OPENCLAW_SKIP_ACPX_RUNTIME = "1";
|
||||
const workspaceDir = await makeTempDir();
|
||||
const ctx = createServiceContext(workspaceDir);
|
||||
const runtimeFactory = vi.fn(() => {
|
||||
throw new Error("runtime factory should not run when ACPX is skipped");
|
||||
});
|
||||
const service = createAcpxRuntimeService({
|
||||
runtimeFactory: runtimeFactory as never,
|
||||
});
|
||||
|
||||
await service.start(ctx);
|
||||
|
||||
expect(runtimeFactory).not.toHaveBeenCalled();
|
||||
expect(getAcpRuntimeBackend("acpx")).toBeUndefined();
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith(
|
||||
"skipping embedded acpx runtime backend (OPENCLAW_SKIP_ACPX_RUNTIME=1)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import type {
|
||||
AcpRuntime,
|
||||
OpenClawPluginService,
|
||||
@@ -91,11 +90,6 @@ export function createAcpxRuntimeService(
|
||||
return {
|
||||
id: "acpx-runtime",
|
||||
async start(ctx: OpenClawPluginServiceContext): Promise<void> {
|
||||
if (process.env.OPENCLAW_SKIP_ACPX_RUNTIME === "1") {
|
||||
ctx.logger.info("skipping embedded acpx runtime backend (OPENCLAW_SKIP_ACPX_RUNTIME=1)");
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: params.pluginConfig,
|
||||
workspaceDir: ctx.workspaceDir,
|
||||
@@ -119,10 +113,6 @@ export function createAcpxRuntimeService(
|
||||
});
|
||||
ctx.logger.info(`embedded acpx runtime backend registered (cwd: ${pluginConfig.cwd})`);
|
||||
|
||||
if (process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE === "1") {
|
||||
return;
|
||||
}
|
||||
|
||||
lifecycleRevision += 1;
|
||||
const currentRevision = lifecycleRevision;
|
||||
void (async () => {
|
||||
@@ -146,7 +136,9 @@ export function createAcpxRuntimeService(
|
||||
if (currentRevision !== lifecycleRevision) {
|
||||
return;
|
||||
}
|
||||
ctx.logger.warn(`embedded acpx runtime setup failed: ${formatErrorMessage(err)}`);
|
||||
ctx.logger.warn(
|
||||
`embedded acpx runtime setup failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
})();
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/core";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import type {
|
||||
ModelDefinitionConfig,
|
||||
ModelProviderConfig,
|
||||
@@ -103,7 +102,7 @@ export async function generateBearerTokenFromIam(params: {
|
||||
} catch (error) {
|
||||
log.debug?.("Mantle IAM token generation unavailable", {
|
||||
region: params.region,
|
||||
error: formatErrorMessage(error),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
@@ -234,7 +233,7 @@ export async function discoverMantleModels(params: {
|
||||
return models;
|
||||
} catch (error) {
|
||||
log.debug?.("Mantle model discovery error", {
|
||||
error: formatErrorMessage(error),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return cached?.models ?? [];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
const LEGACY_PATH = "models.bedrockDiscovery";
|
||||
const TARGET_PATH = "plugins.entries.amazon-bedrock.config.discovery";
|
||||
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
||||
|
||||
function isRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isBlockedObjectKey(key: string): boolean {
|
||||
return BLOCKED_OBJECT_KEYS.has(key);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type ListInferenceProfilesCommandOutput,
|
||||
} from "@aws-sdk/client-bedrock";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/core";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { resolveAwsSdkEnvVarName } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import type {
|
||||
BedrockDiscoveryConfig,
|
||||
@@ -215,7 +214,7 @@ async function fetchInferenceProfileSummaries(
|
||||
return profiles;
|
||||
} catch (error) {
|
||||
log.debug?.("Skipping inference profile discovery", {
|
||||
error: formatErrorMessage(error),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
@@ -407,7 +406,7 @@ export async function discoverBedrockModels(params: {
|
||||
if (!hasLoggedBedrockError) {
|
||||
hasLoggedBedrockError = true;
|
||||
log.warn("Failed to discover Bedrock models", {
|
||||
error: formatErrorMessage(error),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
return [];
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { capturePluginRegistration } from "openclaw/plugin-sdk/testing";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
|
||||
import {
|
||||
ANTHROPIC_FRONTIER_EXECUTION_BIAS,
|
||||
ANTHROPIC_FRONTIER_INTERACTION_STYLE,
|
||||
ANTHROPIC_FRONTIER_OUTPUT_CONTRACT,
|
||||
ANTHROPIC_FRONTIER_TOOL_CALL_STYLE,
|
||||
} from "./prompt-overlay.js";
|
||||
|
||||
const { readClaudeCliCredentialsForSetupMock, readClaudeCliCredentialsForRuntimeMock } = vi.hoisted(
|
||||
() => ({
|
||||
@@ -53,60 +47,6 @@ describe("anthropic provider replay hooks", () => {
|
||||
).toBe("native");
|
||||
});
|
||||
|
||||
it("registers frontier Claude prompt contributions", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicPlugin);
|
||||
const contribution = {
|
||||
stablePrefix: ANTHROPIC_FRONTIER_OUTPUT_CONTRACT,
|
||||
sectionOverrides: {
|
||||
interaction_style: ANTHROPIC_FRONTIER_INTERACTION_STYLE,
|
||||
tool_call_style: ANTHROPIC_FRONTIER_TOOL_CALL_STYLE,
|
||||
execution_bias: ANTHROPIC_FRONTIER_EXECUTION_BIAS,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
provider.resolveSystemPromptContribution?.({
|
||||
config: undefined,
|
||||
agentDir: undefined,
|
||||
workspaceDir: undefined,
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
promptMode: "full",
|
||||
runtimeChannel: undefined,
|
||||
runtimeCapabilities: undefined,
|
||||
agentId: undefined,
|
||||
} as never),
|
||||
).toEqual(contribution);
|
||||
|
||||
expect(
|
||||
provider.resolveSystemPromptContribution?.({
|
||||
config: undefined,
|
||||
agentDir: undefined,
|
||||
workspaceDir: undefined,
|
||||
provider: "claude-cli",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
promptMode: "full",
|
||||
runtimeChannel: undefined,
|
||||
runtimeCapabilities: undefined,
|
||||
agentId: undefined,
|
||||
} as never),
|
||||
).toEqual(contribution);
|
||||
|
||||
expect(
|
||||
provider.resolveSystemPromptContribution?.({
|
||||
config: undefined,
|
||||
agentDir: undefined,
|
||||
workspaceDir: undefined,
|
||||
provider: "anthropic",
|
||||
modelId: "claude-haiku-4-5",
|
||||
promptMode: "full",
|
||||
runtimeChannel: undefined,
|
||||
runtimeCapabilities: undefined,
|
||||
agentId: undefined,
|
||||
} as never),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("owns replay policy for Claude transports", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicPlugin);
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { CLAUDE_CLI_BACKEND_ID } from "./cli-shared.js";
|
||||
|
||||
const ANTHROPIC_PROVIDER_IDS = new Set(["anthropic", CLAUDE_CLI_BACKEND_ID]);
|
||||
const ANTHROPIC_FRONTIER_MODEL_PREFIXES = ["claude-sonnet-4", "claude-opus-4"];
|
||||
|
||||
export const ANTHROPIC_FRONTIER_INTERACTION_STYLE = `## Interaction Style
|
||||
|
||||
Be warm, collaborative, and quietly supportive.
|
||||
Communicate like a capable teammate, not a policy document.
|
||||
Keep preambles short and use brief progress updates while you work.
|
||||
If the user asks you to do the work, act in the same turn instead of restating the plan.
|
||||
Default to concise natural replies unless the user asks for depth.
|
||||
Let personality show through phrasing and judgment, not through filler.`;
|
||||
|
||||
export const ANTHROPIC_FRONTIER_OUTPUT_CONTRACT = `## Claude Output Contract
|
||||
|
||||
Follow the latest user instruction over older summaries, memories, or prior plans when they conflict.
|
||||
Do not present a summary, restatement, or plan as if it were real progress.
|
||||
Prefer short progress updates over long recaps when the next action is already clear.
|
||||
Stay within the current request instead of widening into unrelated scenario updates, grand wrap-ups, or suite-wide status recaps.
|
||||
For scoped reports, return plain bullets or short paragraphs unless the user explicitly asked for tables, decorative headings, or ornamental markdown.
|
||||
Do not invent final tallies, "QA complete" claims, or overall completion counts unless the current turn or tool results actually established them.
|
||||
Multi-part requests stay incomplete until every requested item is handled or clearly marked blocked.`;
|
||||
|
||||
export const ANTHROPIC_FRONTIER_EXECUTION_BIAS = `## Execution Bias
|
||||
|
||||
When tools are available and the next action is clear, act before recapping.
|
||||
Do not say you will inspect, search, open, edit, or verify something unless you emit the tool call in the same turn.
|
||||
After compaction or summary refresh, resume the next unfinished action instead of restarting the analysis from scratch.
|
||||
Keep going until the requested outcome is complete or clearly blocked.`;
|
||||
|
||||
export const ANTHROPIC_FRONTIER_TOOL_CALL_STYLE = `## Tool Call Style
|
||||
|
||||
For routine inspection, search, open, read, edit, or verify steps, call the tool immediately instead of narrating the intent first.
|
||||
Keep pre-tool commentary brief and only use it when the action is sensitive, non-obvious, or user-requested.`;
|
||||
|
||||
function matchesAnthropicFrontierModel(modelId?: string): boolean {
|
||||
const normalizedModelId = modelId?.trim().toLowerCase() ?? "";
|
||||
return ANTHROPIC_FRONTIER_MODEL_PREFIXES.some((prefix) => normalizedModelId.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function shouldApplyAnthropicPromptOverlay(params: {
|
||||
modelProviderId?: string;
|
||||
modelId?: string;
|
||||
}): boolean {
|
||||
return (
|
||||
ANTHROPIC_PROVIDER_IDS.has(params.modelProviderId ?? "") &&
|
||||
matchesAnthropicFrontierModel(params.modelId)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveAnthropicSystemPromptContribution(params: {
|
||||
modelProviderId?: string;
|
||||
modelId?: string;
|
||||
}) {
|
||||
if (
|
||||
!shouldApplyAnthropicPromptOverlay({
|
||||
modelProviderId: params.modelProviderId,
|
||||
modelId: params.modelId,
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
stablePrefix: ANTHROPIC_FRONTIER_OUTPUT_CONTRACT,
|
||||
sectionOverrides: {
|
||||
interaction_style: ANTHROPIC_FRONTIER_INTERACTION_STYLE,
|
||||
tool_call_style: ANTHROPIC_FRONTIER_TOOL_CALL_STYLE,
|
||||
execution_bias: ANTHROPIC_FRONTIER_EXECUTION_BIAS,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
normalizeAnthropicProviderConfig,
|
||||
} from "./config-defaults.js";
|
||||
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { resolveAnthropicSystemPromptContribution } from "./prompt-overlay.js";
|
||||
import { buildAnthropicReplayPolicy } from "./replay-policy.js";
|
||||
import { wrapAnthropicProviderStream } from "./stream-wrappers.js";
|
||||
|
||||
@@ -463,11 +462,6 @@ export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
|
||||
provider.trim().toLowerCase() === CLAUDE_CLI_BACKEND_ID
|
||||
? resolveClaudeCliSyntheticAuth()
|
||||
: undefined,
|
||||
resolveSystemPromptContribution: (ctx) =>
|
||||
resolveAnthropicSystemPromptContribution({
|
||||
modelProviderId: ctx.provider,
|
||||
modelId: ctx.modelId,
|
||||
}),
|
||||
buildReplayPolicy: buildAnthropicReplayPolicy,
|
||||
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
|
||||
resolveReasoningOutputMode: () => "native",
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
streamWithPayloadPatch,
|
||||
} from "openclaw/plugin-sdk/provider-stream-shared";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
const log = createSubsystemLogger("anthropic-stream");
|
||||
|
||||
@@ -158,9 +157,9 @@ export function createAnthropicFastModeWrapper(
|
||||
}
|
||||
|
||||
const payloadPolicy = resolveAnthropicPayloadPolicy({
|
||||
provider: readStringValue(model.provider),
|
||||
api: readStringValue(model.api),
|
||||
baseUrl: readStringValue(model.baseUrl),
|
||||
provider: typeof model.provider === "string" ? model.provider : undefined,
|
||||
api: typeof model.api === "string" ? model.api : undefined,
|
||||
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
|
||||
serviceTier,
|
||||
});
|
||||
if (!payloadPolicy.allowsServiceTier) {
|
||||
@@ -184,9 +183,9 @@ export function createAnthropicServiceTierWrapper(
|
||||
}
|
||||
|
||||
const payloadPolicy = resolveAnthropicPayloadPolicy({
|
||||
provider: readStringValue(model.provider),
|
||||
api: readStringValue(model.api),
|
||||
baseUrl: readStringValue(model.baseUrl),
|
||||
provider: typeof model.provider === "string" ? model.provider : undefined,
|
||||
api: typeof model.api === "string" ? model.api : undefined,
|
||||
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
|
||||
serviceTier,
|
||||
});
|
||||
if (!payloadPolicy.allowsServiceTier) {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export {
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from "openclaw/plugin-sdk/account-resolution";
|
||||
import { resolveChannelStreamingChunkMode } from "openclaw/plugin-sdk/channel-streaming";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
||||
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
@@ -59,7 +58,7 @@ export function resolveBlueBubblesAccount(params: {
|
||||
return {
|
||||
accountId,
|
||||
enabled: baseEnabled !== false && accountEnabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
name: merged.name?.trim() || undefined,
|
||||
config: merged,
|
||||
configured,
|
||||
baseUrl,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
||||
import { assertMultipartActionOk, postMultipartFormData } from "./multipart.js";
|
||||
import {
|
||||
@@ -131,7 +130,7 @@ export async function downloadBlueBubblesAttachment(
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
const text = formatErrorMessage(error);
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`BlueBubbles attachment download failed: ${text}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
createComputedAccountStatusAdapter,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { type ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
import { bluebubblesMessageActions } from "./actions.js";
|
||||
import {
|
||||
@@ -136,7 +135,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
looksLikeId: looksLikeBlueBubblesExplicitTargetId,
|
||||
hint: "<handle|chat_guid:GUID|chat_id:ID|chat_identifier:ID>",
|
||||
resolveTarget: async ({ normalized }) => {
|
||||
const to = normalizeOptionalString(normalized);
|
||||
const to = normalized?.trim();
|
||||
if (!to) {
|
||||
return null;
|
||||
}
|
||||
@@ -161,7 +160,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
|
||||
// Helper to extract a clean handle from any BlueBubbles target format
|
||||
const extractCleanDisplay = (value: string | undefined): string | null => {
|
||||
const trimmed = normalizeOptionalString(value);
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
@@ -197,7 +196,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
};
|
||||
|
||||
// Try to get a clean display from the display parameter first
|
||||
const trimmedDisplay = normalizeOptionalString(display);
|
||||
const trimmedDisplay = display?.trim();
|
||||
if (trimmedDisplay) {
|
||||
if (!shouldParseDisplay(trimmedDisplay)) {
|
||||
return trimmedDisplay;
|
||||
@@ -215,7 +214,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
}
|
||||
|
||||
// Last resort: return display or target as-is
|
||||
return normalizeOptionalString(display) || normalizeOptionalString(target) || "";
|
||||
return display?.trim() || target?.trim() || "";
|
||||
},
|
||||
},
|
||||
setup: blueBubblesSetupAdapter,
|
||||
@@ -287,7 +286,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
},
|
||||
threading: {
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
currentChannelId: normalizeOptionalString(context.To),
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
currentThreadTs: context.ReplyToIdFull ?? context.ReplyToId,
|
||||
hasRepliedRef,
|
||||
}),
|
||||
@@ -315,7 +314,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
deliveryMode: "direct",
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = normalizeOptionalString(to);
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -329,7 +328,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
channel: "bluebubbles",
|
||||
sendText: async ({ cfg, to, text, accountId, replyToId }) => {
|
||||
const runtime = await loadBlueBubblesChannelRuntime();
|
||||
const rawReplyToId = normalizeOptionalString(replyToId) ?? "";
|
||||
const rawReplyToId = typeof replyToId === "string" ? replyToId.trim() : "";
|
||||
const replyToMessageGuid = rawReplyToId
|
||||
? runtime.resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
|
||||
: "";
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { parseFiniteNumber } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { asNullableRecord, readStringField } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
|
||||
import type { BlueBubblesAttachment } from "./types.js";
|
||||
|
||||
export const asRecord = asNullableRecord;
|
||||
const readString = readStringField;
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function readString(record: Record<string, unknown> | null, key: string): string | undefined {
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
const value = record[key];
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function readNumber(record: Record<string, unknown> | null, key: string): number | undefined {
|
||||
if (!record) {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js";
|
||||
import {
|
||||
asRecord,
|
||||
normalizeWebhookMessage,
|
||||
normalizeWebhookReaction,
|
||||
} from "./monitor-normalize.js";
|
||||
import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js";
|
||||
import { logVerbose, processMessage, processReaction } from "./monitor-processing.js";
|
||||
import {
|
||||
_resetBlueBubblesShortIdState,
|
||||
@@ -93,11 +88,17 @@ function parseBlueBubblesWebhookPayload(
|
||||
try {
|
||||
return { ok: true, value: JSON.parse(payload) as unknown };
|
||||
} catch (error) {
|
||||
return { ok: false, error: formatErrorMessage(error) };
|
||||
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function maskSecret(value: string): string {
|
||||
if (value.length <= 6) {
|
||||
return "***";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { BaseProbeResult } from "./runtime-api.js";
|
||||
import { normalizeSecretInputString } from "./secret-input.js";
|
||||
import { buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout } from "./types.js";
|
||||
@@ -25,7 +23,7 @@ const serverInfoCache = new Map<string, { info: BlueBubblesServerInfo; expires:
|
||||
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
function buildCacheKey(accountId?: string): string {
|
||||
return normalizeOptionalString(accountId) || "default";
|
||||
return accountId?.trim() || "default";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +172,7 @@ export async function probeBlueBubbles(params: {
|
||||
return {
|
||||
ok: false,
|
||||
status: null,
|
||||
error: formatErrorMessage(err),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { asRecord } from "./monitor-normalize.js";
|
||||
import { normalizeBlueBubblesHandle, parseBlueBubblesTarget } from "./targets.js";
|
||||
import type { BlueBubblesSendTarget } from "./types.js";
|
||||
|
||||
@@ -25,6 +24,11 @@ export function extractBlueBubblesMessageId(payload: unknown): string {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> | null =>
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const record = payload as Record<string, unknown>;
|
||||
const dataRecord = asRecord(record.data);
|
||||
const resultRecord = asRecord(record.result);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
import { normalizeOptionalString, stripMarkdown } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { stripMarkdown } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
||||
import {
|
||||
getCachedBlueBubblesPrivateApiStatus,
|
||||
@@ -137,9 +137,8 @@ function extractChatGuid(chat: BlueBubblesChatRecord): string | null {
|
||||
chat.chat_identifier,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
const value = normalizeOptionalString(candidate);
|
||||
if (value) {
|
||||
return value;
|
||||
if (typeof candidate === "string" && candidate.trim()) {
|
||||
return candidate.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -160,7 +159,8 @@ function extractChatIdentifierFromChatGuid(chatGuid: string): string | null {
|
||||
if (parts.length < 3) {
|
||||
return null;
|
||||
}
|
||||
return normalizeOptionalString(parts[2]) ?? null;
|
||||
const identifier = parts[2]?.trim();
|
||||
return identifier ? identifier : null;
|
||||
}
|
||||
|
||||
function extractParticipantAddresses(chat: BlueBubblesChatRecord): string[] {
|
||||
@@ -479,7 +479,7 @@ export async function sendMessageBlueBubbles(
|
||||
);
|
||||
}
|
||||
const effectId = resolveEffectId(opts.effectId);
|
||||
const wantsReplyThread = normalizeOptionalString(opts.replyToMessageGuid) !== undefined;
|
||||
const wantsReplyThread = Boolean(opts.replyToMessageGuid?.trim());
|
||||
const wantsEffect = Boolean(effectId);
|
||||
const privateApiDecision = resolvePrivateApiDecision({
|
||||
privateApiStatus,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type ChannelSetupWizard,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveBlueBubblesAccount, resolveDefaultBlueBubblesAccountId } from "./accounts.js";
|
||||
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
||||
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
||||
@@ -40,7 +39,7 @@ function validateBlueBubblesAllowFromEntry(value: string): string | null {
|
||||
if (parsed.kind === "handle" && !parsed.handle) {
|
||||
return null;
|
||||
}
|
||||
return normalizeOptionalString(value) ?? null;
|
||||
return value.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -77,7 +76,7 @@ const promptBlueBubblesAllowFrom = createPromptParsedAllowFromForAccount({
|
||||
});
|
||||
|
||||
function validateBlueBubblesServerUrlInput(value: unknown): string | undefined {
|
||||
const trimmed = normalizeOptionalString(value) ?? "";
|
||||
const trimmed = typeof value === "string" ? value.trim() : "";
|
||||
if (!trimmed) {
|
||||
return "Required";
|
||||
}
|
||||
@@ -109,11 +108,11 @@ function applyBlueBubblesSetupPatch(
|
||||
}
|
||||
|
||||
function resolveBlueBubblesServerUrl(cfg: OpenClawConfig, accountId: string): string | undefined {
|
||||
return normalizeOptionalString(resolveBlueBubblesAccount({ cfg, accountId }).config.serverUrl);
|
||||
return resolveBlueBubblesAccount({ cfg, accountId }).config.serverUrl?.trim() || undefined;
|
||||
}
|
||||
|
||||
function resolveBlueBubblesWebhookPath(cfg: OpenClawConfig, accountId: string): string | undefined {
|
||||
return normalizeOptionalString(resolveBlueBubblesAccount({ cfg, accountId }).config.webhookPath);
|
||||
return resolveBlueBubblesAccount({ cfg, accountId }).config.webhookPath?.trim() || undefined;
|
||||
}
|
||||
|
||||
function validateBlueBubblesWebhookPath(value: string): string | undefined {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { collectIssuesForEnabledAccounts } from "openclaw/plugin-sdk/status-helpers";
|
||||
import { asRecord } from "./monitor-normalize.js";
|
||||
import type { ChannelAccountSnapshot } from "./runtime-api.js";
|
||||
|
||||
type BlueBubblesAccountStatus = {
|
||||
@@ -18,6 +17,10 @@ type BlueBubblesProbeResult = {
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
@@ -25,30 +28,28 @@ function asString(value: unknown): string | null {
|
||||
function readBlueBubblesAccountStatus(
|
||||
value: ChannelAccountSnapshot,
|
||||
): BlueBubblesAccountStatus | null {
|
||||
const record = asRecord(value);
|
||||
if (!record) {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: record.accountId,
|
||||
enabled: record.enabled,
|
||||
configured: record.configured,
|
||||
running: record.running,
|
||||
baseUrl: record.baseUrl,
|
||||
lastError: record.lastError,
|
||||
probe: record.probe,
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
configured: value.configured,
|
||||
running: value.running,
|
||||
baseUrl: value.baseUrl,
|
||||
lastError: value.lastError,
|
||||
probe: value.probe,
|
||||
};
|
||||
}
|
||||
|
||||
function readBlueBubblesProbeResult(value: unknown): BlueBubblesProbeResult | null {
|
||||
const record = asRecord(value);
|
||||
if (!record) {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: typeof record.ok === "boolean" ? record.ok : undefined,
|
||||
status: typeof record.status === "number" ? record.status : null,
|
||||
error: asString(record.error) ?? null,
|
||||
ok: typeof value.ok === "boolean" ? value.ok : undefined,
|
||||
status: typeof value.status === "number" ? value.status : null,
|
||||
error: asString(value.error) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
} from "openclaw/plugin-sdk/channel-targets";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
export type BlueBubblesService = "imessage" | "sms" | "auto";
|
||||
|
||||
@@ -30,7 +29,7 @@ const CHAT_IDENTIFIER_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4
|
||||
const CHAT_IDENTIFIER_HEX_RE = /^[0-9a-f]{24,64}$/i;
|
||||
|
||||
function parseRawChatGuid(value: string): string | null {
|
||||
const trimmed = normalizeOptionalString(value);
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
@@ -38,9 +37,9 @@ function parseRawChatGuid(value: string): string | null {
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
const service = normalizeOptionalString(parts[0]);
|
||||
const separator = normalizeOptionalString(parts[1]);
|
||||
const identifier = normalizeOptionalString(parts[2]);
|
||||
const service = parts[0]?.trim();
|
||||
const separator = parts[1]?.trim();
|
||||
const identifier = parts[2]?.trim();
|
||||
if (!service || !identifier) {
|
||||
return null;
|
||||
}
|
||||
@@ -55,7 +54,7 @@ function stripPrefix(value: string, prefix: string): string {
|
||||
}
|
||||
|
||||
function stripBlueBubblesPrefix(value: string): string {
|
||||
const trimmed = normalizeOptionalString(value) ?? "";
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
@@ -66,7 +65,7 @@ function stripBlueBubblesPrefix(value: string): string {
|
||||
}
|
||||
|
||||
function looksLikeRawChatIdentifier(value: string): boolean {
|
||||
const trimmed = normalizeOptionalString(value);
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
@@ -140,7 +139,7 @@ export function extractHandleFromChatGuid(chatGuid: string): string | null {
|
||||
const parts = chatGuid.split(";");
|
||||
// DM format: service;-;handle (3 parts, middle is "-")
|
||||
if (parts.length === 3 && parts[1] === "-") {
|
||||
const handle = normalizeOptionalString(parts[2]);
|
||||
const handle = parts[2]?.trim();
|
||||
if (handle) {
|
||||
return normalizeBlueBubblesHandle(handle);
|
||||
}
|
||||
@@ -223,7 +222,7 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string):
|
||||
return true;
|
||||
}
|
||||
if (normalized) {
|
||||
const normalizedTrimmed = normalizeOptionalString(normalized);
|
||||
const normalizedTrimmed = normalized.trim();
|
||||
if (!normalizedTrimmed) {
|
||||
return false;
|
||||
}
|
||||
@@ -347,7 +346,7 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
|
||||
}
|
||||
|
||||
export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget {
|
||||
const trimmed = normalizeOptionalString(raw) ?? "";
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return { kind: "handle", handle: "" };
|
||||
}
|
||||
@@ -413,11 +412,11 @@ export function formatBlueBubblesChatTarget(params: {
|
||||
if (params.chatId && Number.isFinite(params.chatId)) {
|
||||
return `chat_id:${params.chatId}`;
|
||||
}
|
||||
const guid = normalizeOptionalString(params.chatGuid);
|
||||
const guid = params.chatGuid?.trim();
|
||||
if (guid) {
|
||||
return `chat_guid:${guid}`;
|
||||
}
|
||||
const identifier = normalizeOptionalString(params.chatIdentifier);
|
||||
const identifier = params.chatIdentifier?.trim();
|
||||
if (identifier) {
|
||||
return `chat_identifier:${identifier}`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-path";
|
||||
import type { BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
@@ -7,7 +6,7 @@ export { normalizeWebhookPath };
|
||||
export const DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook";
|
||||
|
||||
export function resolveWebhookPathFromConfig(config?: BlueBubblesAccountConfig): string {
|
||||
const raw = normalizeOptionalString(config?.webhookPath);
|
||||
const raw = config?.webhookPath?.trim();
|
||||
if (raw) {
|
||||
return normalizeWebhookPath(raw);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
WebSearchProviderPlugin,
|
||||
WebSearchProviderToolDefinition,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
createBraveSchema,
|
||||
mapBraveLlmContextResults,
|
||||
@@ -20,6 +19,10 @@ type ConfigTarget = Parameters<
|
||||
NonNullable<WebSearchProviderPlugin["setConfiguredCredentialValue"]>
|
||||
>[0];
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function resolveProviderWebSearchPluginConfig(
|
||||
config: ConfigInput,
|
||||
pluginId: string,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
function getTopLevelCredentialValue(searchConfig?: Record<string, unknown>): unknown {
|
||||
return searchConfig?.apiKey;
|
||||
}
|
||||
|
||||
function setTopLevelCredentialValue(
|
||||
searchConfigTarget: Record<string, unknown>,
|
||||
value: unknown,
|
||||
): void {
|
||||
searchConfigTarget.apiKey = value;
|
||||
}
|
||||
|
||||
export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
return {
|
||||
id: "brave",
|
||||
label: "Brave Search",
|
||||
hint: "Structured results · country/language/time filters",
|
||||
onboardingScopes: ["text-inference"],
|
||||
credentialLabel: "Brave Search API key",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
placeholder: "BSA...",
|
||||
signupUrl: "https://brave.com/search/api/",
|
||||
docsUrl: "https://docs.openclaw.ai/brave-search",
|
||||
autoDetectOrder: 10,
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"],
|
||||
getCredentialValue: getTopLevelCredentialValue,
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value);
|
||||
},
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { isRecord } from "./src/record-shared.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function listContainsBrowser(value: unknown): boolean {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
||||
browserAct,
|
||||
@@ -107,7 +106,7 @@ function formatConsoleToolResult(result: {
|
||||
content: [{ type: "text" as const, text: wrapped.wrappedText }],
|
||||
details: {
|
||||
...wrapped.safeDetails,
|
||||
targetId: readStringValue(result.targetId),
|
||||
targetId: typeof result.targetId === "string" ? result.targetId : undefined,
|
||||
messageCount: Array.isArray(result.messages) ? result.messages.length : undefined,
|
||||
},
|
||||
};
|
||||
@@ -134,7 +133,7 @@ function isChromeStaleTargetError(profile: string | undefined, err: unknown): bo
|
||||
function stripTargetIdFromActRequest(
|
||||
request: Parameters<typeof browserAct>[1],
|
||||
): Parameters<typeof browserAct>[1] | null {
|
||||
const targetId = normalizeOptionalString(request.targetId);
|
||||
const targetId = typeof request.targetId === "string" ? request.targetId.trim() : undefined;
|
||||
if (!targetId) {
|
||||
return null;
|
||||
}
|
||||
@@ -195,7 +194,7 @@ export async function executeSnapshotAction(params: {
|
||||
const refs: "aria" | "role" | undefined =
|
||||
input.refs === "aria" || input.refs === "role" ? input.refs : undefined;
|
||||
const hasMaxChars = Object.hasOwn(input, "maxChars");
|
||||
const targetId = normalizeOptionalString(input.targetId);
|
||||
const targetId = typeof input.targetId === "string" ? input.targetId.trim() : undefined;
|
||||
const limit =
|
||||
typeof input.limit === "number" && Number.isFinite(input.limit) ? input.limit : undefined;
|
||||
const maxChars =
|
||||
@@ -206,8 +205,8 @@ export async function executeSnapshotAction(params: {
|
||||
const compact = typeof input.compact === "boolean" ? input.compact : undefined;
|
||||
const depth =
|
||||
typeof input.depth === "number" && Number.isFinite(input.depth) ? input.depth : undefined;
|
||||
const selector = normalizeOptionalString(input.selector);
|
||||
const frame = normalizeOptionalString(input.frame);
|
||||
const selector = typeof input.selector === "string" ? input.selector.trim() : undefined;
|
||||
const frame = typeof input.frame === "string" ? input.frame.trim() : undefined;
|
||||
const resolvedMaxChars =
|
||||
format === "ai"
|
||||
? hasMaxChars
|
||||
@@ -315,8 +314,8 @@ export async function executeConsoleAction(params: {
|
||||
proxyRequest: BrowserProxyRequest | null;
|
||||
}): Promise<AgentToolResult<unknown>> {
|
||||
const { input, baseUrl, profile, proxyRequest } = params;
|
||||
const level = normalizeOptionalString(input.level);
|
||||
const targetId = normalizeOptionalString(input.targetId);
|
||||
const level = typeof input.level === "string" ? input.level.trim() : undefined;
|
||||
const targetId = typeof input.targetId === "string" ? input.targetId.trim() : undefined;
|
||||
if (proxyRequest) {
|
||||
const result = (await proxyRequest({
|
||||
method: "GET",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import crypto from "node:crypto";
|
||||
import { normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
executeActAction,
|
||||
executeConsoleAction,
|
||||
@@ -115,7 +114,7 @@ export const __testing = {
|
||||
};
|
||||
|
||||
function readOptionalTargetAndTimeout(params: Record<string, unknown>) {
|
||||
const targetId = normalizeOptionalString(params.targetId);
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||
const timeoutMs =
|
||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||
? params.timeoutMs
|
||||
@@ -648,7 +647,7 @@ export function createBrowserTool(opts?: {
|
||||
proxyRequest,
|
||||
});
|
||||
case "pdf": {
|
||||
const targetId = normalizeOptionalString(params.targetId);
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||
const result = proxyRequest
|
||||
? ((await proxyRequest({
|
||||
method: "POST",
|
||||
@@ -710,7 +709,7 @@ export function createBrowserTool(opts?: {
|
||||
}
|
||||
case "dialog": {
|
||||
const accept = Boolean(params.accept);
|
||||
const promptText = readStringValue(params.promptText);
|
||||
const promptText = typeof params.promptText === "string" ? params.promptText : undefined;
|
||||
const { targetId, timeoutMs } = readOptionalTargetAndTimeout(params);
|
||||
if (proxyRequest) {
|
||||
const result = await proxyRequest({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import express from "express";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { isLoopbackHost } from "../gateway/net.js";
|
||||
import { deleteBridgeAuthForPort, setBridgeAuthForPort } from "./bridge-auth-registry.js";
|
||||
import type { ResolvedBrowserConfig } from "./config.js";
|
||||
@@ -34,9 +33,8 @@ function buildNoVncBootstrapHtml(params: ResolvedNoVncObserver): string {
|
||||
autoconnect: "1",
|
||||
resize: "remote",
|
||||
});
|
||||
const password = normalizeOptionalString(params.password);
|
||||
if (password) {
|
||||
hash.set("password", password);
|
||||
if (params.password?.trim()) {
|
||||
hash.set("password", params.password);
|
||||
}
|
||||
const targetUrl = `http://127.0.0.1:${params.noVncPort}/vnc.html#${hash.toString()}`;
|
||||
const encodedTarget = JSON.stringify(targetUrl);
|
||||
@@ -82,7 +80,7 @@ export async function startBrowserBridgeServer(params: {
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
res.setHeader("Referrer-Policy", "no-referrer");
|
||||
const rawToken = normalizeOptionalString(req.query?.token);
|
||||
const rawToken = typeof req.query?.token === "string" ? req.query.token.trim() : "";
|
||||
if (!rawToken) {
|
||||
res.status(400).send("Missing token");
|
||||
return;
|
||||
@@ -96,8 +94,8 @@ export async function startBrowserBridgeServer(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const authToken = normalizeOptionalString(params.authToken);
|
||||
const authPassword = normalizeOptionalString(params.authPassword);
|
||||
const authToken = params.authToken?.trim() || undefined;
|
||||
const authPassword = params.authPassword?.trim() || undefined;
|
||||
if (!authToken && !authPassword) {
|
||||
throw new Error("bridge server requires auth (authToken/authPassword missing)");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { normalizeString } from "../record-shared.js";
|
||||
import type { SnapshotAriaNode } from "./client.js";
|
||||
import {
|
||||
getRoleSnapshotStats,
|
||||
@@ -21,6 +20,17 @@ function normalizeRole(node: ChromeMcpSnapshotNode): string {
|
||||
return role || "generic";
|
||||
}
|
||||
|
||||
function normalizeString(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function escapeQuoted(value: string): string {
|
||||
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { asRecord } from "../record-shared.js";
|
||||
import type { ChromeMcpSnapshotNode } from "./chrome-mcp.snapshot.js";
|
||||
import type { BrowserTab } from "./client.js";
|
||||
import { BrowserProfileUnavailableError, BrowserTabNotFoundError } from "./errors.js";
|
||||
@@ -47,6 +45,12 @@ const sessions = new Map<string, ChromeMcpSession>();
|
||||
const pendingSessions = new Map<string, Promise<ChromeMcpSession>>();
|
||||
let sessionFactory: ChromeMcpSessionFactory | null = null;
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function asPages(value: unknown): ChromeMcpStructuredPage[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
@@ -59,7 +63,7 @@ function asPages(value: unknown): ChromeMcpStructuredPage[] {
|
||||
}
|
||||
out.push({
|
||||
id: record.id,
|
||||
url: readStringValue(record.url),
|
||||
url: typeof record.url === "string" ? record.url : undefined,
|
||||
selected: record.selected === true,
|
||||
});
|
||||
}
|
||||
@@ -107,7 +111,7 @@ function extractTextPages(result: ChromeMcpToolResult): ChromeMcpStructuredPage[
|
||||
}
|
||||
pages.push({
|
||||
id: Number.parseInt(match[1] ?? "", 10),
|
||||
url: normalizeOptionalString(match[2]),
|
||||
url: match[2]?.trim() || undefined,
|
||||
selected: Boolean(match[3]),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeOptionalTrimmedStringList,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
type BrowserConfig,
|
||||
type BrowserProfileConfig,
|
||||
@@ -116,7 +112,15 @@ function resolveCdpPortRangeStart(
|
||||
return start;
|
||||
}
|
||||
|
||||
const normalizeStringList = normalizeOptionalTrimmedStringList;
|
||||
function normalizeStringList(raw: string[] | undefined): string[] | undefined {
|
||||
if (!Array.isArray(raw) || raw.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const values = raw
|
||||
.map((value) => value.trim())
|
||||
.filter((value): value is string => value.length > 0);
|
||||
return values.length > 0 ? values : undefined;
|
||||
}
|
||||
|
||||
function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined {
|
||||
const rawPolicy = cfg?.ssrfPolicy as
|
||||
@@ -234,8 +238,8 @@ export function resolveBrowserConfig(
|
||||
const headless = cfg?.headless === true;
|
||||
const noSandbox = cfg?.noSandbox === true;
|
||||
const attachOnly = cfg?.attachOnly === true;
|
||||
const executablePath = normalizeOptionalString(cfg?.executablePath);
|
||||
const defaultProfileFromConfig = normalizeOptionalString(cfg?.defaultProfile);
|
||||
const executablePath = cfg?.executablePath?.trim() || undefined;
|
||||
const defaultProfileFromConfig = cfg?.defaultProfile?.trim() || undefined;
|
||||
|
||||
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
|
||||
const isWsUrl = cdpInfo.parsed.protocol === "ws:" || cdpInfo.parsed.protocol === "wss:";
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { BrowserProfileConfig, OpenClawConfig } from "../config/config.js";
|
||||
import { loadConfig, writeConfigFile } from "../config/config.js";
|
||||
import { deriveDefaultBrowserCdpPortRange } from "../config/port-defaults.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { assertCdpEndpointAllowed } from "./cdp.helpers.js";
|
||||
import { resolveOpenClawUserDataDir } from "./chrome.js";
|
||||
@@ -84,8 +82,8 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
|
||||
|
||||
const createProfile = async (params: CreateProfileParams): Promise<CreateProfileResult> => {
|
||||
const name = params.name.trim();
|
||||
const rawCdpUrl = normalizeOptionalString(params.cdpUrl);
|
||||
const rawUserDataDir = normalizeOptionalString(params.userDataDir);
|
||||
const rawCdpUrl = params.cdpUrl?.trim() || undefined;
|
||||
const rawUserDataDir = params.userDataDir?.trim() || undefined;
|
||||
const normalizedUserDataDir = rawUserDataDir ? resolveUserPath(rawUserDataDir) : undefined;
|
||||
const driver = params.driver === "existing-session" ? "existing-session" : undefined;
|
||||
|
||||
@@ -129,7 +127,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
|
||||
parsed = parseHttpUrl(rawCdpUrl, "browser.profiles.cdpUrl");
|
||||
await assertCdpEndpointAllowed(parsed.normalized, state.resolved.ssrfPolicy);
|
||||
} catch (err) {
|
||||
throw new BrowserValidationError(formatErrorMessage(err));
|
||||
throw new BrowserValidationError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
if (driver === "existing-session") {
|
||||
throw new BrowserValidationError(
|
||||
|
||||
@@ -459,7 +459,7 @@ async function connectBrowser(cdpUrl: string): Promise<ConnectedBrowser> {
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
// Don't retry rate-limit errors; retrying worsens the 429.
|
||||
const errMsg = formatErrorMessage(err);
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
if (errMsg.includes("rate limit")) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import type { BrowserActRequest, BrowserFormField } from "./client-actions-core.js";
|
||||
import { DEFAULT_FILL_FIELD_TYPE } from "./form-fields.js";
|
||||
import { DEFAULT_UPLOAD_DIR, resolveStrictExistingPathsWithinRoot } from "./paths.js";
|
||||
@@ -881,7 +880,7 @@ export async function batchViaPlaywright(opts: {
|
||||
await executeSingleAction(action, opts.cdpUrl, opts.targetId, opts.evaluateEnabled, depth);
|
||||
results.push({ ok: true });
|
||||
} catch (err) {
|
||||
const message = formatErrorMessage(err);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
results.push({ ok: false, error: message });
|
||||
if (opts.stopOnError !== false) {
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { parseRoleRef } from "./pw-role-snapshot.js";
|
||||
|
||||
let nextUploadArmId = 0;
|
||||
@@ -50,7 +49,7 @@ export function normalizeTimeoutMs(timeoutMs: number | undefined, fallback: numb
|
||||
}
|
||||
|
||||
export function toAIFriendlyError(error: unknown, selector: string): Error {
|
||||
const message = formatErrorMessage(error);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (message.includes("strict mode violation")) {
|
||||
const countMatch = message.match(/resolved to (\d+) elements/);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { ensurePageState, getPageForTargetId } from "./pw-session.js";
|
||||
|
||||
export async function cookiesGetViaPlaywright(opts: {
|
||||
@@ -64,7 +63,7 @@ export async function storageGetViaPlaywright(opts: {
|
||||
const page = await getPageForTargetId(opts);
|
||||
ensurePageState(page);
|
||||
const kind = opts.kind;
|
||||
const key = readStringValue(opts.key);
|
||||
const key = typeof opts.key === "string" ? opts.key : undefined;
|
||||
const values = await page.evaluate(
|
||||
({ kind: kind2, key: key2 }) => {
|
||||
const store = kind2 === "session" ? window.sessionStorage : window.localStorage;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
type BrowserRequestProfileParams = {
|
||||
query?: Record<string, unknown>;
|
||||
body?: unknown;
|
||||
@@ -32,16 +30,20 @@ export function isPersistentBrowserProfileMutation(method: string, path: string)
|
||||
export function resolveRequestedBrowserProfile(
|
||||
params: BrowserRequestProfileParams,
|
||||
): string | undefined {
|
||||
const queryProfile = normalizeOptionalString(params.query?.profile);
|
||||
const queryProfile =
|
||||
typeof params.query?.profile === "string" ? params.query.profile.trim() : undefined;
|
||||
if (queryProfile) {
|
||||
return queryProfile;
|
||||
}
|
||||
if (params.body && typeof params.body === "object") {
|
||||
const bodyProfile =
|
||||
"profile" in params.body ? normalizeOptionalString(params.body.profile) : undefined;
|
||||
"profile" in params.body && typeof params.body.profile === "string"
|
||||
? params.body.profile.trim()
|
||||
: undefined;
|
||||
if (bodyProfile) {
|
||||
return bodyProfile;
|
||||
}
|
||||
}
|
||||
return normalizeOptionalString(params.profile);
|
||||
const explicitProfile = typeof params.profile === "string" ? params.profile.trim() : undefined;
|
||||
return explicitProfile || undefined;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import {
|
||||
clickChromeMcpElement,
|
||||
closeChromeMcpTab,
|
||||
@@ -1086,7 +1085,7 @@ export function registerBrowserAgentActRoutes(
|
||||
try {
|
||||
actions = Array.isArray(body.actions) ? body.actions.map(normalizeBatchAction) : [];
|
||||
} catch (err) {
|
||||
return jsonError(res, 400, formatErrorMessage(err));
|
||||
return jsonError(res, 400, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
if (!actions.length) {
|
||||
return jsonError(res, 400, "actions are required");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { BrowserRouteContext } from "../server-context.js";
|
||||
import {
|
||||
readBody,
|
||||
@@ -31,7 +30,7 @@ export function registerBrowserAgentDebugRoutes(
|
||||
const messages = await pw.getConsoleMessagesViaPlaywright({
|
||||
cdpUrl,
|
||||
targetId: tab.targetId,
|
||||
level: normalizeOptionalString(level),
|
||||
level: level.trim() || undefined,
|
||||
});
|
||||
res.json({ ok: true, messages, targetId: tab.targetId });
|
||||
},
|
||||
@@ -74,7 +73,7 @@ export function registerBrowserAgentDebugRoutes(
|
||||
const result = await pw.getNetworkRequestsViaPlaywright({
|
||||
cdpUrl,
|
||||
targetId: tab.targetId,
|
||||
filter: normalizeOptionalString(filter),
|
||||
filter: filter.trim() || undefined,
|
||||
clear,
|
||||
});
|
||||
res.json({ ok: true, targetId: tab.targetId, ...result });
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { ResolvedBrowserProfile } from "../config.js";
|
||||
import {
|
||||
DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH,
|
||||
@@ -42,13 +41,14 @@ export function resolveSnapshotPlan(params: {
|
||||
explicitFormat,
|
||||
mode,
|
||||
});
|
||||
const limitRaw = readStringValue(params.query.limit);
|
||||
const limitRaw = typeof params.query.limit === "string" ? Number(params.query.limit) : undefined;
|
||||
const hasMaxChars = Object.hasOwn(params.query, "maxChars");
|
||||
const maxCharsRaw = readStringValue(params.query.maxChars);
|
||||
const limit = Number.isFinite(Number(limitRaw)) ? Number(limitRaw) : undefined;
|
||||
const maxCharsRaw =
|
||||
typeof params.query.maxChars === "string" ? Number(params.query.maxChars) : undefined;
|
||||
const limit = Number.isFinite(limitRaw) ? limitRaw : undefined;
|
||||
const maxChars =
|
||||
Number.isFinite(Number(maxCharsRaw)) && Number(maxCharsRaw) > 0
|
||||
? Math.floor(Number(maxCharsRaw))
|
||||
typeof maxCharsRaw === "number" && Number.isFinite(maxCharsRaw) && maxCharsRaw > 0
|
||||
? Math.floor(maxCharsRaw)
|
||||
: undefined;
|
||||
const resolvedMaxChars =
|
||||
format === "ai"
|
||||
@@ -68,8 +68,8 @@ export function resolveSnapshotPlan(params: {
|
||||
const compact = compactRaw ?? (mode === "efficient" ? true : undefined);
|
||||
const depth =
|
||||
depthRaw ?? (mode === "efficient" ? DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH : undefined);
|
||||
const selectorValue = normalizeOptionalString(toStringOrEmpty(params.query.selector));
|
||||
const frameSelectorValue = normalizeOptionalString(toStringOrEmpty(params.query.frame));
|
||||
const selectorValue = toStringOrEmpty(params.query.selector).trim() || undefined;
|
||||
const frameSelectorValue = toStringOrEmpty(params.query.frame).trim() || undefined;
|
||||
|
||||
return {
|
||||
format,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { BrowserRouteContext } from "../server-context.js";
|
||||
import {
|
||||
readBody,
|
||||
@@ -168,7 +167,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
cdpUrl,
|
||||
targetId: tab.targetId,
|
||||
kind,
|
||||
key: normalizeOptionalString(key),
|
||||
key: key.trim() || undefined,
|
||||
});
|
||||
res.json({ ok: true, targetId: tab.targetId, ...result });
|
||||
},
|
||||
@@ -293,7 +292,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
const targetId = resolveTargetIdFromBody(body);
|
||||
const clear = toBoolean(body.clear) ?? false;
|
||||
const username = toStringOrEmpty(body.username) || undefined;
|
||||
const password = readStringValue(body.password);
|
||||
const password = typeof body.password === "string" ? body.password : undefined;
|
||||
|
||||
await withPlaywrightRouteContext({
|
||||
req,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { parseBooleanValue } from "../../utils/boolean.js";
|
||||
import type { BrowserRouteContext, ProfileContext } from "../server-context.js";
|
||||
import type { BrowserRequest, BrowserResponse } from "./types.js";
|
||||
@@ -15,14 +14,14 @@ export function getProfileContext(
|
||||
|
||||
// Check query string first (works for GET and POST)
|
||||
if (typeof req.query.profile === "string") {
|
||||
profileName = normalizeOptionalString(req.query.profile);
|
||||
profileName = req.query.profile.trim() || undefined;
|
||||
}
|
||||
|
||||
// Fall back to body for POST requests
|
||||
if (!profileName && req.body && typeof req.body === "object") {
|
||||
const body = req.body as Record<string, unknown>;
|
||||
if (typeof body.profile === "string") {
|
||||
profileName = normalizeOptionalString(body.profile);
|
||||
profileName = body.profile.trim() || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { BrowserParentOpts } from "../browser-cli-shared.js";
|
||||
import { danger, defaultRuntime } from "../core-api.js";
|
||||
import {
|
||||
@@ -62,9 +61,9 @@ export function registerBrowserElementCommands(
|
||||
body: {
|
||||
kind: "click",
|
||||
ref: refValue,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
doubleClick: Boolean(opts.double),
|
||||
button: normalizeOptionalString(opts.button),
|
||||
button: opts.button?.trim() || undefined,
|
||||
modifiers,
|
||||
},
|
||||
successMessage: (result) => {
|
||||
@@ -96,7 +95,7 @@ export function registerBrowserElementCommands(
|
||||
text,
|
||||
submit: Boolean(opts.submit),
|
||||
slowly: Boolean(opts.slowly),
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: `typed into ref ${refValue}`,
|
||||
});
|
||||
@@ -110,7 +109,7 @@ export function registerBrowserElementCommands(
|
||||
.action(async (key: string, opts, cmd) => {
|
||||
await runElementAction({
|
||||
cmd,
|
||||
body: { kind: "press", key, targetId: normalizeOptionalString(opts.targetId) },
|
||||
body: { kind: "press", key, targetId: opts.targetId?.trim() || undefined },
|
||||
successMessage: `pressed ${key}`,
|
||||
});
|
||||
});
|
||||
@@ -123,7 +122,7 @@ export function registerBrowserElementCommands(
|
||||
.action(async (ref: string, opts, cmd) => {
|
||||
await runElementAction({
|
||||
cmd,
|
||||
body: { kind: "hover", ref, targetId: normalizeOptionalString(opts.targetId) },
|
||||
body: { kind: "hover", ref, targetId: opts.targetId?.trim() || undefined },
|
||||
successMessage: `hovered ref ${ref}`,
|
||||
});
|
||||
});
|
||||
@@ -147,7 +146,7 @@ export function registerBrowserElementCommands(
|
||||
body: {
|
||||
kind: "scrollIntoView",
|
||||
ref: refValue,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs,
|
||||
},
|
||||
timeoutMs,
|
||||
@@ -168,7 +167,7 @@ export function registerBrowserElementCommands(
|
||||
kind: "drag",
|
||||
startRef,
|
||||
endRef,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: `dragged ${startRef} → ${endRef}`,
|
||||
});
|
||||
@@ -187,7 +186,7 @@ export function registerBrowserElementCommands(
|
||||
kind: "select",
|
||||
ref,
|
||||
values,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: `selected ${values.join(", ")}`,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { callBrowserRequest, type BrowserParentOpts } from "../browser-cli-shared.js";
|
||||
import {
|
||||
danger,
|
||||
@@ -58,7 +57,8 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
) {
|
||||
const resolveTimeoutAndTarget = (opts: { timeoutMs?: unknown; targetId?: unknown }) => {
|
||||
const timeoutMs = Number.isFinite(opts.timeoutMs) ? Number(opts.timeoutMs) : undefined;
|
||||
const targetId = normalizeOptionalString(opts.targetId);
|
||||
const targetId =
|
||||
typeof opts.targetId === "string" ? opts.targetId.trim() || undefined : undefined;
|
||||
return { timeoutMs, targetId };
|
||||
};
|
||||
|
||||
@@ -109,9 +109,9 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
path: "/hooks/file-chooser",
|
||||
body: {
|
||||
paths: normalizedPaths,
|
||||
ref: normalizeOptionalString(opts.ref),
|
||||
inputRef: normalizeOptionalString(opts.inputRef),
|
||||
element: normalizeOptionalString(opts.element),
|
||||
ref: opts.ref?.trim() || undefined,
|
||||
inputRef: opts.inputRef?.trim() || undefined,
|
||||
element: opts.element?.trim() || undefined,
|
||||
targetId,
|
||||
timeoutMs,
|
||||
},
|
||||
@@ -137,7 +137,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
await runDownloadCommand(cmd, opts, {
|
||||
path: "/wait/download",
|
||||
body: {
|
||||
path: normalizeOptionalString(outPath),
|
||||
path: outPath?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -193,7 +193,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
path: "/hooks/dialog",
|
||||
body: {
|
||||
accept,
|
||||
promptText: normalizeOptionalString(opts.prompt),
|
||||
promptText: opts.prompt?.trim() || undefined,
|
||||
targetId,
|
||||
timeoutMs,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { BrowserParentOpts } from "../browser-cli-shared.js";
|
||||
import { danger, defaultRuntime } from "../core-api.js";
|
||||
import {
|
||||
@@ -32,7 +31,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
body: {
|
||||
kind: "fill",
|
||||
fields,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
logBrowserActionResult(parent, result, `filled ${fields.length} field(s)`);
|
||||
@@ -61,7 +60,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
.action(async (selector: string | undefined, opts, cmd) => {
|
||||
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const sel = normalizeOptionalString(selector);
|
||||
const sel = selector?.trim() || undefined;
|
||||
const load =
|
||||
opts.load === "load" || opts.load === "domcontentloaded" || opts.load === "networkidle"
|
||||
? (opts.load as "load" | "domcontentloaded" | "networkidle")
|
||||
@@ -73,13 +72,13 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
body: {
|
||||
kind: "wait",
|
||||
timeMs: Number.isFinite(opts.time) ? opts.time : undefined,
|
||||
text: normalizeOptionalString(opts.text),
|
||||
textGone: normalizeOptionalString(opts.textGone),
|
||||
text: opts.text?.trim() || undefined,
|
||||
textGone: opts.textGone?.trim() || undefined,
|
||||
selector: sel,
|
||||
url: normalizeOptionalString(opts.url),
|
||||
url: opts.url?.trim() || undefined,
|
||||
loadState: load,
|
||||
fn: normalizeOptionalString(opts.fn),
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
fn: opts.fn?.trim() || undefined,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs,
|
||||
},
|
||||
timeoutMs,
|
||||
@@ -111,8 +110,8 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
body: {
|
||||
kind: "evaluate",
|
||||
fn: opts.fn,
|
||||
ref: normalizeOptionalString(opts.ref),
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
ref: opts.ref?.trim() || undefined,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
if (parent?.json) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { runBrowserResizeWithOutput } from "../browser-cli-resize.js";
|
||||
import { callBrowserRequest, type BrowserParentOpts } from "../browser-cli-shared.js";
|
||||
import { danger, defaultRuntime } from "../core-api.js";
|
||||
@@ -25,7 +24,7 @@ export function registerBrowserNavigationCommands(
|
||||
query: profile ? { profile } : undefined,
|
||||
body: {
|
||||
url,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
},
|
||||
{ timeoutMs: 20000 },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { runCommandWithRuntime } from "../core-api.js";
|
||||
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
|
||||
import { danger, defaultRuntime, shortenHomePath } from "./core-api.js";
|
||||
@@ -30,8 +29,8 @@ export function registerBrowserActionObserveCommands(
|
||||
method: "GET",
|
||||
path: "/console",
|
||||
query: {
|
||||
level: normalizeOptionalString(opts.level),
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
level: opts.level?.trim() || undefined,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
profile,
|
||||
},
|
||||
},
|
||||
@@ -59,7 +58,7 @@ export function registerBrowserActionObserveCommands(
|
||||
method: "POST",
|
||||
path: "/pdf",
|
||||
query: profile ? { profile } : undefined,
|
||||
body: { targetId: normalizeOptionalString(opts.targetId) },
|
||||
body: { targetId: opts.targetId?.trim() || undefined },
|
||||
},
|
||||
{ timeoutMs: 20000 },
|
||||
);
|
||||
@@ -98,7 +97,7 @@ export function registerBrowserActionObserveCommands(
|
||||
query: profile ? { profile } : undefined,
|
||||
body: {
|
||||
url,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs,
|
||||
maxChars,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { runCommandWithRuntime } from "../core-api.js";
|
||||
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
|
||||
import { danger, defaultRuntime, shortenHomePath } from "./core-api.js";
|
||||
@@ -60,8 +59,8 @@ function resolveDebugQuery(params: {
|
||||
filter?: unknown;
|
||||
}) {
|
||||
return {
|
||||
targetId: normalizeOptionalString(params.targetId),
|
||||
filter: normalizeOptionalString(params.filter),
|
||||
targetId: typeof params.targetId === "string" ? params.targetId.trim() || undefined : undefined,
|
||||
filter: typeof params.filter === "string" ? params.filter.trim() || undefined : undefined,
|
||||
clear: Boolean(params.clear),
|
||||
profile: params.profile,
|
||||
};
|
||||
@@ -84,7 +83,7 @@ export function registerBrowserDebugCommands(
|
||||
query: resolveProfileQuery(profile),
|
||||
body: {
|
||||
ref: ref.trim(),
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
if (printJsonResult(parent, result)) {
|
||||
@@ -190,7 +189,7 @@ export function registerBrowserDebugCommands(
|
||||
path: "/trace/start",
|
||||
query: resolveProfileQuery(profile),
|
||||
body: {
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
screenshots: Boolean(opts.screenshots),
|
||||
snapshots: Boolean(opts.snapshots),
|
||||
sources: Boolean(opts.sources),
|
||||
@@ -218,8 +217,8 @@ export function registerBrowserDebugCommands(
|
||||
path: "/trace/stop",
|
||||
query: resolveProfileQuery(profile),
|
||||
body: {
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
path: normalizeOptionalString(opts.out),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
path: opts.out?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
if (printJsonResult(parent, result)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
|
||||
import {
|
||||
danger,
|
||||
@@ -32,10 +31,10 @@ export function registerBrowserInspectCommands(
|
||||
path: "/screenshot",
|
||||
query: profile ? { profile } : undefined,
|
||||
body: {
|
||||
targetId: normalizeOptionalString(targetId),
|
||||
targetId: targetId?.trim() || undefined,
|
||||
fullPage: Boolean(opts.fullPage),
|
||||
ref: normalizeOptionalString(opts.ref),
|
||||
element: normalizeOptionalString(opts.element),
|
||||
ref: opts.ref?.trim() || undefined,
|
||||
element: opts.element?.trim() || undefined,
|
||||
type: opts.type === "jpeg" ? "jpeg" : "png",
|
||||
},
|
||||
},
|
||||
@@ -79,13 +78,13 @@ export function registerBrowserInspectCommands(
|
||||
try {
|
||||
const query: Record<string, string | number | boolean | undefined> = {
|
||||
format,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
limit: Number.isFinite(opts.limit) ? opts.limit : undefined,
|
||||
interactive: opts.interactive ? true : undefined,
|
||||
compact: opts.compact ? true : undefined,
|
||||
depth: Number.isFinite(opts.depth) ? opts.depth : undefined,
|
||||
selector: normalizeOptionalString(opts.selector),
|
||||
frame: normalizeOptionalString(opts.frame),
|
||||
selector: opts.selector?.trim() || undefined,
|
||||
frame: opts.frame?.trim() || undefined,
|
||||
labels: opts.labels ? true : undefined,
|
||||
mode,
|
||||
profile,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { callGatewayFromCli, type GatewayRpcOpts } from "./core-api.js";
|
||||
|
||||
export type BrowserParentOpts = GatewayRpcOpts & {
|
||||
@@ -76,7 +75,7 @@ export async function callBrowserResize(
|
||||
kind: "resize",
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
targetId: normalizeOptionalString(params.targetId),
|
||||
targetId: params.targetId?.trim() || undefined,
|
||||
},
|
||||
},
|
||||
extra,
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
|
||||
import { danger, defaultRuntime, inheritOptionFromParent } from "./core-api.js";
|
||||
|
||||
function resolveUrl(opts: { url?: string }, command: Command): string | undefined {
|
||||
return (
|
||||
normalizeOptionalString(opts.url) ??
|
||||
normalizeOptionalString(inheritOptionFromParent<string>(command, "url"))
|
||||
);
|
||||
if (typeof opts.url === "string" && opts.url.trim()) {
|
||||
return opts.url.trim();
|
||||
}
|
||||
const inherited = inheritOptionFromParent<string>(command, "url");
|
||||
if (typeof inherited === "string" && inherited.trim()) {
|
||||
return inherited.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveTargetId(rawTargetId: unknown, command: Command): string | undefined {
|
||||
return (
|
||||
normalizeOptionalString(rawTargetId) ??
|
||||
normalizeOptionalString(inheritOptionFromParent<string>(command, "targetId"))
|
||||
);
|
||||
const local = typeof rawTargetId === "string" ? rawTargetId.trim() : "";
|
||||
if (local) {
|
||||
return local;
|
||||
}
|
||||
const inherited = inheritOptionFromParent<string>(command, "targetId");
|
||||
if (typeof inherited !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = inherited.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
async function runMutationRequest(params: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { runCommandWithRuntime } from "../core-api.js";
|
||||
import { runBrowserResizeWithOutput } from "./browser-cli-resize.js";
|
||||
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
|
||||
@@ -92,7 +91,7 @@ export function registerBrowserStateCommands(
|
||||
path: "/set/offline",
|
||||
body: {
|
||||
offline,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: `offline: ${offline}`,
|
||||
});
|
||||
@@ -108,7 +107,8 @@ export function registerBrowserStateCommands(
|
||||
const parent = parentOpts(cmd);
|
||||
await runBrowserCommand(async () => {
|
||||
const headersJsonValue =
|
||||
normalizeOptionalString(opts.headersJson) ?? normalizeOptionalString(headersJson);
|
||||
(typeof opts.headersJson === "string" && opts.headersJson.trim()) ||
|
||||
(headersJson?.trim() ? headersJson.trim() : undefined);
|
||||
if (!headersJsonValue) {
|
||||
throw new Error("Missing headers JSON (pass --headers-json or positional JSON argument)");
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export function registerBrowserStateCommands(
|
||||
query: profile ? { profile } : undefined,
|
||||
body: {
|
||||
headers,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
},
|
||||
{ timeoutMs: 20000 },
|
||||
@@ -157,10 +157,10 @@ export function registerBrowserStateCommands(
|
||||
parent,
|
||||
path: "/set/credentials",
|
||||
body: {
|
||||
username: normalizeOptionalString(username),
|
||||
username: username?.trim() || undefined,
|
||||
password,
|
||||
clear: Boolean(opts.clear),
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: opts.clear ? "credentials cleared" : "credentials set",
|
||||
});
|
||||
@@ -184,9 +184,9 @@ export function registerBrowserStateCommands(
|
||||
latitude: Number.isFinite(latitude) ? latitude : undefined,
|
||||
longitude: Number.isFinite(longitude) ? longitude : undefined,
|
||||
accuracy: Number.isFinite(opts.accuracy) ? opts.accuracy : undefined,
|
||||
origin: normalizeOptionalString(opts.origin),
|
||||
origin: opts.origin?.trim() || undefined,
|
||||
clear: Boolean(opts.clear),
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: opts.clear ? "geolocation cleared" : "geolocation set",
|
||||
});
|
||||
@@ -212,7 +212,7 @@ export function registerBrowserStateCommands(
|
||||
path: "/set/media",
|
||||
body: {
|
||||
colorScheme,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: `media colorScheme: ${colorScheme}`,
|
||||
});
|
||||
@@ -230,7 +230,7 @@ export function registerBrowserStateCommands(
|
||||
path: "/set/timezone",
|
||||
body: {
|
||||
timezoneId,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: `timezone: ${timezoneId}`,
|
||||
});
|
||||
@@ -248,7 +248,7 @@ export function registerBrowserStateCommands(
|
||||
path: "/set/locale",
|
||||
body: {
|
||||
locale,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: `locale: ${locale}`,
|
||||
});
|
||||
@@ -266,7 +266,7 @@ export function registerBrowserStateCommands(
|
||||
path: "/set/device",
|
||||
body: {
|
||||
name,
|
||||
targetId: normalizeOptionalString(opts.targetId),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
successMessage: `device: ${name}`,
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { note } from "openclaw/plugin-sdk/browser-setup-tools";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
parseBrowserMajorVersion,
|
||||
readBrowserVersion,
|
||||
resolveGoogleChromeExecutableForPlatform,
|
||||
} from "./browser/chrome.executables.js";
|
||||
import type { OpenClawConfig } from "./config/config.js";
|
||||
import { asRecord } from "./record-shared.js";
|
||||
|
||||
const CHROME_MCP_MIN_MAJOR = 144;
|
||||
const REMOTE_DEBUGGING_PAGES = [
|
||||
@@ -15,6 +13,12 @@ const REMOTE_DEBUGGING_PAGES = [
|
||||
"edge://inspect/#remote-debugging",
|
||||
].join(", ");
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
type ExistingSessionProfile = {
|
||||
name: string;
|
||||
userDataDir?: string;
|
||||
@@ -27,7 +31,8 @@ function collectChromeMcpProfiles(cfg: OpenClawConfig): ExistingSessionProfile[]
|
||||
}
|
||||
|
||||
const profiles = new Map<string, ExistingSessionProfile>();
|
||||
const defaultProfile = normalizeOptionalString(browser.defaultProfile) ?? "";
|
||||
const defaultProfile =
|
||||
typeof browser.defaultProfile === "string" ? browser.defaultProfile.trim() : "";
|
||||
if (defaultProfile === "user") {
|
||||
profiles.set("user", { name: "user" });
|
||||
}
|
||||
@@ -39,12 +44,11 @@ function collectChromeMcpProfiles(cfg: OpenClawConfig): ExistingSessionProfile[]
|
||||
|
||||
for (const [profileName, rawProfile] of Object.entries(configuredProfiles)) {
|
||||
const profile = asRecord(rawProfile);
|
||||
const driver = normalizeOptionalString(profile?.driver) ?? "";
|
||||
const driver = typeof profile?.driver === "string" ? profile.driver.trim() : "";
|
||||
if (driver === "existing-session") {
|
||||
profiles.set(profileName, {
|
||||
name: profileName,
|
||||
userDataDir: normalizeOptionalString(profile?.userDataDir),
|
||||
});
|
||||
const userDataDir =
|
||||
typeof profile?.userDataDir === "string" ? profile.userDataDir.trim() : undefined;
|
||||
profiles.set(profileName, { name: profileName, userDataDir: userDataDir || undefined });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import {
|
||||
asNullableRecord,
|
||||
hasNonEmptyString as sharedHasNonEmptyString,
|
||||
isRecord,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
export { asNullableRecord as asRecord, isRecord };
|
||||
|
||||
export const hasNonEmptyString = sharedHasNonEmptyString;
|
||||
|
||||
export function normalizeString(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { formatCliCommand } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { isPrivateNetworkOptInEnabled, isPrivateIpAddress } from "openclaw/plugin-sdk/ssrf-policy";
|
||||
import { redactCdpUrl, resolveBrowserConfig, resolveProfile } from "./browser/config.js";
|
||||
import { resolveBrowserControlAuth } from "./browser/control-auth.js";
|
||||
import { hasNonEmptyString } from "./record-shared.js";
|
||||
|
||||
const BLOCKED_HOSTNAMES = new Set([
|
||||
"localhost",
|
||||
@@ -12,6 +11,10 @@ const BLOCKED_HOSTNAMES = new Set([
|
||||
"metadata.google.internal",
|
||||
]);
|
||||
|
||||
function hasNonEmptyString(value: unknown): boolean {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function isTrustedPrivateHostname(hostname: string): boolean {
|
||||
const normalized = hostname.trim().toLowerCase();
|
||||
return normalized.length > 0 && BLOCKED_HOSTNAMES.has(normalized);
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import { loginChutes } from "openclaw/plugin-sdk/provider-auth-login";
|
||||
import { readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
CHUTES_DEFAULT_MODEL_REF,
|
||||
applyChutesApiKeyConfig,
|
||||
@@ -83,7 +82,7 @@ async function runChutesOAuth(ctx: ProviderAuthContext): Promise<ProviderAuthRes
|
||||
access: creds.access,
|
||||
refresh: creds.refresh,
|
||||
expires: creds.expires,
|
||||
email: readStringValue(creds.email),
|
||||
email: typeof creds.email === "string" ? creds.email : undefined,
|
||||
credentialExtra: {
|
||||
clientId,
|
||||
...("accountId" in creds && typeof creds.accountId === "string"
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
type SsrFPolicy,
|
||||
} from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { isRecord, resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188";
|
||||
const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org";
|
||||
@@ -87,6 +87,10 @@ export function _setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard |
|
||||
comfyFetchGuard = impl ?? fetchWithSsrFGuard;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readConfigString(config: ComfyProviderConfig, key: string): string | undefined {
|
||||
const value = config[key];
|
||||
if (typeof value !== "string") {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
clearDeviceBootstrapTokens,
|
||||
definePluginEntry,
|
||||
@@ -140,7 +139,7 @@ const QR_CHANNEL_SENDERS: Record<string, QrChannelSender> = {
|
||||
};
|
||||
|
||||
function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null {
|
||||
const candidate = normalizeOptionalString(raw);
|
||||
const candidate = raw.trim();
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
@@ -148,7 +147,7 @@ function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null
|
||||
if (parsedUrl) {
|
||||
return parsedUrl;
|
||||
}
|
||||
const hostPort = normalizeOptionalString(candidate.split("/", 1)[0]) ?? "";
|
||||
const hostPort = candidate.split("/", 1)[0]?.trim() ?? "";
|
||||
return hostPort ? `${schemeFallback}://${hostPort}` : null;
|
||||
}
|
||||
|
||||
@@ -231,7 +230,7 @@ function pickMatchingIPv4(predicate: (address: string) => boolean): string | nul
|
||||
if (!entry || entry.internal || !isIpv4) {
|
||||
continue;
|
||||
}
|
||||
const address = normalizeOptionalString(entry.address) ?? "";
|
||||
const address = entry.address?.trim() ?? "";
|
||||
if (!address) {
|
||||
continue;
|
||||
}
|
||||
@@ -282,7 +281,10 @@ function resolveAuthLabel(cfg: OpenClawPluginApi["config"]): ResolveAuthLabelRes
|
||||
|
||||
function pickFirstDefined(candidates: Array<unknown>): string | null {
|
||||
for (const value of candidates) {
|
||||
const trimmed = normalizeOptionalString(value);
|
||||
if (typeof value !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
@@ -310,9 +312,8 @@ async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResu
|
||||
const scheme = resolveScheme(cfg);
|
||||
const port = resolveGatewayPort(cfg);
|
||||
|
||||
const configuredPublicUrl = normalizeOptionalString(pluginCfg.publicUrl);
|
||||
if (configuredPublicUrl) {
|
||||
const url = normalizeUrl(configuredPublicUrl, scheme);
|
||||
if (typeof pluginCfg.publicUrl === "string" && pluginCfg.publicUrl.trim()) {
|
||||
const url = normalizeUrl(pluginCfg.publicUrl, scheme);
|
||||
if (url) {
|
||||
return { url, source: "plugins.entries.device-pair.config.publicUrl" };
|
||||
}
|
||||
@@ -328,8 +329,8 @@ async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResu
|
||||
return { url: `wss://${host}`, source: `gateway.tailscale.mode=${tailscaleMode}` };
|
||||
}
|
||||
|
||||
const remoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url);
|
||||
if (remoteUrl) {
|
||||
const remoteUrl = cfg.gateway?.remote?.url;
|
||||
if (typeof remoteUrl === "string" && remoteUrl.trim()) {
|
||||
const url = normalizeUrl(remoteUrl, scheme);
|
||||
if (url) {
|
||||
return { url, source: "gateway.remote.url" };
|
||||
@@ -477,19 +478,14 @@ function canSendQrPngToChannel(channel: string): boolean {
|
||||
|
||||
function resolveQrReplyTarget(ctx: QrCommandContext): string {
|
||||
if (ctx.channel === "discord") {
|
||||
const senderId = normalizeOptionalString(ctx.senderId) ?? "";
|
||||
const senderId = ctx.senderId?.trim() ?? "";
|
||||
if (senderId) {
|
||||
return senderId.startsWith("user:") || senderId.startsWith("channel:")
|
||||
? senderId
|
||||
: `user:${senderId}`;
|
||||
}
|
||||
}
|
||||
return (
|
||||
normalizeOptionalString(ctx.senderId) ||
|
||||
normalizeOptionalString(ctx.from) ||
|
||||
normalizeOptionalString(ctx.to) ||
|
||||
""
|
||||
);
|
||||
return ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
|
||||
}
|
||||
|
||||
const PAIR_SETUP_NON_ISSUING_ACTIONS = new Set([
|
||||
@@ -525,7 +521,7 @@ async function sendQrPngToSupportedChannel(params: {
|
||||
qrFilePath: string;
|
||||
}): Promise<boolean> {
|
||||
const mediaLocalRoots = [path.dirname(params.qrFilePath)];
|
||||
const accountId = normalizeOptionalString(params.ctx.accountId) || undefined;
|
||||
const accountId = params.ctx.accountId?.trim() || undefined;
|
||||
const sender = QR_CHANNEL_SENDERS[params.ctx.channel];
|
||||
if (!sender) {
|
||||
return false;
|
||||
@@ -561,7 +557,7 @@ export default definePluginEntry({
|
||||
description: "Generate setup codes and approve device pairing requests.",
|
||||
acceptsArgs: true,
|
||||
handler: async (ctx) => {
|
||||
const args = normalizeOptionalString(ctx.args) ?? "";
|
||||
const args = ctx.args?.trim() ?? "";
|
||||
const tokens = args.split(/\s+/).filter(Boolean);
|
||||
const action = tokens[0]?.toLowerCase() ?? "";
|
||||
const gatewayClientScopes = Array.isArray(ctx.gatewayClientScopes)
|
||||
@@ -583,7 +579,7 @@ export default definePluginEntry({
|
||||
}
|
||||
|
||||
if (action === "notify") {
|
||||
const notifyAction = normalizeOptionalString(tokens[1])?.toLowerCase() ?? "status";
|
||||
const notifyAction = tokens[1]?.trim().toLowerCase() ?? "status";
|
||||
return await handleNotifyCommand({
|
||||
api,
|
||||
ctx,
|
||||
@@ -598,7 +594,7 @@ export default definePluginEntry({
|
||||
const list = await listDevicePairing();
|
||||
const selected = selectPendingApprovalRequest({
|
||||
pending: list.pending,
|
||||
requested: normalizeOptionalString(tokens[1]),
|
||||
requested: tokens[1]?.trim(),
|
||||
});
|
||||
if (selected.reply) {
|
||||
return selected.reply;
|
||||
@@ -748,11 +744,7 @@ export default definePluginEntry({
|
||||
};
|
||||
}
|
||||
const channel = ctx.channel;
|
||||
const target =
|
||||
normalizeOptionalString(ctx.senderId) ||
|
||||
normalizeOptionalString(ctx.from) ||
|
||||
normalizeOptionalString(ctx.to) ||
|
||||
"";
|
||||
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
|
||||
const payload = await issueSetupPayload(urlResult.url);
|
||||
|
||||
if (channel === "telegram" && target) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { OpenClawPluginApi } from "./api.js";
|
||||
import { listDevicePairing } from "./api.js";
|
||||
|
||||
@@ -42,7 +41,7 @@ function formatStringList(values?: readonly string[]): string {
|
||||
}
|
||||
|
||||
function formatRoleList(request: PendingPairingRequest): string {
|
||||
const role = normalizeOptionalString(request.role);
|
||||
const role = request.role?.trim();
|
||||
if (role) {
|
||||
return role;
|
||||
}
|
||||
@@ -59,9 +58,9 @@ export function formatPendingRequests(pending: PendingPairingRequest[]): string
|
||||
}
|
||||
const lines: string[] = ["Pending device pairing requests:"];
|
||||
for (const req of pending) {
|
||||
const label = normalizeOptionalString(req.displayName) || req.deviceId;
|
||||
const platform = normalizeOptionalString(req.platform);
|
||||
const ip = normalizeOptionalString(req.remoteIp);
|
||||
const label = req.displayName?.trim() || req.deviceId;
|
||||
const platform = req.platform?.trim();
|
||||
const ip = req.remoteIp?.trim();
|
||||
const parts = [
|
||||
`- ${req.requestId}`,
|
||||
label ? `name=${label}` : null,
|
||||
@@ -93,14 +92,17 @@ function normalizeNotifyState(raw: unknown): NotifyStateFile {
|
||||
continue;
|
||||
}
|
||||
const record = item as Record<string, unknown>;
|
||||
const to = normalizeOptionalString(record.to) ?? "";
|
||||
const to = typeof record.to === "string" ? record.to.trim() : "";
|
||||
if (!to) {
|
||||
continue;
|
||||
}
|
||||
const accountId = normalizeOptionalString(record.accountId) ?? undefined;
|
||||
const accountId =
|
||||
typeof record.accountId === "string" && record.accountId.trim()
|
||||
? record.accountId.trim()
|
||||
: undefined;
|
||||
const messageThreadId =
|
||||
typeof record.messageThreadId === "string"
|
||||
? normalizeOptionalString(record.messageThreadId) || undefined
|
||||
? record.messageThreadId.trim() || undefined
|
||||
: typeof record.messageThreadId === "number" && Number.isFinite(record.messageThreadId)
|
||||
? Math.trunc(record.messageThreadId)
|
||||
: undefined;
|
||||
@@ -120,14 +122,13 @@ function normalizeNotifyState(raw: unknown): NotifyStateFile {
|
||||
|
||||
const notifiedRequestIds: Record<string, number> = {};
|
||||
for (const [requestId, ts] of Object.entries(notifiedRaw)) {
|
||||
const normalizedRequestId = normalizeOptionalString(requestId);
|
||||
if (!normalizedRequestId) {
|
||||
if (!requestId.trim()) {
|
||||
continue;
|
||||
}
|
||||
if (typeof ts !== "number" || !Number.isFinite(ts) || ts <= 0) {
|
||||
continue;
|
||||
}
|
||||
notifiedRequestIds[normalizedRequestId] = Math.trunc(ts);
|
||||
notifiedRequestIds[requestId] = Math.trunc(ts);
|
||||
}
|
||||
|
||||
return { subscribers, notifiedRequestIds };
|
||||
@@ -169,11 +170,7 @@ function resolveNotifyTarget(ctx: {
|
||||
accountId?: string;
|
||||
messageThreadId?: string | number;
|
||||
}): NotifyTarget | null {
|
||||
const to =
|
||||
normalizeOptionalString(ctx.senderId) ||
|
||||
normalizeOptionalString(ctx.from) ||
|
||||
normalizeOptionalString(ctx.to) ||
|
||||
"";
|
||||
const to = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
|
||||
if (!to) {
|
||||
return null;
|
||||
}
|
||||
@@ -209,9 +206,9 @@ function upsertNotifySubscriber(
|
||||
}
|
||||
|
||||
function buildPairingRequestNotificationText(request: PendingPairingRequest): string {
|
||||
const label = normalizeOptionalString(request.displayName) || request.deviceId;
|
||||
const platform = normalizeOptionalString(request.platform);
|
||||
const ip = normalizeOptionalString(request.remoteIp);
|
||||
const label = request.displayName?.trim() || request.deviceId;
|
||||
const platform = request.platform?.trim();
|
||||
const ip = request.remoteIp?.trim();
|
||||
const role = formatRoleList(request);
|
||||
const scopes = formatScopeList(request);
|
||||
const lines = [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { approveDevicePairing, listDevicePairing } from "./api.js";
|
||||
import { formatPendingRequests } from "./notify.js";
|
||||
|
||||
@@ -44,8 +43,8 @@ export function selectPendingApprovalRequest(params: {
|
||||
}
|
||||
|
||||
function formatApprovedPairingReply(approved: ApprovedPairingEntry): { text: string } {
|
||||
const label = normalizeOptionalString(approved.device.displayName) || approved.device.deviceId;
|
||||
const platform = normalizeOptionalString(approved.device.platform);
|
||||
const label = approved.device.displayName?.trim() || approved.device.deviceId;
|
||||
const platform = approved.device.platform?.trim();
|
||||
const platformLabel = platform ? ` (${platform})` : "";
|
||||
return { text: `✅ Paired ${label}${platformLabel}.` };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { chromium } from "playwright-core";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import type { DiffRenderOptions, DiffTheme } from "./types.js";
|
||||
@@ -256,7 +255,7 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter {
|
||||
if (error instanceof Error && error.message === IMAGE_SIZE_LIMIT_ERROR) {
|
||||
throw error;
|
||||
}
|
||||
const reason = formatErrorMessage(error);
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`Diff PNG/PDF rendering requires a Chromium-compatible browser. Set browser.executablePath or install Chrome/Chromium. ${reason}`,
|
||||
{ cause: error },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { PluginLogger } from "../api.js";
|
||||
import type { DiffArtifactContext, DiffArtifactMeta, DiffOutputFormat } from "./types.js";
|
||||
|
||||
@@ -380,3 +379,7 @@ function normalizeArtifactContext(value: unknown): DiffArtifactContext | undefin
|
||||
|
||||
return Object.values(context).some((entry) => entry !== undefined) ? context : undefined;
|
||||
}
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import type { AnyAgentTool, OpenClawPluginApi, OpenClawPluginToolContext } from "../api.js";
|
||||
import { PlaywrightDiffScreenshotter, type DiffScreenshotter } from "./browser.js";
|
||||
import { resolveDiffImageRenderOptions } from "./config.js";
|
||||
@@ -296,18 +295,19 @@ export function createDiffsTool(params: {
|
||||
};
|
||||
} catch (error) {
|
||||
if (mode === "both") {
|
||||
const errorMessage = formatErrorMessage(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Diff viewer ready.\n${viewerUrl}\nFile rendering failed: ${errorMessage}`,
|
||||
text:
|
||||
`Diff viewer ready.\n${viewerUrl}\n` +
|
||||
`File rendering failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
...baseDetails,
|
||||
fileError: errorMessage,
|
||||
imageError: errorMessage,
|
||||
fileError: error instanceof Error ? error.message : String(error),
|
||||
imageError: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { DIFF_INDICATORS, DIFF_LAYOUTS, DIFF_THEMES } from "./types.js";
|
||||
import type { DiffViewerPayload } from "./types.js";
|
||||
|
||||
@@ -86,6 +85,10 @@ function isViewerOptions(value: unknown): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function includesValue<T extends readonly string[]>(values: T, value: unknown): value is T[number] {
|
||||
return typeof value === "string" && values.includes(value as T[number]);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
"description": "OpenClaw Discord channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@buape/carbon": "0.14.0",
|
||||
"@buape/carbon": "0.0.0-beta-20260406003433",
|
||||
"@discordjs/voice": "^0.19.2",
|
||||
"@snazzah/davey": "^0.1.11",
|
||||
"discord-api-types": "^0.38.44",
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"opusscript": "^0.1.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@discordjs/opus": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"openclaw": "workspace:*"
|
||||
},
|
||||
@@ -22,9 +25,6 @@
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@discordjs/opus": "^0.10.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
mergeDiscordAccountConfig,
|
||||
resolveDefaultDiscordAccountId,
|
||||
@@ -67,7 +66,7 @@ export function inspectDiscordAccount(params: {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
name: merged.name?.trim() || undefined,
|
||||
token: accountToken.token,
|
||||
tokenSource: accountToken.tokenSource,
|
||||
tokenStatus: accountToken.tokenStatus,
|
||||
@@ -79,7 +78,7 @@ export function inspectDiscordAccount(params: {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
name: merged.name?.trim() || undefined,
|
||||
token: "",
|
||||
tokenSource: "none",
|
||||
tokenStatus: "missing",
|
||||
@@ -93,7 +92,7 @@ export function inspectDiscordAccount(params: {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
name: merged.name?.trim() || undefined,
|
||||
token: channelToken.token,
|
||||
tokenSource: channelToken.tokenSource,
|
||||
tokenStatus: channelToken.tokenStatus,
|
||||
@@ -110,7 +109,7 @@ export function inspectDiscordAccount(params: {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
name: merged.name?.trim() || undefined,
|
||||
token: envToken.replace(/^Bot\s+/i, ""),
|
||||
tokenSource: "env",
|
||||
tokenStatus: "available",
|
||||
@@ -122,7 +121,7 @@ export function inspectDiscordAccount(params: {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
name: merged.name?.trim() || undefined,
|
||||
token: "",
|
||||
tokenSource: "none",
|
||||
tokenStatus: "missing",
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from "openclaw/plugin-sdk/account-helpers";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { DiscordAccountConfig, DiscordActionConfig, OpenClawConfig } from "./runtime-api.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
@@ -70,7 +69,7 @@ export function resolveDiscordAccount(params: {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
name: merged.name?.trim() || undefined,
|
||||
token: tokenResolution.token,
|
||||
tokenSource: tokenResolution.source,
|
||||
config: merged,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
DiscordGuildChannelConfig,
|
||||
DiscordGuildEntry,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import { fetchChannelPermissionsDiscord } from "./send.js";
|
||||
@@ -125,7 +124,7 @@ export async function auditDiscordChannelPermissions(params: {
|
||||
channels.push({
|
||||
channelId,
|
||||
ok: false,
|
||||
error: formatErrorMessage(err),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
matchKey: channelId,
|
||||
matchSource: "id",
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
ChannelMessageToolDiscovery,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { DiscordActionConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
||||
import {
|
||||
createDiscordActionGate,
|
||||
listEnabledDiscordAccounts,
|
||||
@@ -170,7 +169,8 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
|
||||
extractToolSend: ({ args }) => {
|
||||
const action = typeof args.action === "string" ? args.action.trim() : "";
|
||||
if (action === "sendMessage") {
|
||||
return extractToolSend(args, "sendMessage");
|
||||
const to = typeof args.to === "string" ? args.to : undefined;
|
||||
return to ? { to } : null;
|
||||
}
|
||||
if (action === "threadReply") {
|
||||
const channelId = typeof args.channelId === "string" ? args.channelId.trim() : "";
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
createChannelDirectoryAdapter,
|
||||
createRuntimeDirectoryLiveAdapter,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-runtime";
|
||||
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -677,7 +676,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
const message = formatErrorMessage(err);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
details.permissions = { channelId: parsedTarget.id, error: message };
|
||||
return {
|
||||
details,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user