mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 21:52:53 +00:00
* 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>
60 lines
1.6 KiB
TypeScript
60 lines
1.6 KiB
TypeScript
import { splitMediaFromOutput } from "../../media/parse.js";
|
|
import { parseInlineDirectives } from "../../utils/directive-tags.js";
|
|
import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
|
|
|
export type ReplyDirectiveParseResult = {
|
|
text: string;
|
|
mediaUrls?: string[];
|
|
mediaUrl?: string;
|
|
replyToId?: string;
|
|
replyToCurrent?: boolean;
|
|
replyToTag: boolean;
|
|
audioAsVoice?: boolean;
|
|
isSilent: boolean;
|
|
};
|
|
|
|
export type ReplyDirectiveParseOptions = {
|
|
currentMessageId?: string;
|
|
silentToken?: string;
|
|
extractMarkdownImages?: boolean;
|
|
extractMediaDirectives?: boolean;
|
|
};
|
|
|
|
export function parseReplyDirectives(
|
|
raw: string,
|
|
options: ReplyDirectiveParseOptions = {},
|
|
): ReplyDirectiveParseResult {
|
|
const split = splitMediaFromOutput(raw, {
|
|
extractMarkdownImages: options.extractMarkdownImages,
|
|
extractMediaDirectives: options.extractMediaDirectives,
|
|
});
|
|
let text = split.text ?? "";
|
|
|
|
const replyParsed = parseInlineDirectives(text, {
|
|
currentMessageId: options.currentMessageId,
|
|
stripAudioTag: false,
|
|
stripReplyTags: true,
|
|
});
|
|
|
|
if (replyParsed.hasReplyTag) {
|
|
text = replyParsed.text;
|
|
}
|
|
|
|
const silentToken = options.silentToken ?? SILENT_REPLY_TOKEN;
|
|
const isSilent = isSilentReplyPayloadText(text, silentToken);
|
|
if (isSilent) {
|
|
text = "";
|
|
}
|
|
|
|
return {
|
|
text,
|
|
mediaUrls: split.mediaUrls,
|
|
mediaUrl: split.mediaUrl,
|
|
replyToId: replyParsed.replyToId,
|
|
replyToCurrent: replyParsed.replyToCurrent || undefined,
|
|
replyToTag: replyParsed.hasReplyTag,
|
|
audioAsVoice: split.audioAsVoice,
|
|
isSilent,
|
|
};
|
|
}
|