QQBot is the only channel that root-sandboxes outbound local files. Its three
gate sites (resolveOutboundMediaPath, the voice send re-check, and
structured-payload validation) only trusted the QQ Bot media storage roots, so
framework-generated scratch media written under OpenClaw's hardened temp root
(e.g. cron auto-TTS voice files from speech-core) was rejected. The send then
returned a no-identity error, the message was silently lost, yet cron still
recorded it as delivered.
Add one shared resolver (resolveTrustedOutboundMediaPath) that also trusts the
preferred OpenClaw temp root — already a sanctioned media root in core
(buildMediaLocalRoots) — and route all three gates through it so the trust set
agrees everywhere. Fixes#92816.
Co-authored-by: zengwen <zeng_wen@foxmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(qqbot): add /bot-group-allways command to toggle group mention requirement
Add slash command to configure defaultRequireMention for qqbot accounts.
Clear runtime config snapshot cache after config write to ensure
getRuntimeConfig() reads fresh values on next message.
- Add register-group-allways command (on/off/status)
- Support named accounts and default account
- Clear runtime config cache after write for immediate effect
- Add unit tests for group config resolution
* test(qqbot): fix group allways test imports
* feat(qqbot): add /bot-group-allways command to toggle group mention requirement (#91423) (thanks @cxyhhhhh)
---------
Co-authored-by: sliverp <870080352@qq.com>
* fix(qqbot): migrate group tool policy config
* test: stabilize changed check lanes
* style: format changed main files
* test: align CI matrix expectations
Summary:
- Adds QQBot outbound `sanitizeText` wired to `sanitizeAssistantVisibleText` plus a regression test for stripping `<thinking>` and `<think>` blocks.
- PR surface: Source +2, Tests +19. Total +21 across 2 files.
- Reproducibility: yes. source-reproducible: current main QQBot outbound lacks `sanitizeText`, and shared deli ... nnel text sanitization when that hook exists. I did not run a live Tencent QQBot plus MiniMax reproduction.
Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(qqbot): add curly braces for eslint(curly) compliance
Validation:
- ClawSweeper review passed for head 17cf140183.
- Required merge gates passed before the squash merge.
Prepared head SHA: 17cf140183
Review: https://github.com/openclaw/openclaw/pull/90132#issuecomment-4618527026
Co-authored-by: openperf <16864032@qq.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
Move QQBot credential backups, gateway sessions, known-user records, and ref-index rows into plugin SQLite KV stores. Import shipped JSON/JSONL state files on first use and keep auxiliary known-user/ref-index state best-effort so message delivery is not blocked by cache persistence failures.
QQBot credential backups now resolve under the active OpenClaw state directory instead of the old home-global QQBot data path. This keeps isolated gateway profiles from restoring each other's QQBot appId/clientSecret backups while preserving per-state-root recovery.
Proof: focused QQBot path/storage-laziness Vitest suite passed on Node 24.15.0, focused oxlint passed, source-runtime two-root backup proof passed, exact-head CI run 26814565282 passed, and ClawSweeper re-review run 26815054980 marked proof sufficient.
Closes#84313.
Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>
* fix(qqbot): allow RFC2544 benchmark range for token fetch (#88984)
QQ Bot `bots.qq.com` token-fetch path was failing for users whose DNS resolver maps the hostname into the RFC 2544 benchmark range `198.18.0.0/15` (commonly seen with fake-IP proxy stacks: sing-box, Clash, Surge, WSL2 DNS). The default SSRF guard treats that range as private and blocks the request, surfacing as "Network error getting access_token: Blocked: resolves to private/internal/special-use IP address".
Pass a host-scoped `SsrFPolicy` (`allowRfc2544BenchmarkRange: true`) to the single hard-coded `TOKEN_URL` request, mirroring the existing `QQBOT_MEDIA_SSRF_POLICY` pattern used by the media path. Because `TOKEN_URL` is a const and not user-controlled, the relaxation cannot widen attack surface to other hosts.
Adds a regression test asserting `policy: { allowRfc2544BenchmarkRange: true }` is forwarded into `fetchWithSsrFGuard`, and updates the existing equality assertion accordingly.
Fixes#88984
* fix(qqbot): scope token ssrf policy
Bumps OpenClaw release metadata to 2026.5.31 across package manifests, app version files, plugin metadata, changelog headings, and generated shrinkwraps.
Verification:
- pnpm plugins:sync:check
- pnpm ios:version:check
- pnpm deps:shrinkwrap:check
- git diff --check
- stale 2026.5.30/build-code scan across changed files
- autoreview clean: no accepted/actionable findings
- PR CI green for real gates: Checks, security scans, dependency guard, app lanes, real behavior proof
Known non-code workflow issue:
- label workflow failed because this PR hits GitHub's 100-label issue cap before the size-label step.
* feat(browser): add optional vision understanding to screenshot tool
* fix(browser): wrap vision output as external content, enforce maxBytes, forward auth profiles
* fix(browser): remove no-op scope/attachments config, drop profile pass-through lacking runtime support
* feat(media-understanding): add profile/preferredProfile to DescribeImageFileWithModelParams and forward to describeImage
* style(browser): add curly braces to satisfy eslint curly rule
* fix(browser): correct tools.browser.enabled help text to match actual behavior
* fix(browser): thread agentDir/workspaceDir from plugin tool context into browser vision
* refactor(browser): move vision config from tools.browser to browser.models
The browser plugin's vision configuration now lives on the top-level
`browser` config namespace (browser.models, browser.visionEnabled,
browser.visionPrompt, etc.) instead of `tools.browser`. This aligns
with the plugin's existing config location and avoids confusion between
tool-level and plugin-level settings.
- Remove tools.browser from ToolsSchema and ToolsConfig
- Add models/vision* fields to BrowserConfig and its zod schema
- Update getBrowserVisionConfig to read from cfg.browser
- Update schema help, labels, and quality test
- Update vision.test.ts to use new config shape
* docs(browser): add screenshot vision configuration section
Document the new browser.models config for automatic screenshot
description via vision models, enabling text-only main models to
reason about web page content.
* fix(browser): remove deliverable media markers from vision result, drop unused import
P1: Vision-success path no longer exposes the raw screenshot as
deliverable media (removes MEDIA: line and details.media.mediaUrl).
This prevents channel delivery from auto-sending sensitive page content
when the intended output is a text description.
P2: Remove unused ToolsMediaUnderstandingSchema import that would fail
noUnusedLocals typecheck.
* fix(browser): add command/args fields to browser models schema
The browser vision model schema uses .strict(), so CLI-type entries
with command/args were rejected by TypeScript. Add these fields to
align with MediaUnderstandingModelSchema.
* chore(browser): remove debug console.log statements
* fix(browser): harden screenshot vision result against MEDIA: directive injection and restore image sanitization on failure fallback
ClawSweeper #84247 review round 2:
P1 (security, high): neutralize line-start MEDIA: directives in vision descriptions
before wrapping with wrapExternalContent. The agent media extractor scans every
browser tool-result text block via splitMediaFromOutput which treats line-start
MEDIA: as a trusted local-media delivery directive, and browser is on the
trusted-media allowlist. Without neutralization, page or vision-provider output
containing 'MEDIA:/tmp/secret.png' could synthesize a channel-deliverable media
artifact from untrusted content. wrapExternalContent itself does not strip
line-start directives. Introduce neutralizeMediaDirectives in vision.ts that
prepends '[neutralized] ' to any line whose trimStart() begins with MEDIA:
(case-insensitive), defanging the parser anchor while keeping the original
text human-readable.
P2 (compatibility): pass resolveRuntimeImageSanitization() to imageResultFromFile
in the vision-failure catch fallback. The non-vision screenshot path already
forwards this option (d5cc0d53b7) so configured agents.defaults.imageMaxDimensionPx
takes effect. Without this fix, any provider timeout/error silently bypasses the
sanitization guard and returns a raw full-resolution screenshot.
Regression coverage:
- vision.test.ts: 6 unit cases for neutralizeMediaDirectives (no-op fast path,
mid-line MEDIA: untouched, line-start defanged, leading-whitespace defanged,
case-insensitive, multiple directives per blob).
- browser-tool.test.ts: 2 integration cases that drive the full screenshot
tool execute path:
- 'neutralizes MEDIA: directives in vision text and does not attach media'
asserts no line matches /^\s*MEDIA:/i in returned text, secret path text
is preserved verbatim, details.media is absent, and imageResultFromFile
is not called on the success path.
- 'preserves screenshot image sanitization on vision failure fallback'
mocks describeImageFileWithModel to reject and asserts the fallback
imageResultFromFile call receives imageSanitization: {maxDimensionPx:1600}
plus the 'browser screenshot vision failed' extraText.
* fix(browser): apply clawsweeper fallback media fix from PR #84247
* refactor: reuse media image understanding for browser screenshots
* refactor: use structured media delivery
* test: update music completion media instruction expectation
* fix: trim buffered reply directive padding
* test: refresh codex prompt snapshots for message media aliases
---------
Co-authored-by: scotthuang <scotthuang@tencent.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Fixes#66509.
QQBot now sends text-only tool progress immediately when partial streaming is enabled instead of buffering it until a fallback timer that is cleared by the final block. Immediate progress uses QQ plain-text sends so markdown-enabled accounts do not reinterpret media-like progress text, while streaming-off behavior remains final-only.
Thanks @gabrielduartesignart for the report.
Co-authored-by: samzong <samzong.lu@gmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>