The scoped outbound media tests added in #92872 assert raw mkdtemp paths,
but prod resolvers canonicalize roots, so on macOS the /var -> /private/var
tmpdir symlink made three tests fail (green on Linux CI). Realpath the tmp
roots like sibling tests, and export the real UploadDailyLimitExceededError
from the outbound-dispatch sender.js mock so prod instanceof checks in error
paths hit real assertions instead of vitest's missing-export guard.
Also add AGENTS.md test guidance covering both patterns.
* fix(qqbot): allow scoped sandbox media sends
* fix(qqbot): thread scoped media access through all outbound paths
Cover the seven QQBot outbound media paths that dropped core scoped
sandbox media access: gateway block/tool/streaming deliver, direct
text qqmedia tags, structured QQBOT_PAYLOAD, and the scoped workspace
relative/missing-voice path resolution. The low-level sendPhoto /
sendVideoMsg / sendDocument now consume core mediaAccess.readFile via
the shared outbound loader with explicit localRoots guard, so
host-mediated workspace media can be sent without widening QQBot FS
access. Voice stays on the existing SILK transcode path to avoid
bypassing QQ-required format conversion.
* fix(qqbot): thread workspaceDir into gateway reply media context
Gateway dispatchOutbound previously built gatewayMediaContext with only
mediaLocalRoots, so a sandboxed block reply that carried a relative
media tag (e.g. <qqmedia>report.docx</qqmedia>) reached the downstream
sender without mediaAccess.workspaceDir and was rejected by containment
checks before the file could be resolved against the agent workspace.
Resolve the agent workspace dir via the public plugin-sdk agent-runtime
helper and pass it through mediaAccess.workspaceDir alongside the
existing mediaLocalRoots so the gateway block reply, tool forwarding,
QQBOT_PAYLOAD, and official C2C streaming paths all share the same
scoped workspace resolution as direct outbound sends. No public plugin
SDK boundary is widened; the helper is an existing export.
Add a regression test that exercises a relative <qqmedia> tag in a
gateway block reply and confirms it resolves against the configured
agent workspace.
* fix(qqbot): wrap host-read media sends with structured error handling
* fix(qqbot): preserve host-read media quota errors
* fix(qqbot): preserve host-read quota fallback path
* fix(qqbot): resolve host-read quota fallback path
* test(qqbot): satisfy host-read media lint
* fix(qqbot): cover host-read voice media
* fix(qqbot): satisfy host-read voice lint
* Drop incidental formatting from PR merge
Keep the conflict-resolution update scoped to the QQBot media send files.
* fix(qqbot): map sandbox workspace media paths
* fix(qqbot): preserve host-read media roots
* fix(qqbot): scope sandbox media delivery roots
* fix(qqbot): map virtual media roots before host read
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(qqbot): scope structured payload media roots
* fix(qqbot): keep auto media helper internal
* docs(changelog): note QQBot scoped sandbox media fix (#92872) (thanks @zhangguiping-xydt)
---------
Co-authored-by: sliverp <870080352@qq.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
splitTableCells/splitPartialTableCells split on every '|', including a GFM
backslash-escaped pipe ('\|'), which is literal cell content rather than a
column delimiter. A cell containing '\|' was therefore mis-counted as multiple
columns, so the oversized-row fallback (renderTableRowAsFields) rendered the
trailing content under the wrong header.
Split via an escape-aware scan that treats '\|' as a literal '|' (and '\\' as
a literal backslash so a following '|' still delimits). Behavior is byte-for-byte
unchanged for any row without an escaped pipe, so existing chunking is preserved.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
generateJobName truncated reminder content with String.slice(0, 20) on
a UTF-16 code-unit index, so an astral character (e.g. an emoji) landing
on the boundary was cut into a lone surrogate, producing a malformed
cron job name.
Truncate with the shared truncateUtf16Safe helper so a surrogate pair is
never split, keeping the existing 20-unit budget and ellipsis suffix.
Adds a test asserting the truncated job name contains no lone surrogate.
* chore(release): close out 2026.6.10 on main
* chore(release): align native app metadata for 2026.6.10
* chore(release): sync Android 2026.6.10 notes
* docs(changelog): preserve 2026.6.9 history
* docs(changelog): preserve 2026.6.9 history
Gate the QQ Bot symlink-media helper regression test on actual file-symlink capability, so environments that cannot create file symlinks skip that specific test while capable hosts still run it.
Validation:
- Windows Vitest proof in the PR body: `extensions/qqbot/src/engine/utils/file-utils.test.ts` passed with 4 tests passed and 1 symlink test skipped when file symlinks were unavailable.
- Current CI is clean at `cb7d5a162e24f7ec5be6985e97b2b74ae45b20f9`, including the refreshed Real behavior proof run `27992101343`.
Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
`isTableSeparatorLine` required 3+ dashes per cell (`/^:?-{3,}:?$/`), but a
GFM delimiter cell needs only one or more dashes. So a valid table whose
separator used 1 or 2 dashes (e.g. `|--|--|`) was not recognized: the header
stayed pending and was silently overwritten by each following row, so the
table's header, separator, and every row but the last vanished from the sent
message.
Accept `-+` so valid GFM separators are recognized, matching the spec and the
sibling LINE channel. Every existing test separator already uses 3+ dashes, so
they are byte-identical.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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