diff --git a/docs/providers-and-models.md b/docs/providers-and-models.md index d5b6d3db0..2f34534e7 100644 --- a/docs/providers-and-models.md +++ b/docs/providers-and-models.md @@ -6,13 +6,28 @@ **Settings → Providers** selects the main model for conversation, planning and final replies. **Settings → Assistive Models** groups Vision (including screenshot -limits and redaction), Speech to text, Jev (TypeSafe), and SafeSocial. Configuring an assistive +limits and redaction), Speech to text, Generative media (fal.ai, powering the +`generate_image` agent tool), Jev (TypeSafe), and SafeSocial. Configuring an assistive model does not replace the active provider. Jev is outside the dynamic provider list; its verification, fast-classification and experimental browser switches are independent opt-ins. See [the settings guide](https://webbrain.one/docs/settings/#multimodal) and [data flow](privacy-and-data-flow.md#optional-jev-typesafe-scheduled-task-verification) for setup and disclosure details. Existing `#multimodal` settings links still work. +### Generative media (fal.ai) + +The `generate_image` agent tool submits a text prompt to fal.ai's queue API +(`agent/fal-media.js`) and returns the hosted media URL. Configure the API key +and model under **Settings → Assistive Models → Generative media (fal.ai)**. +The settings card offers **Test Connection** (a cheap auth probe that never +generates media) and per-card **Save / Clear** controls. The tool is a full-tier +Act-mode tool: it is **not** offered in Ask mode or on the compact/mid normal +tool surface, so use a Full-tier provider in Act mode to exercise it. + +The Act system prompt and the planner tool catalog tell the model to call +`generate_image` whenever the user asks to generate media, rather than +navigating to third-party image sites. + ### SafeSocial image classifier (experimental) **Settings → Assistive Models → SafeSocial** optionally filters Instagram images @@ -81,39 +96,39 @@ class BaseLLMProvider { ## Built-in Providers -| Provider ID | Type | Category | Default Model | Vision | -|---|---|---|---|---| -| `webbrain_cloud` | `openai` | cloud | `webbrain-cloud 1.0` | Yes | -| `llamacpp` | `llamacpp` | local | (loaded model) | Auto metadata / override | -| `ollama` | `openai` | local | (loaded model) | Auto via `/api/show` / override | -| `lmstudio` | `openai` | local | (loaded model) | Auto metadata / override | -| `jan` | `openai` | local | (loaded model) | Yes (default on) | -| `vllm` | `openai` | local | (loaded model) | Yes (default on) | -| `sglang` | `openai` | local | (loaded model) | Yes (default on) | -| `localai` | `openai` | local | (loaded model) | Auto metadata / override | -| `gpt4all` | `openai` | local | (loaded model) | Yes (default on) | -| `local_openai_proxy` | `openai` | local | (required) | Off / manual toggle | -| `unsloth` | `openai` | local | (required) | Off / manual toggle | -| `webgpu` (Chromium) | `webgpu` | local | Compass Tiny v2.1 (only preset); experimental custom HF ONNX repos | No | -| `azure_openai` | `azure_openai` | cloud | (deployment) | Manual toggle | -| `aws_bedrock` | `aws_bedrock` | cloud | (model id) | No | -| `openai` | `openai` | cloud | `gpt-5.6-terra` | Model-name regex | -| `anthropic` | `anthropic` | cloud | `claude-sonnet-4-6` | Model-name regex | -| `gemini` | `openai` | cloud | `gemini-3.1-flash` | Model-name regex | -| `cloudflare` | `openai` | router | `@cf/zai-org/glm-5.2` | Model-name regex | -| `mistral` | `openai` | cloud | `mistral-large-latest` | Model-name regex | -| `deepseek` | `openai` | cloud | `deepseek-flash` | Model-name regex | -| `xai` (Grok) | `openai` | cloud | `grok-4.3` | Model-name regex | -| `nvidia` (NIM) | `openai` | router | `meta/llama-3.1-8b-instruct` | Model-name regex | -| `groq` | `openai` | router | `llama-3.3-70b-versatile` | Model-name regex | -| `minimax` | `openai` | cloud | `minimax-m2.7` | Model-name regex | -| `kimi` | `openai` | cloud | `kimi-k2.5` | Model-name regex | -| `alibaba` (Qwen) | `openai` | cloud | `qwen-max` | Model-name regex | -| `together` | `openai` | router | `meta-llama/Llama-3.3-70B-Instruct-Turbo` | Model-name regex | -| `openrouter` | `openai` | router | `openrouter/free` | Model-name regex | -| `huggingface` | `openai` | router | `zai-org/GLM-5.2` | Model-name regex | -| `fireworks` | `openai` | router | `accounts/fireworks/models/llama-v3p3-70b-instruct` | Model-name regex | -| `z_ai` | `openai` | cloud | `glm-5.2` | Model-name regex | +| Provider ID | Type | Category | Default Model | Vision | +| -------------------- | -------------- | -------- | ------------------------------------------------------------------ | ------------------------------- | +| `webbrain_cloud` | `openai` | cloud | `webbrain-cloud 1.0` | Yes | +| `llamacpp` | `llamacpp` | local | (loaded model) | Auto metadata / override | +| `ollama` | `openai` | local | (loaded model) | Auto via `/api/show` / override | +| `lmstudio` | `openai` | local | (loaded model) | Auto metadata / override | +| `jan` | `openai` | local | (loaded model) | Yes (default on) | +| `vllm` | `openai` | local | (loaded model) | Yes (default on) | +| `sglang` | `openai` | local | (loaded model) | Yes (default on) | +| `localai` | `openai` | local | (loaded model) | Auto metadata / override | +| `gpt4all` | `openai` | local | (loaded model) | Yes (default on) | +| `local_openai_proxy` | `openai` | local | (required) | Off / manual toggle | +| `unsloth` | `openai` | local | (required) | Off / manual toggle | +| `webgpu` (Chromium) | `webgpu` | local | Compass Tiny v2.1 (only preset); experimental custom HF ONNX repos | No | +| `azure_openai` | `azure_openai` | cloud | (deployment) | Manual toggle | +| `aws_bedrock` | `aws_bedrock` | cloud | (model id) | No | +| `openai` | `openai` | cloud | `gpt-5.6-terra` | Model-name regex | +| `anthropic` | `anthropic` | cloud | `claude-sonnet-4-6` | Model-name regex | +| `gemini` | `openai` | cloud | `gemini-3.1-flash` | Model-name regex | +| `cloudflare` | `openai` | router | `@cf/zai-org/glm-5.2` | Model-name regex | +| `mistral` | `openai` | cloud | `mistral-large-latest` | Model-name regex | +| `deepseek` | `openai` | cloud | `deepseek-flash` | Model-name regex | +| `xai` (Grok) | `openai` | cloud | `grok-4.3` | Model-name regex | +| `nvidia` (NIM) | `openai` | router | `meta/llama-3.1-8b-instruct` | Model-name regex | +| `groq` | `openai` | router | `llama-3.3-70b-versatile` | Model-name regex | +| `minimax` | `openai` | cloud | `minimax-m2.7` | Model-name regex | +| `kimi` | `openai` | cloud | `kimi-k2.5` | Model-name regex | +| `alibaba` (Qwen) | `openai` | cloud | `qwen-max` | Model-name regex | +| `together` | `openai` | router | `meta-llama/Llama-3.3-70B-Instruct-Turbo` | Model-name regex | +| `openrouter` | `openai` | router | `openrouter/free` | Model-name regex | +| `huggingface` | `openai` | router | `zai-org/GLM-5.2` | Model-name regex | +| `fireworks` | `openai` | router | `accounts/fireworks/models/llama-v3p3-70b-instruct` | Model-name regex | +| `z_ai` | `openai` | cloud | `glm-5.2` | Model-name regex | ### Extended provider catalog @@ -124,23 +139,23 @@ their official API documentation. Together with the original cards, Settings contains **110 built-in providers on Chromium** and **109 on Firefox**; the difference is the Chromium-only in-browser WebGPU runtime. -| IDs | -|---| +| IDs | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `302ai`, `abacus`, `aihubmix`, `alibaba-coding-plan`, `alibaba-coding-plan-cn`, `azure-cognitive-services`, `bailing`, `baseten`, `berget`, `cerebras`, `chutes`, `clarifai`, `cloudferro-sherlock`, `cohere`, `cortecs`, `deepinfra`, `digitalocean`, `dinference`, `drun`, `evroc`, `fastrouter`, `friendli` | -| `google-vertex`, `google-vertex-anthropic`, `helicone`, `iflowcn`, `inception`, `inference`, `io-net`, `jiekou`, `kilo`, `kimi-for-coding`, `kuae-cloud-coding-plan`, `llama`, `lucidquery`, `meganova`, `minimax-cn-coding-plan`, `minimax-coding-plan`, `moark`, `modelscope`, `morph` | -| `nano-gpt`, `nearai`, `nebius`, `nova`, `novita-ai`, `ollama-cloud`, `opencode`, `opencode-go`, `orcarouter`, `ovhcloud`, `perplexity`, `perplexity-agent`, `poe`, `pollinations`, `privatemode-ai`, `qihang-ai`, `qiniu-ai`, `requesty`, `scaleway`, `siliconflow`, `siliconflow-cn`, `stackit` | -| `stepfun`, `submodel`, `synthetic`, `tencent-coding-plan`, `upstage`, `v0`, `venice`, `vercel`, `vivgrid`, `vultr`, `wandb`, `xiaomi`, `zai-coding-plan`, `zenmux`, `zhipuai`, `zhipuai-coding-plan` | +| `google-vertex`, `google-vertex-anthropic`, `helicone`, `iflowcn`, `inception`, `inference`, `io-net`, `jiekou`, `kilo`, `kimi-for-coding`, `kuae-cloud-coding-plan`, `llama`, `lucidquery`, `meganova`, `minimax-cn-coding-plan`, `minimax-coding-plan`, `moark`, `modelscope`, `morph` | +| `nano-gpt`, `nearai`, `nebius`, `nova`, `novita-ai`, `ollama-cloud`, `opencode`, `opencode-go`, `orcarouter`, `ovhcloud`, `perplexity`, `perplexity-agent`, `poe`, `pollinations`, `privatemode-ai`, `qihang-ai`, `qiniu-ai`, `requesty`, `scaleway`, `siliconflow`, `siliconflow-cn`, `stackit` | +| `stepfun`, `submodel`, `synthetic`, `tencent-coding-plan`, `upstage`, `v0`, `venice`, `vercel`, `vivgrid`, `vultr`, `wandb`, `xiaomi`, `zai-coding-plan`, `zenmux`, `zhipuai`, `zhipuai-coding-plan` | Most use the OpenAI-compatible Chat Completions contract and bearer API keys. The exceptions are: -| Provider | Authentication / protocol | -|---|---| -| Azure AI Foundry | Resource name plus `api-key`; model is the deployed model name | -| Google Vertex AI | Project, location, and a Google authorization key sent as `x-goog-api-key`; `global` uses `aiplatform.googleapis.com` | -| Google Vertex AI (Anthropic) | Vertex `rawPredict` / `streamRawPredict` with the same authorization-key fields; `us` and `eu` use their multi-region hosts | -| Perplexity Agent | OpenAI Responses-compatible `/v1/responses` | -| Cloudflare | Existing card supports Workers AI plus an optional AI Gateway ID; blank IDs use Cloudflare's `default` gateway for `@cf/` models | +| Provider | Authentication / protocol | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Azure AI Foundry | Resource name plus `api-key`; model is the deployed model name | +| Google Vertex AI | Project, location, and a Google authorization key sent as `x-goog-api-key`; `global` uses `aiplatform.googleapis.com` | +| Google Vertex AI (Anthropic) | Vertex `rawPredict` / `streamRawPredict` with the same authorization-key fields; `us` and `eu` use their multi-region hosts | +| Perplexity Agent | OpenAI Responses-compatible `/v1/responses` | +| Cloudflare | Existing card supports Workers AI plus an optional AI Gateway ID; blank IDs use Cloudflare's `default` gateway for `@cf/` models | Morph and standard Perplexity Sonar are text-only integrations in the agent and advertise `supportsTools: false`. New provider cards remain inactive until @@ -356,26 +371,26 @@ Provider tier and conversation mode are separate knobs: `provider.promptTier` resolves the active tier. Cloud providers are forced to Full. Local providers default to Mid. OpenRouter/router providers default to Full unless explicitly changed. Existing configs that still set the legacy `useCompactPrompt` boolean map to Compact. -| Tier | Intended model class | Normal tool surface | -|---|---|---| -| `compact` | very small/local models | Shortest prompt and a small normal Act tool set. No scheduling, iframe, download-resource, or advanced DOM/UI fallback tools. | -| `mid` | capable local models | Balanced prompt and common task tools: downloads, scheduling, iframe tools, form verification, and `download_resource_from_page`, while excluding Full-only advanced UI/DOM fallbacks. | -| `full` | frontier/cloud or large local models | Full normal Act prompt and advanced fallbacks such as hover, drag-drop, frames, and shadow DOM. | +| Tier | Intended model class | Normal tool surface | +| --------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `compact` | very small/local models | Shortest prompt and a small normal Act tool set. No scheduling, iframe, download-resource, or advanced DOM/UI fallback tools. | +| `mid` | capable local models | Balanced prompt and common task tools: downloads, scheduling, iframe tools, form verification, and `download_resource_from_page`, while excluding Full-only advanced UI/DOM fallbacks. | +| `full` | frontier/cloud or large local models | Full normal Act prompt and advanced fallbacks such as hover, drag-drop, frames, and shadow DOM. | Ask mode ignores provider tier and stays read-only. Act mode uses the selected tier's normal tools. Dev mode requires Mid or Full, uses the selected Act prompt, appends `SYSTEM_PROMPT_DEV_APPENDIX`, and adds Dev-only source/style tools plus Dev-extended shadow/frame inspection for Mid-tier debugging. Compact Dev is blocked before an LLM request is sent. ### Vision Detection -| Provider | Mechanism | -|---|---| -| OpenAI-compatible | Regex against model name (`gpt-4o`, `gpt-5`, `claude-3`, `claude-sonnet-4`, `gemini-2.0-flash`, etc.) | -| DeepSeek | The `deepseek-flash` family (including the retired `deepseek-v4-flash` aliases) is multimodal; `deepseek-v4-pro` and the V3-era ids are text-only | -| Anthropic | `claude-(3\|sonnet-4\|opus-4)` patterns | -| Ollama | `POST /api/show` `capabilities`, with legacy projector / `.vision.` metadata fallbacks; Auto / Force on / Off | -| llama.cpp | `GET /props` → `modalities.vision`, with Auto / Force on / Off | -| LM Studio | `GET /api/v1/models` → `capabilities.vision`; legacy `/api/v0/models` `type`, with overrides | -| LocalAI | `GET /v1/models/capabilities` → `input_modalities` / `capabilities`, with overrides | -| Jan / vLLM / SGLang | Explicit `supportsVision` config toggle (via OpenAI provider) | +| Provider | Mechanism | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| OpenAI-compatible | Regex against model name (`gpt-4o`, `gpt-5`, `claude-3`, `claude-sonnet-4`, `gemini-2.0-flash`, etc.) | +| DeepSeek | The `deepseek-flash` family (including the retired `deepseek-v4-flash` aliases) is multimodal; `deepseek-v4-pro` and the V3-era ids are text-only | +| Anthropic | `claude-(3\|sonnet-4\|opus-4)` patterns | +| Ollama | `POST /api/show` `capabilities`, with legacy projector / `.vision.` metadata fallbacks; Auto / Force on / Off | +| llama.cpp | `GET /props` → `modalities.vision`, with Auto / Force on / Off | +| LM Studio | `GET /api/v1/models` → `capabilities.vision`; legacy `/api/v0/models` `type`, with overrides | +| LocalAI | `GET /v1/models/capabilities` → `input_modalities` / `capabilities`, with overrides | +| Jan / vLLM / SGLang | Explicit `supportsVision` config toggle (via OpenAI provider) | Auto results are keyed by provider, exact selected model, and canonical base URL. Concurrent checks share one request, and a late response from an older @@ -386,12 +401,12 @@ dedicated vision provider continues to use the existing split-provider path. When the active provider is Anthropic, the agent converts OpenAI-format messages: -| OpenAI format | Anthropic format | -|---|---| -| `system` message | `system` field (top-level) | +| OpenAI format | Anthropic format | +| -------------------------- | --------------------------------------- | +| `system` message | `system` field (top-level) | | `assistant` + `tool_calls` | `assistant` + `tool_use` content blocks | -| `tool` role | `user` + `tool_result` content blocks | -| `image_url` (data URL) | `image` source block | +| `tool` role | `user` + `tool_result` content blocks | +| `image_url` (data URL) | `image` source block | ### DeepSeek @@ -402,16 +417,16 @@ output, image input). Every other DeepSeek id — including the retired `deepseek-v4-pro` — stays on a conservative profile (64K context, 8K output, text-only) rather than inheriting capacities it may not have. -| Aspect | Behaviour | -|---|---| -| Wire format | Chat Completions by default (`apiFormat: 'auto'`); the Responses API is an opt-in from the Advanced panel | -| Thinking | Top-level `thinking` object plus `reasoning_effort`; disabling thinking omits `reasoning_effort` entirely. The shared UI ladder maps `minimal`→`low` and `medium`/`xhigh`→`high` | -| Reasoning replay | `reasoning_content` is replayed across turns because DeepSeek returns 400 when a tool-carrying follow-up drops it | -| Streaming | `stream_options.include_usage` on every request; the parser ignores DeepSeek's SSE `: keep-alive` comments | -| Structured output | Chat Completions uses JSON Object mode; the Responses API uses `text.format` JSON Schema for the planner | -| Images | `deepseek-flash` accepts `image_url` data URLs and public URLs in `user` messages | -| Cost | Off-peak list price converted at 1 USD = 7.1 CNY (1 input, 0.02 cached input, 4 output; peak 2 / 0.04 / 8). Cache hits arrive as the top-level `prompt_cache_hit_tokens` counter and are priced at the cache-read rate | -| Anthropic endpoint | `https://api.deepseek.com/anthropic` works with the built-in `anthropic` card by overriding its base URL | +| Aspect | Behaviour | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Wire format | Chat Completions by default (`apiFormat: 'auto'`); the Responses API is an opt-in from the Advanced panel | +| Thinking | Top-level `thinking` object plus `reasoning_effort`; disabling thinking omits `reasoning_effort` entirely. The shared UI ladder maps `minimal`→`low` and `medium`/`xhigh`→`high` | +| Reasoning replay | `reasoning_content` is replayed across turns because DeepSeek returns 400 when a tool-carrying follow-up drops it | +| Streaming | `stream_options.include_usage` on every request; the parser ignores DeepSeek's SSE `: keep-alive` comments | +| Structured output | Chat Completions uses JSON Object mode; the Responses API uses `text.format` JSON Schema for the planner | +| Images | `deepseek-flash` accepts `image_url` data URLs and public URLs in `user` messages | +| Cost | Off-peak list price converted at 1 USD = 7.1 CNY (1 input, 0.02 cached input, 4 output; peak 2 / 0.04 / 8). Cache hits arrive as the top-level `prompt_cache_hit_tokens` counter and are priced at the cache-read rate | +| Anthropic endpoint | `https://api.deepseek.com/anthropic` works with the built-in `anthropic` card by overriding its base URL | The contract lives in `providers/deepseek-config.js` (pure helpers and constants) and `providers/deepseek.js` (`DeepSeekProvider`). The shared @@ -429,15 +444,15 @@ Manages provider lifecycle: ```js const pm = new ProviderManager(); -await pm.load(); // Load from chrome.storage.local -await pm.save(); // Persist to chrome.storage.local -pm.getActive(); // Get the active provider instance -await pm.setActive('openai'); // Switch active provider -await pm.updateProvider('openai', { model: 'gpt-5' }); // Update config -await pm.duplicateProvider('openai'); // Create openai__duplicate -await pm.removeDuplicateProvider('openai__duplicate'); // Remove it -pm.getAll(); // All provider configs (for Settings UI) -await pm.testProvider('openai'); // Test connection +await pm.load(); // Load from chrome.storage.local +await pm.save(); // Persist to chrome.storage.local +pm.getActive(); // Get the active provider instance +await pm.setActive("openai"); // Switch active provider +await pm.updateProvider("openai", { model: "gpt-5" }); // Update config +await pm.duplicateProvider("openai"); // Create openai__duplicate +await pm.removeDuplicateProvider("openai__duplicate"); // Remove it +pm.getAll(); // All provider configs (for Settings UI) +await pm.testProvider("openai"); // Test connection ``` Each non-WebBrain provider config includes a persisted `configured` flag. An @@ -491,19 +506,19 @@ Those rates are editable in the provider card so custom model pricing can be adj The user can configure a separate vision provider for screenshot description. The agent sub-calls this provider to get a text description of the viewport, then feeds only the description (not the raw image) to the main planning provider. This reduces token costs when the main provider is text-only: -| Aspect | Separate vision model + text planner | Single multimodal planner | -|---|---|---| -| Processing flow | The vision model describes the screenshot, then the text planner reasons over that description and chooses tools. | One model sees the screenshot, reasons about the task, and chooses tools in the same call. | -| Access to raw pixels | Only the vision model sees the image; the planner receives text. | The planner retains direct access to the image while deciding what to do. | -| Visual information loss | The description is a lossy handoff and may omit small text, spatial relationships, colors, icons, or state cues. | No intermediate description is required, so the model can revisit visual details during reasoning. | -| Planning and tool calls | The vision model is observation-only; the text planner owns all action and tool decisions. | The same model performs visual interpretation and tool planning. | -| Specialist-model advantage | Perception and planning can use models selected independently for their strongest capability. | One model must be strong at both multimodal perception and browser-tool use. | -| Visual grounding and coordinates | Text descriptions can weaken the relationship between an element and its exact visual position; accessibility-tree `ref_id` targets remain preferable. | Image and coordinate context stay together, although semantic `ref_id` targets are still safer than coordinate clicks. | -| Latency | Usually requires two sequential inference calls. | Usually requires one inference call. | -| Cost | Pays for the vision call plus the planner call, but can keep expensive image tokens away from the planner. | Pays for one multimodal call, whose image-token cost depends on the provider and image detail. | -| Prompt-injection boundary | The observation model receives no agent tools, creating a stronger separation between screenshot content and actions. | The model that sees screenshot content can also choose tools, so multimodal prompt-injection defenses carry more responsibility. | -| Failure characteristics | Adds a sidecar timeout or transcription-failure point; a text-only planner may have to continue without visual enrichment. | Removes the handoff failure, but the entire turn depends on one multimodal endpoint and its combined capabilities. | -| Best fit | Strong text/tool planner paired with a specialist vision model, especially when most actions use DOM or accessibility evidence. | A model that is already strong at both vision and tool use, especially for tasks requiring fine visual detail or tight visual reasoning. | +| Aspect | Separate vision model + text planner | Single multimodal planner | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| Processing flow | The vision model describes the screenshot, then the text planner reasons over that description and chooses tools. | One model sees the screenshot, reasons about the task, and chooses tools in the same call. | +| Access to raw pixels | Only the vision model sees the image; the planner receives text. | The planner retains direct access to the image while deciding what to do. | +| Visual information loss | The description is a lossy handoff and may omit small text, spatial relationships, colors, icons, or state cues. | No intermediate description is required, so the model can revisit visual details during reasoning. | +| Planning and tool calls | The vision model is observation-only; the text planner owns all action and tool decisions. | The same model performs visual interpretation and tool planning. | +| Specialist-model advantage | Perception and planning can use models selected independently for their strongest capability. | One model must be strong at both multimodal perception and browser-tool use. | +| Visual grounding and coordinates | Text descriptions can weaken the relationship between an element and its exact visual position; accessibility-tree `ref_id` targets remain preferable. | Image and coordinate context stay together, although semantic `ref_id` targets are still safer than coordinate clicks. | +| Latency | Usually requires two sequential inference calls. | Usually requires one inference call. | +| Cost | Pays for the vision call plus the planner call, but can keep expensive image tokens away from the planner. | Pays for one multimodal call, whose image-token cost depends on the provider and image detail. | +| Prompt-injection boundary | The observation model receives no agent tools, creating a stronger separation between screenshot content and actions. | The model that sees screenshot content can also choose tools, so multimodal prompt-injection defenses carry more responsibility. | +| Failure characteristics | Adds a sidecar timeout or transcription-failure point; a text-only planner may have to continue without visual enrichment. | Removes the handoff failure, but the entire turn depends on one multimodal endpoint and its combined capabilities. | +| Best fit | Strong text/tool planner paired with a specialist vision model, especially when most actions use DOM or accessibility evidence. | A model that is already strong at both vision and tool use, especially for tasks requiring fine visual detail or tight visual reasoning. | ```js const vision = await providerManager.getVisionProvider(); diff --git a/src/chrome/src/agent/agent.js b/src/chrome/src/agent/agent.js index 2cf091836..59f1b8faa 100644 --- a/src/chrome/src/agent/agent.js +++ b/src/chrome/src/agent/agent.js @@ -124,6 +124,7 @@ import { visionGenerationOptions, } from '../providers/provider-compatibility.js'; import { resolveMaxOutputTokens } from '../providers/context-windows.js'; +import { generateImage } from './fal-media.js'; import { extractFirstJsonObject } from './json-extract.js'; import { repairAssistantDisplayText, sanitizeText as sanitizePlannerText } from './text-sanitize.js'; import { emptyOutputFailureMessage, modelOutputDiagnostics } from './model-output-diagnostics.js'; @@ -35600,6 +35601,9 @@ If the user has already named or confirmed this exact recipient, do NOT ask agai if (name === 'fetch_url') { return await fetchUrl(args.url, args, { tabId, signal: executionContext?._contentActionAbortSignal }); } + if (name === 'generate_image') { + return await generateImage(args, { signal: executionContext?._contentActionAbortSignal }); + } if (name === 'read_page_source') { return await readPageSource(args.url, args, { tabId, signal: executionContext?._contentActionAbortSignal }); } diff --git a/src/chrome/src/agent/fal-media.js b/src/chrome/src/agent/fal-media.js new file mode 100644 index 000000000..1994f5812 --- /dev/null +++ b/src/chrome/src/agent/fal-media.js @@ -0,0 +1,270 @@ +// fal.ai generative media (assistive model). +// +// Configured in Settings -> Assistive Models -> "Generative media (fal.ai)" +// and stored in extension storage as `imageGenModel = { apiKey, model }`. +// fal.ai uses a queue API: submit a prompt, poll status_url, then fetch +// response_url. Auth uses `Authorization: Key `. + +export const IMAGE_GEN_MODEL_KEY = 'imageGenModel'; +export const FAL_QUEUE_BASE = 'https://queue.fal.run'; +export const FAL_AUTH_PROBE_URL = 'https://api.fal.ai/v1/workflows?limit=1'; +const FAL_STATUS_POLL_INTERVAL_MS = 2000; +const FAL_STATUS_TIMEOUT_MS = 120000; +const FAL_CANCEL_TIMEOUT_MS = 2000; + +/** Normalize a fal.ai model id (for example, "fal-ai/flux/schnell"). */ +export function normalizeFalModelId(model) { + const id = String(model || '').trim().replace(/^\/+|\/+$/g, ''); + if (!id) return ''; + if (!/^[A-Za-z0-9][A-Za-z0-9._\-/]*$/.test(id) || id.includes('..')) return ''; + return id; +} + +export function isImageGenConfigured(cfg) { + return !!(cfg && cfg.apiKey && cfg.model); +} + +export function falQueueSubmitUrl(model) { + return `${FAL_QUEUE_BASE}/${model}`; +} + +/** Extract a usable media URL from the response shapes used by fal models. */ +export function extractFalMediaUrl(payload) { + if (!payload || typeof payload !== 'object') return ''; + if (typeof payload.url === 'string' && /^https:\/\//.test(payload.url)) return payload.url; + for (const listKey of ['images', 'videos', 'audio']) { + const list = payload[listKey]; + if (Array.isArray(list) && typeof list[0]?.url === 'string' && /^https:\/\//.test(list[0].url)) { + return list[0].url; + } + } + for (const objKey of ['image', 'video', 'audio']) { + const obj = payload[objKey]; + if (obj && typeof obj.url === 'string' && /^https:\/\//.test(obj.url)) return obj.url; + } + return ''; +} + +async function falAuthHeaders(apiKey) { + return { 'Authorization': `Key ${apiKey}`, 'Content-Type': 'application/json' }; +} + +function trustedFalQueueUrl(value) { + if (typeof value !== 'string' || !value) return ''; + try { + const url = new URL(value); + return url.protocol === 'https:' && url.origin === FAL_QUEUE_BASE ? url.href : ''; + } catch { + return ''; + } +} + +function abortReason(signal) { + if (signal?.reason instanceof Error) return signal.reason; + const error = new Error('fal.ai generation was cancelled.'); + error.name = 'AbortError'; + return error; +} + +function throwIfAborted(signal) { + if (signal?.aborted) throw abortReason(signal); +} + +function abortableDelay(ms, signal) { + throwIfAborted(signal); + return new Promise((resolve, reject) => { + const onAbort = () => { + clearTimeout(timer); + reject(abortReason(signal)); + }; + const timer = setTimeout(() => { + signal?.removeEventListener?.('abort', onAbort); + resolve(); + }, ms); + signal?.addEventListener?.('abort', onAbort, { once: true }); + }); +} + +function createOperationSignal(externalSignal, timeoutMs) { + const controller = new AbortController(); + const timeoutError = new Error('fal.ai generation timed out.'); + timeoutError.name = 'TimeoutError'; + let timedOut = false; + const onExternalAbort = () => { + if (!controller.signal.aborted) controller.abort(abortReason(externalSignal)); + }; + if (externalSignal?.aborted) onExternalAbort(); + else externalSignal?.addEventListener?.('abort', onExternalAbort, { once: true }); + const timer = setTimeout(() => { + timedOut = true; + if (!controller.signal.aborted) controller.abort(timeoutError); + }, timeoutMs); + return { + signal: controller.signal, + timeoutError, + timedOut: () => timedOut, + dispose() { + clearTimeout(timer); + externalSignal?.removeEventListener?.('abort', onExternalAbort); + }, + }; +} + +async function cancelFalRequest(cancelUrl, headers, fetchImpl) { + if (!cancelUrl) return; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FAL_CANCEL_TIMEOUT_MS); + try { + await fetchImpl(cancelUrl, { + method: 'PUT', + headers, + signal: controller.signal, + keepalive: true, + }); + } catch { + // Cancellation is best-effort; preserve the original abort/timeout error. + } finally { + clearTimeout(timer); + } +} + +/** Run a queued generation: submit, poll, then fetch the result. */ +export async function runFalGeneration({ + prompt, + config, + fetchImpl = fetch, + timeoutMs = FAL_STATUS_TIMEOUT_MS, + signal = null, +}) { + const model = normalizeFalModelId(config?.model); + if (!model) throw new Error('Invalid fal.ai model id.'); + if (!config?.apiKey) throw new Error('fal.ai API key not configured.'); + const text = String(prompt || '').trim(); + if (!text) throw new Error('prompt is required.'); + + const operation = createOperationSignal(signal, timeoutMs); + const headers = await falAuthHeaders(config.apiKey); + let cancelUrl = ''; + let completed = false; + try { + throwIfAborted(operation.signal); + const submitRes = await fetchImpl(falQueueSubmitUrl(model), { + method: 'POST', + headers, + body: JSON.stringify({ prompt: text }), + signal: operation.signal, + }); + if (!submitRes.ok) { + let body = ''; + try { body = (await submitRes.text()).slice(0, 300); } catch { /* ignore */ } + throw new Error(`fal.ai submit failed (HTTP ${submitRes.status}): ${body || submitRes.statusText}`); + } + + let queued; + try { + queued = await submitRes.json(); + } catch (error) { + throw new Error(`fal.ai submit returned invalid JSON: ${error.message}`); + } + const statusUrl = trustedFalQueueUrl(queued?.status_url); + const responseUrl = trustedFalQueueUrl(queued?.response_url); + cancelUrl = trustedFalQueueUrl(queued?.cancel_url) + || (responseUrl ? `${responseUrl.replace(/\/$/, '')}/cancel` : ''); + if (!statusUrl || !responseUrl) { + throw new Error('fal.ai submit response missing trusted status_url/response_url.'); + } + + let status = 'IN_QUEUE'; + while (true) { + await abortableDelay(FAL_STATUS_POLL_INTERVAL_MS, operation.signal); + const statusRes = await fetchImpl(statusUrl, { headers, signal: operation.signal }); + if (!statusRes.ok) throw new Error(`fal.ai status check failed (HTTP ${statusRes.status}).`); + + let statusPayload; + try { + statusPayload = await statusRes.json(); + } catch (error) { + throw new Error(`fal.ai status returned invalid JSON: ${error.message}`); + } + status = String(statusPayload?.status || '').toUpperCase(); + if (status === 'COMPLETED') { + const resultRes = await fetchImpl(responseUrl, { headers, signal: operation.signal }); + if (!resultRes.ok) throw new Error(`fal.ai result fetch failed (HTTP ${resultRes.status}).`); + + let payload; + try { + payload = await resultRes.json(); + } catch (error) { + throw new Error(`fal.ai result returned invalid JSON: ${error.message}`); + } + const url = extractFalMediaUrl(payload); + if (!url) throw new Error('fal.ai result contained no media URL.'); + completed = true; + return { url, model, status }; + } + if (status === 'FAILED' || status === 'ERROR') { + const errorText = typeof statusPayload?.error === 'string' ? statusPayload.error : 'unknown error'; + throw new Error(`fal.ai generation failed: ${errorText}`); + } + } + } catch (error) { + if (cancelUrl && !completed) await cancelFalRequest(cancelUrl, headers, fetchImpl); + if (operation.timedOut()) throw operation.timeoutError; + if (signal?.aborted) throw abortReason(signal); + throw error; + } finally { + operation.dispose(); + } +} + +/** Agent tool entry point. Reads the assistive-model config from storage. */ +export async function generateImage(args, options = {}) { + const fetchImpl = typeof options === 'function' ? options : (options.fetchImpl || fetch); + const signal = typeof options === 'object' ? options.signal : null; + let cfg; + const api = (typeof browser !== 'undefined' && browser?.storage) ? browser + : (typeof chrome !== 'undefined' ? chrome : null); + try { + const stored = await api.storage.local.get([IMAGE_GEN_MODEL_KEY]); + cfg = stored?.[IMAGE_GEN_MODEL_KEY]; + } catch (error) { + return { success: false, error: 'Failed to read generative media config: ' + error.message }; + } + if (!isImageGenConfigured(cfg)) { + return { success: false, error: 'Generative media is not configured. Set up fal.ai in Settings -> Assistive Models.' }; + } + try { + const result = await runFalGeneration({ prompt: args?.prompt, config: cfg, fetchImpl, signal }); + return { success: true, url: result.url, model: result.model }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +/** Verify a key against a free, read-only endpoint that requires authentication. */ +export async function testImageGenProvider(fetchImpl = fetch) { + let cfg; + const api = (typeof browser !== 'undefined' && browser?.storage) ? browser + : (typeof chrome !== 'undefined' ? chrome : null); + try { + const stored = await api.storage.local.get([IMAGE_GEN_MODEL_KEY]); + cfg = stored?.[IMAGE_GEN_MODEL_KEY]; + } catch (error) { + return { ok: false, error: 'Failed to read generative media config: ' + error.message }; + } + if (!isImageGenConfigured(cfg)) { + return { ok: false, error: 'Generative media not configured (API Key and Model are required).' }; + } + const model = normalizeFalModelId(cfg.model); + if (!model) return { ok: false, error: 'Invalid fal.ai model id.' }; + try { + const res = await fetchImpl(FAL_AUTH_PROBE_URL, { headers: await falAuthHeaders(cfg.apiKey) }); + if (res.status === 401 || res.status === 403) { + return { ok: false, error: 'fal.ai rejected the API key (HTTP ' + res.status + ').' }; + } + if (res.ok) return { ok: true, model }; + return { ok: false, error: `Unexpected response from fal.ai (HTTP ${res.status}).` }; + } catch (error) { + return { ok: false, error: error.message }; + } +} diff --git a/src/chrome/src/agent/permission-gate.js b/src/chrome/src/agent/permission-gate.js index 452ded469..829e9b8e4 100644 --- a/src/chrome/src/agent/permission-gate.js +++ b/src/chrome/src/agent/permission-gate.js @@ -82,6 +82,8 @@ export const UNTRUSTED_CONTENT_TOOLS = new Set([ 'execute_webmcp_tool', 'fetch_url', 'research_url', + // fal.ai returns provider-authored URLs and error text. + 'generate_image', // ChatGPT's answer and cited links are third-party page content. 'delegate_research', 'read_pdf', @@ -419,6 +421,8 @@ const TOOL_CAPABILITY = { download_social_media: Capability.DOWNLOAD, schedule_resume: Capability.SCHEDULE, schedule_task: Capability.SCHEDULE, + // generate_image spends the user's fal.ai credits via a paid network call. + generate_image: Capability.NETWORK, }; /** @@ -564,6 +568,7 @@ export function hostForCapability(capability, args, currentUrlOrHost, toolName) return normalizeHost(args._otpMailboxUrl); } if (toolName === 'delegate_research') return 'chatgpt.com'; + if (toolName === 'generate_image') return 'queue.fal.run'; if (toolName === 'execute_webmcp_tool') { // A tool can belong to a cross-origin frame. Charge mutations to that // frame's resolved URL instead of borrowing the top-level page grant. diff --git a/src/chrome/src/agent/planner.js b/src/chrome/src/agent/planner.js index d4f4db262..875e430e2 100644 --- a/src/chrome/src/agent/planner.js +++ b/src/chrome/src/agent/planner.js @@ -376,6 +376,7 @@ ${PLANNER_RESPONSE_LANGUAGE_RULES} read: get_accessibility_tree, read_page, extract_data, fetch_url, research_url interact: click_ax, set_checked, type_ax, set_field, find_text, press_keys, scroll, navigate, gmail_count_results, carousel_navigate, promote_iframe wait: wait_for_element, wait_for_stable + media: generate_image (create an image/video/audio directly from a text prompt via the user's fal.ai generative-media model — use when the user asks to GENERATE media, not to browse an image site) memory: scratchpad_write, progress_update, progress_read schedule: schedule_task (future/recurring work the user explicitly asked for), schedule_resume (pause CURRENT run blocked on external event) user input: clarify (pause and ask one concise question when a required value remains missing after relevant inspection) @@ -464,6 +465,7 @@ ${PLANNER_RESPONSE_ONLY_RULES} - Canonical summary, steps, and risks must be English. localized fields must use the requested wbLocale. ${PLANNER_RESPONSE_LANGUAGE_RULES} - For execute, keep the compact plan to 1–4 steps. For plan_only, provide 2–8 useful steps. For respond and clarify, steps may be empty. +- When the user asks to generate an image/video/audio, plan one generate_image step (WebBrain's built-in fal.ai media tool). Do not plan steps to visit image-generation sites or to check whether the current page supports image generation. - clarify pauses execution to ask one concise question for a required value. done is terminal and must never be used to request information needed to continue. - press_keys supports only unmodified Escape, Tab, Enter, arrow keys, and ; (semicolon, for page shortcuts such as Gmail Expand all). Never plan modifier combinations or browser UI shortcuts; use find_text to select one page-text match instead of Ctrl/Cmd+F. Each call replaces the previous selection and cannot create simultaneous highlights or browser Find UI. - For Instagram /p// carousel enumeration, use strictly increasing carousel_navigate indexes unless the latest user request explicitly asks for reverse traversal, in which case use strictly decreasing indexes; never use arrow keys, coordinate clicks, Previous/Next, or go_back to traverse slides. diff --git a/src/chrome/src/agent/tools.js b/src/chrome/src/agent/tools.js index c176eab01..d324511af 100644 --- a/src/chrome/src/agent/tools.js +++ b/src/chrome/src/agent/tools.js @@ -1244,6 +1244,20 @@ export const AGENT_TOOLS = [ }, }, }, + { + type: 'function', + function: { + name: 'generate_image', + description: 'Generate media (usually an image) from a text prompt using the user\'s configured fal.ai assistive model (Settings → Assistive Models → Generative media). Runs on fal.ai\'s queue API and may take up to a minute. Returns the hosted media URL on success. Not available in Ask mode.', + parameters: { + type: 'object', + properties: { + prompt: { type: 'string', description: 'Text prompt describing the media to generate.' }, + }, + required: ['prompt'], + }, + }, + }, ]; /** @@ -1952,6 +1966,7 @@ ${BROWSER_TAB_LIMITATION} - scratchpad_write: Pin a note in context that survives summarization (use on long tasks to remember download IDs, file paths, plans) - progress_update / progress_read: Structured app-owned ledger for the active repeated item/action task. Use it for per-user/per-item status and collected fields; close pending/acted rows before done. - download_public_media (if enabled by a skill) / download_social_media: One-shot image/video download from public social sites. Prefer the enabled skill tool for public media URLs; otherwise use download_social_media. Single call — no need to inspect the DOM yourself. +- generate_image: Create media (usually an image, sometimes video/audio) directly from a text prompt through the user's configured fal.ai generative-media model. When the user asks to GENERATE media ("generate an image of a red apple", "make a logo", "create a video clip"), call this tool — do NOT navigate to third-party image sites (Midjourney, DALL·E, Bing Images, etc.). Requires Settings → Assistive Models → Generative media. Not available in Ask mode. - Recording is user-driven only. If the user asks to record, do NOT call tools; tell them to type \`/record\` for current-tab recording or \`/record --full-screen\` for screen/window recording; add \`--transcribe\` to either form if they want a Whisper transcript after stop. If they ask to stop a recording, tell them to press Escape twice in WebBrain/browser surfaces or use Chrome's Stop sharing control. - hover: CDP-trusted hover over a ref_id. Use ONLY for menus/tooltips that REVEAL on hover (GitHub three-dot menus, Linear card actions, nav menus with reveal-on-hover children). Re-read the tree after to find the newly-visible items. Do NOT call hover before every click — most things are clickable directly. - drag_drop: Drag one ref_id onto another via CDP-trusted pointer events. Use for Trello/Linear/Notion-style card reordering, file-tree node moves, image-crop handles, slider thumbs. Pass \`steps: 15–20\` if the first attempt doesn't trigger the drop indicator on momentum-tracking dnd. Verify by re-reading the tree. diff --git a/src/chrome/src/background.js b/src/chrome/src/background.js index 9237c4d4d..328320ead 100644 --- a/src/chrome/src/background.js +++ b/src/chrome/src/background.js @@ -41,6 +41,7 @@ import { } from './providers/oauth-claude.js'; import { getBalance as capsolverGetBalance } from './agent/captcha-solver.js'; import { isCapsolverEnabled } from './agent/capsolver-config.js'; +import { testImageGenProvider } from './agent/fal-media.js'; import { createSystemOneJudge } from './agent/systemone-judge.js'; import { cloudSafeScheduledJob, createCloudRunController } from './cloud-runs.js'; import { ensureOffscreen } from './offscreen/ensure.js'; @@ -181,7 +182,7 @@ async function startExplicitVisionModelDownload() { total: 0, error: String(result?.error || 'The local vision model download could not be started.').slice(0, 500), updatedAt: Date.now(), - }).catch(() => {}); + }).catch(() => { }); return result; } @@ -227,7 +228,7 @@ agent.setConversationScopeChangeListener((tabId, state) => { tabId, type: 'conversation_scope', data: state, - }).catch(() => {}); + }).catch(() => { }); }); const userMemoryStore = createUserMemoryStore(chrome.storage.local); const savedWorkflowStore = createSavedWorkflowStore(chrome.storage.local); @@ -273,7 +274,7 @@ const scheduler = new ScheduledJobManager({ tabId, type, data, - }).catch(() => {}); + }).catch(() => { }); maybeFlashScheduledTerminalEvent(tabId, type, data); }, showIndicator: (tabId) => sendIndicatorMessage(tabId, 'WB_SHOW_AGENT_INDICATORS'), @@ -305,7 +306,7 @@ const cloudRunController = createCloudRunController({ }); alwaysAllowApiMutationsReady .then(() => cloudRunController.syncBridge()) - .catch(() => {}); + .catch(() => { }); const MAX_AGENT_STEPS_DEFAULT = 130; const MAX_AGENT_STEPS_UNLIMITED_SENTINEL = 200; @@ -422,7 +423,7 @@ const selectionShortcutLocaleReady = chrome.storage.local.get({ wbLocale: '' }) .then((stored) => { selectionShortcutLocale = resolveStoredSelectionShortcutLocale(stored?.wbLocale); }) - .catch(() => {}); + .catch(() => { }); function getContextMenuPromptStore() { return chrome.storage?.session || chrome.storage?.local || null; @@ -458,7 +459,7 @@ async function createContextMenus() { console.warn('[WebBrain] Failed to create context menu:', err.message || err); } if (item.id === CONTEXT_MENU_OPEN_PDF_VIEWER_ID) { - syncPdfContextMenuForActiveTab().catch(() => {}); + syncPdfContextMenuForActiveTab().catch(() => { }); } }); }; @@ -539,7 +540,7 @@ async function loadClarifyTimeout() { updates.clarifyTimeoutSec = CLARIFY_TIMEOUT_OFF_SLIDER; stored.clarifyTimeoutSec = CLARIFY_TIMEOUT_OFF_SLIDER; } - await chrome.storage.local.set(updates).catch(() => {}); + await chrome.storage.local.set(updates).catch(() => { }); } agent.clarifyTimeoutSec = normalizeClarifyTimeoutSec( stored.clarifyTimeoutSec != null ? stored.clarifyTimeoutSec : 60, @@ -564,7 +565,7 @@ async function loadResearchEscalation() { agent.researchEscalationEnabled = stored.researchEscalationEnabled === true; agent.researchEscalationEngine = String(stored.researchEscalationEngine || 'chatgpt'); } -const researchEscalationReady = loadResearchEscalation().catch(() => {}); +const researchEscalationReady = loadResearchEscalation().catch(() => { }); // Local screenshot redaction (issue #312): when on, screenshots are pixelated // over DOM-detected PII (form fields + email/phone text) BEFORE leaving the @@ -584,13 +585,13 @@ async function loadImageBudget() { } // Retained so handleMessage can await hydration on a cold SW start — the first // chat must not race ahead of the persisted image-budget settings (issue #311). -const imageBudgetReady = loadImageBudget().catch(() => {}); +const imageBudgetReady = loadImageBudget().catch(() => { }); async function loadStrictSecretMode() { const stored = await chrome.storage.local.get('strictSecretMode').catch(() => ({})); agent.strictSecretMode = stored?.strictSecretMode === true; } -const strictSecretModeReady = loadStrictSecretMode().catch(() => {}); +const strictSecretModeReady = loadStrictSecretMode().catch(() => { }); async function loadWebMCPEnabled() { const stored = await chrome.storage.local.get('webMcpEnabled'); @@ -661,7 +662,7 @@ function recordClarificationMemoryCandidate(tabId, question, answer) { if (!normalizedAnswer) return; const normalizedQuestion = normalizeUserMemoryText(question, 500); if (looksLikeSensitiveMemoryText(normalizedAnswer) - || (normalizedQuestion && looksLikeSensitiveMemoryText(normalizedQuestion))) { + || (normalizedQuestion && looksLikeSensitiveMemoryText(normalizedQuestion))) { return; } const context = getUserMemoryTurnContext(tabId); @@ -748,7 +749,7 @@ async function isUserMemoryFormCaptureEnabled() { async function withUserMemoryExtractionQueueLock(task) { const run = userMemoryExtractionQueueLock.then(task, task); - userMemoryExtractionQueueLock = run.catch(() => {}); + userMemoryExtractionQueueLock = run.catch(() => { }); return run; } @@ -814,13 +815,13 @@ async function markUserMemoryExtractionJobFailed(jobId) { async function withUserMemoryStoreLock(task) { const run = userMemoryStoreLock.then(task, task); - userMemoryStoreLock = run.catch(() => {}); + userMemoryStoreLock = run.catch(() => { }); return run; } async function withSavedWorkflowStoreLock(task) { const run = savedWorkflowStoreLock.then(task, task); - savedWorkflowStoreLock = run.catch(() => {}); + savedWorkflowStoreLock = run.catch(() => { }); return run; } @@ -869,7 +870,7 @@ function notifyUserMemoryCreated() { chrome.runtime.sendMessage({ target: 'sidepanel', action: 'user_memory_created', - }).catch(() => {}); + }).catch(() => { }); } function scheduleUserMemoryExtractionDrain(delayMs = USER_MEMORY_EXTRACTION_DELAY_MS) { @@ -1175,8 +1176,8 @@ chrome.runtime.onInstalled.addListener(async (details) => { await providerManager.load(); await loadMaxSteps(); await loadClarifyTimeout(); - await syncAgentUserMemoryFromStorage().catch(() => {}); - await cloudRunController.syncBridge().catch(() => {}); + await syncAgentUserMemoryFromStorage().catch(() => { }); + await cloudRunController.syncBridge().catch(() => { }); // Chrome registers the manifest handler with enabled=true after install. // Reconcile now and once more after registration settles so the native // default cannot overwrite the extension's opt-in default. The in-handler @@ -1194,8 +1195,8 @@ chrome.runtime.onStartup?.addListener(async () => { await providerManager.load(); await loadMaxSteps(); await loadClarifyTimeout(); - await syncAgentUserMemoryFromStorage().catch(() => {}); - await cloudRunController.syncBridge().catch(() => {}); + await syncAgentUserMemoryFromStorage().catch(() => { }); + await cloudRunController.syncBridge().catch(() => { }); scheduleUserMemoryExtractionDrain(5000); }); @@ -1208,12 +1209,12 @@ chrome.storage.onChanged.addListener((changes, areaName) => { } if (changes.wbLocale) { selectionShortcutLocale = normalizeSelectionShortcutLocale(changes.wbLocale.newValue); - createContextMenus().catch(() => {}); + createContextMenus().catch(() => { }); } - if (PROFILE_SYNC_DATA_KEYS.some((key) => changes[key])) profileSync.noteChanges(changes).catch(() => {}); - if (changes.providers || changes.activeProvider || changes.helpImproveWebBrain) providerManager.load().catch(() => {}); + if (PROFILE_SYNC_DATA_KEYS.some((key) => changes[key])) profileSync.noteChanges(changes).catch(() => { }); + if (changes.providers || changes.activeProvider || changes.helpImproveWebBrain) providerManager.load().catch(() => { }); if (changes.webbrainCloudBridgeEnabled || changes.webbrainCloudBridgeUrl) { - cloudRunController.syncBridge().catch(() => {}); + cloudRunController.syncBridge().catch(() => { }); } if (changes.maxAgentSteps) { agent.maxSteps = normalizeMaxAgentSteps(changes.maxAgentSteps.newValue); @@ -1418,7 +1419,7 @@ async function loadPanelTabs() { } catch (e) { /* session storage not available */ } } function savePanelTabs() { - chrome.storage.session?.set({ [PANEL_TABS_KEY]: Array.from(panelTabs) }).catch(() => {}); + chrome.storage.session?.set({ [PANEL_TABS_KEY]: Array.from(panelTabs) }).catch(() => { }); } loadPanelTabs(); @@ -1451,11 +1452,11 @@ async function loadWebBrainGroups() { function saveWebBrainGroups() { chrome.storage.session?.set({ [WB_GROUPS_KEY]: Array.from(webBrainGroupByWindow.entries()), - }).catch(() => {}); + }).catch(() => { }); } loadWebBrainGroups(); -chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }).catch(() => {}); +chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }).catch(() => { }); // Panel visibility model — opt-in per tab (Cmd+T no longer leaks the panel). // @@ -1561,8 +1562,8 @@ chrome.runtime.onMessage.addListener((msg, sender) => { if (tab?.url !== installGuideUrl) return; panelTabs.add(tab.id); savePanelTabs(); - ensureWebBrainGroup(tab).catch(() => {}); - }).catch(() => {}); + ensureWebBrainGroup(tab).catch(() => { }); + }).catch(() => { }); }); // Tracks the pending 250 ms retry timer per tab so it can be cancelled if the @@ -1578,10 +1579,10 @@ function notifySidePanelOfContextMenuPrompt(payload) { prompt: payload, }; clearTimeout(pendingContextMenuNotifications.get(tabId)); - chrome.runtime.sendMessage(msg).catch(() => {}); + chrome.runtime.sendMessage(msg).catch(() => { }); const timerId = setTimeout(() => { pendingContextMenuNotifications.delete(tabId); - chrome.runtime.sendMessage(msg).catch(() => {}); + chrome.runtime.sendMessage(msg).catch(() => { }); }, 250); pendingContextMenuNotifications.set(tabId, timerId); } @@ -1596,7 +1597,7 @@ function openSidePanelForContextMenu(tab) { enabled: true, }); chrome.sidePanel.open({ tabId: tab.id }); - ensureWebBrainGroup(tab).catch(() => {}); + ensureWebBrainGroup(tab).catch(() => { }); } async function handleContextMenuAsk(info, tab) { @@ -1644,7 +1645,7 @@ async function handleContextMenuAsk(info, tab) { openSidePanelForContextMenu(tab); try { await contextMenuStorage.save(tab.id, payload); - } catch {} + } catch { } notifySidePanelOfContextMenuPrompt(payload); } @@ -1707,7 +1708,7 @@ function queueSelectionShortcutPrompt(msg, tab, sendResponse) { (async () => { try { await contextMenuStorage.save(tab.id, payload); - } catch {} + } catch { } notifySidePanelOfContextMenuPrompt(payload); return { ok: true, queued: true, requiresManualOpen: false }; })().then(sendResponse).catch((error) => { @@ -1741,7 +1742,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // now (stub is enough for setOptions/open), then verify the live tab. try { openSidePanelForContextMenu({ id: tabId }); - } catch {} + } catch { } chrome.tabs.get(tabId) .then(tab => queueSelectionShortcutPrompt(msg, tab, sendResponse)) .catch(error => sendResponse({ @@ -1801,8 +1802,8 @@ function startIndicatorHeartbeat(tabId) { sendIndicatorMessage(tabId, 'WB_SHOW_AGENT_INDICATORS'); } }) - .catch(() => {}); - } catch {} + .catch(() => { }); + } catch { } }, INDICATOR_HEARTBEAT_INTERVAL_MS); indicatorHeartbeatTimers.set(tabId, timer); } @@ -1838,7 +1839,7 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { // discard the entry it just recorded. trackPdfResponse() already replaces // the entry on the next main-frame response, and onRemoved clears it. if (changeInfo?.url || changeInfo?.status) { - syncPdfContextMenuForActiveTab().catch(() => {}); + syncPdfContextMenuForActiveTab().catch(() => { }); } if (changeInfo?.status === 'complete') { reassertIndicatorIfActive(tabId); @@ -1848,7 +1849,7 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { chrome.tabs.onRemoved.addListener((tabId) => { activeIndicatorTabs.delete(tabId); stopIndicatorHeartbeat(tabId); - withTeacherSessionStoreLock(() => teacherSessionStore.clear(tabId)).catch(() => {}); + withTeacherSessionStoreLock(() => teacherSessionStore.clear(tabId)).catch(() => { }); }); const RUN_UI_PREFIX = 'runUi:'; @@ -1888,7 +1889,7 @@ function persistRunUiSnapshot(tabId, snapshot) { runUiPersistenceQueues.set(tabId, write); write.finally(() => { if (runUiPersistenceQueues.get(tabId) === write) runUiPersistenceQueues.delete(tabId); - }).catch(() => {}); + }).catch(() => { }); return write; } @@ -2007,7 +2008,7 @@ async function getRunUiSnapshot(tabId) { if (snapshot && typeof snapshot === 'object') { return runUiJournal.restore(tabId, snapshot); } - } catch {} + } catch { } return null; } @@ -2016,11 +2017,11 @@ function clearRunUiSnapshot(tabId) { runUiJournal.clear(tabId); runUiPersistenceFailures.delete(tabId); const previous = runUiPersistenceQueues.get(tabId) || Promise.resolve(); - const removal = previous.catch(() => {}).then(() => chrome.storage.session?.remove(RUN_UI_PREFIX + tabId)); + const removal = previous.catch(() => { }).then(() => chrome.storage.session?.remove(RUN_UI_PREFIX + tabId)); runUiPersistenceQueues.set(tabId, removal); removal.finally(() => { if (runUiPersistenceQueues.get(tabId) === removal) runUiPersistenceQueues.delete(tabId); - }).catch(() => {}); + }).catch(() => { }); } function sendAgentUpdate(tabId, requestId, type, data) { @@ -2035,7 +2036,7 @@ function sendAgentUpdate(tabId, requestId, type, data) { seq: event?.seq || null, type, data: event?.data ?? data, - }).catch(() => {}); + }).catch(() => { }); } function assertNoActiveTabRun(tabId) { @@ -2154,8 +2155,8 @@ function acquireRunKeepalive() { let released = false; const touch = () => { try { - chrome.runtime.getPlatformInfo().catch(() => {}); - } catch {} + chrome.runtime.getPlatformInfo().catch(() => { }); + } catch { } }; touch(); const timer = setInterval(touch, RUN_KEEPALIVE_INTERVAL_MS); @@ -2241,7 +2242,7 @@ async function sendAgentRunComplete(tabId, snapshot = null) { flashTabAttention({ tabId, success: liveStatus === 'completed' && snapshot.runSucceeded === true, - }).catch(() => {}); + }).catch(() => { }); } const submittedTurnDurable = snapshot.kind === 'continue' || await agent.hasDurableSubmittedTurn( @@ -2277,7 +2278,7 @@ async function sendAgentRunComplete(tabId, snapshot = null) { submittedTurnDurable, attachmentDeliveryState, }, - }).catch(() => {}); + }).catch(() => { }); } // Stop button on the page → abort the agent run for that tab. Mirrors @@ -2319,7 +2320,7 @@ chrome.action.onClicked.addListener((tab) => { // before the user can switch tabs. Async — we already lost the user- // gesture window for sidePanel.open, but ensureWebBrainGroup doesn't // need it. - ensureWebBrainGroup(tab).catch(() => {}); + ensureWebBrainGroup(tab).catch(() => { }); }); // (Was: chrome.tabs.onActivated + chrome.tabs.onUpdated listeners that @@ -2357,11 +2358,11 @@ chrome.tabs.onRemoved.addListener((tabId) => { clearTimeout(pendingContextMenuNotifications.get(tabId)); pendingContextMenuNotifications.delete(tabId); contextMenuStorage.cleanup(tabId); - tabChatHandoff.clear(tabId).catch(() => {}); - clearStagedScreenshots(chrome.storage.local, tabId).catch(() => {}); + tabChatHandoff.clear(tabId).catch(() => { }); + clearStagedScreenshots(chrome.storage.local, tabId).catch(() => { }); savePanelTabs(); - scheduler.cancelForTab(tabId).catch(() => {}); - agent.clearDevCssPatchesForTab(tabId).catch(() => {}); + scheduler.cancelForTab(tabId).catch(() => { }); + agent.clearDevCssPatchesForTab(tabId).catch(() => { }); try { agent._cleanupTab(tabId); } catch { /* ignore */ } }); @@ -2376,7 +2377,7 @@ function invalidateContextMenuForTab(tabId) { target: 'sidepanel', action: 'context_menu_tab_navigated', tabId, - }).catch(() => {}); + }).catch(() => { }); } // SPA navigation tracking. Many sites change route via History API without @@ -2395,7 +2396,7 @@ function recordNav(tabId, type, url, { resetTypeIdentity = true } = {}) { } function recordTeacherNavigation(tabId, url, options) { - teacherRunInterlock.navigation(tabId, url, options).catch(() => {}); + teacherRunInterlock.navigation(tabId, url, options).catch(() => { }); } const TEACHER_EXPLICIT_NAVIGATION_TYPES = new Set([ @@ -2404,26 +2405,26 @@ const TEACHER_EXPLICIT_NAVIGATION_TYPES = new Set([ chrome.webNavigation?.onHistoryStateUpdated?.addListener((details) => { if (details.frameId !== 0) return; - agent.observeCloudflareManagedChallengeNavigation(details).catch(() => {}); + agent.observeCloudflareManagedChallengeNavigation(details).catch(() => { }); recordNav(details.tabId, 'history', details.url); recordTeacherNavigation(details.tabId, details.url); invalidateContextMenuForTab(details.tabId); }); chrome.webNavigation?.onReferenceFragmentUpdated?.addListener((details) => { if (details.frameId !== 0) return; - agent.observeCloudflareManagedChallengeNavigation(details).catch(() => {}); + agent.observeCloudflareManagedChallengeNavigation(details).catch(() => { }); recordNav(details.tabId, 'fragment', details.url); invalidateContextMenuForTab(details.tabId); }); chrome.webNavigation?.onCommitted?.addListener((details) => { if (details.frameId !== 0) return; - agent.observeCloudflareManagedChallengeNavigation(details).catch(() => {}); + agent.observeCloudflareManagedChallengeNavigation(details).catch(() => { }); recordNav(details.tabId, 'committed', details.url); recordTeacherNavigation(details.tabId, details.url, { force: TEACHER_EXPLICIT_NAVIGATION_TYPES.has(details.transitionType), }); invalidateContextMenuForTab(details.tabId); - agent.clearDevCssPatchesForTab(details.tabId).catch(() => {}); + agent.clearDevCssPatchesForTab(details.tabId).catch(() => { }); }); chrome.webNavigation?.onCompleted?.addListener((details) => { if (details.frameId === 0) { @@ -2437,11 +2438,11 @@ chrome.webNavigation?.onCompleted?.addListener((details) => { // detection and embedded widgets may use the same managed endpoint. const observeCloudflareManagedChallengeResponse = details => { trackPdfResponse(details); - syncPdfContextMenuForActiveTab().catch(() => {}); - agent.observeCloudflareManagedChallengeResponse(details).catch(() => {}); + syncPdfContextMenuForActiveTab().catch(() => { }); + agent.observeCloudflareManagedChallengeResponse(details).catch(() => { }); }; const observeCloudflareChallengePlatformRequest = details => { - agent.observeCloudflareChallengePlatformRequest(details).catch(() => {}); + agent.observeCloudflareChallengePlatformRequest(details).catch(() => { }); }; chrome.webRequest?.onHeadersReceived?.addListener?.( observeCloudflareManagedChallengeResponse, @@ -2503,7 +2504,7 @@ function extractApiReplayBody(requestBody) { const text = params.toString(); return text.length <= API_REPLAY_BODY_LIMIT ? text : null; } - } catch (_) {} + } catch (_) { } return null; } @@ -2670,7 +2671,7 @@ async function showCompletionNotification(tabId, success) { if (!notificationId) return resolve(); setTimeout(() => { completionNotificationFocusHandlers.delete(notificationId); - chrome.notifications.clear(notificationId, () => {}); + chrome.notifications.clear(notificationId, () => { }); }, COMPLETION_NOTIFICATION_VISIBLE_MS); if (Number.isInteger(tabId)) { completionNotificationFocusHandlers.set(notificationId, async () => { @@ -2689,11 +2690,11 @@ async function showCompletionNotification(tabId, success) { chrome.tabs.onActivated.addListener(({ tabId } = {}) => { flashedBadgeTabs.delete(tabId); - syncPdfContextMenuForActiveTab().catch(() => {}); + syncPdfContextMenuForActiveTab().catch(() => { }); // Clear unconditionally: the Set only lives in service-worker memory, but // a tab-scoped badge survives MV3 worker suspension/restarts. Resetting // the per-tab override is idempotent and restores any global badge. - chrome.action.setBadgeText({ tabId, text: '' }).catch(() => {}); + chrome.action.setBadgeText({ tabId, text: '' }).catch(() => { }); }); // Focusing a window does not fire tabs.onActivated for its already-active @@ -2883,7 +2884,7 @@ async function handleMessage(msg, sender) { chrome.runtime.sendMessage({ type: 'apocalypse-mode-state', enabled: snapshot.enabled === true, - }).catch(() => {}); + }).catch(() => { }); if (msg.enabled === true) { const textModel = await providerManager.enableAndStartWebgpuTextDownload(); return { ...snapshot, textModel }; @@ -3175,9 +3176,9 @@ async function handleMessage(msg, sender) { const compiled = draft?.conversationId === conversationId ? finalizeSavedWorkflowDraft(draft, { name: msg.name }) : await compileLatestSuccessfulWorkflow(workflowTrace, { - conversationId, - name: msg.name, - }); + conversationId, + name: msg.name, + }); if (!compiled.workflow) return { ok: false, ...compiled }; const saved = await withSavedWorkflowStoreLock(() => savedWorkflowStore.put(compiled.workflow)); return { ok: saved.changed, workflow: saved.workflow, warnings: compiled.warnings, reason: saved.reason || '' }; @@ -3605,7 +3606,7 @@ async function handleMessage(msg, sender) { handoffOwnerId: tabChatClearResult.handoffOwnerId, handoffGeneration: tabChatClearResult.handoffGeneration, clearedContextMenuPromptId, - }).catch(() => {}); + }).catch(() => { }); } return { ok: true, clearedContextMenuPromptId }; } @@ -3834,7 +3835,7 @@ async function handleMessage(msg, sender) { tabId, handoffOwnerId: result.handoffOwnerId, handoffGeneration: result.handoffGeneration, - }).catch(() => {}); + }).catch(() => { }); } return result; } @@ -3848,7 +3849,7 @@ async function handleMessage(msg, sender) { const tabId = msg.tabId || sender.tab?.id || null; let tab = null; if (tabId != null) { - try { tab = await chrome.tabs.get(tabId); } catch {} + try { tab = await chrome.tabs.get(tabId); } catch { } } return await scheduler.createTaskJob({ tabId, @@ -3864,7 +3865,7 @@ async function handleMessage(msg, sender) { const tabId = msg.tabId || sender.tab?.id || null; let tab = null; if (tabId != null) { - try { tab = await chrome.tabs.get(tabId); } catch {} + try { tab = await chrome.tabs.get(tabId); } catch { } } return await scheduler.createWatchJob({ args: msg.watch || msg.args || {}, @@ -4077,6 +4078,10 @@ async function handleMessage(msg, sender) { return await providerManager.testTranscriptionProvider(); } + case 'test_image_gen_provider': { + return await testImageGenProvider(); + } + case 'test_system_one': { await strictSecretModeReady; try { diff --git a/src/chrome/src/config-transfer.js b/src/chrome/src/config-transfer.js index 7ea6e1bb2..9b14cb557 100644 --- a/src/chrome/src/config-transfer.js +++ b/src/chrome/src/config-transfer.js @@ -66,6 +66,7 @@ export const DEFAULT_CONFIG_SETTINGS = Object.freeze({ activeProvider: 'webbrain_cloud', visionModel: null, transcriptionModel: null, + imageGenModel: null, profileEnabled: false, profileText: '', [USER_MEMORY_STORAGE_KEY]: { version: 1, records: [] }, @@ -160,7 +161,7 @@ const ARRAY_KEYS = new Set([ CUSTOM_SKILLS_STORAGE_KEY, DEFAULT_SKILLS_REMOVED_STORAGE_KEY, ]); -const NULLABLE_OBJECT_KEYS = new Set(['visionModel', 'transcriptionModel']); +const NULLABLE_OBJECT_KEYS = new Set(['visionModel', 'transcriptionModel', 'imageGenModel']); function isPlainObject(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; diff --git a/src/chrome/src/profile-sync.js b/src/chrome/src/profile-sync.js index 70b681173..2d6931cd0 100644 --- a/src/chrome/src/profile-sync.js +++ b/src/chrome/src/profile-sync.js @@ -4,7 +4,7 @@ export const PROFILE_SYNC_KEYS = { enabled: 'profileSyncEnabled', token: 'profileSyncToken', deviceGuid: 'webbrainDeviceGuid', metadata: 'profileSyncMetadataV1', recovery: 'profileSyncRecoveryV1', everEnabled: 'profileSyncEverEnabled', }; -export const PROFILE_SYNC_DATA_KEYS = [USER_MEMORY_STORAGE_KEY, 'providers', 'activeProvider', 'visionModel', 'transcriptionModel', 'profileEnabled', 'profileText']; +export const PROFILE_SYNC_DATA_KEYS = [USER_MEMORY_STORAGE_KEY, 'providers', 'activeProvider', 'visionModel', 'transcriptionModel', 'imageGenModel', 'profileEnabled', 'profileText']; const API = 'https://api.webbrain.one/v1/sync'; const ITERATIONS = 600000; const NON_PORTABLE_PROVIDER_ID = 'webgpu'; @@ -144,6 +144,7 @@ function mergeLegacyProviderState(local, remote, conflicts) { auxiliaryProviders: { visionModel: local.auxiliaryProviders?.visionModel ?? remote.auxiliaryProviders?.visionModel ?? null, transcriptionModel: local.auxiliaryProviders?.transcriptionModel ?? remote.auxiliaryProviders?.transcriptionModel ?? null, + imageGenModel: local.auxiliaryProviders?.imageGenModel ?? remote.auxiliaryProviders?.imageGenModel ?? null, }, }; } @@ -162,7 +163,7 @@ function mergeProviderState(local, remote, lm, rm, conflicts) { if (providers[id] === undefined) delete providers[id]; } const auxiliaryProviders = {}; - for (const id of ['visionModel', 'transcriptionModel']) { + for (const id of ['visionModel', 'transcriptionModel', 'imageGenModel']) { auxiliaryProviders[id] = newer(local.auxiliaryProviders?.[id], remote.auxiliaryProviders?.[id], itemTimestamp(lm, 'auxiliaryItemsAt', id), itemTimestamp(rm, 'auxiliaryItemsAt', id), conflicts, `providers.${id}`) ?? null; } const localActiveAt = lm.activeProviderAt ?? (lm.providerItemsAt ? 0 : lm.providersAt || 0); @@ -200,7 +201,7 @@ export function mergeProfileVaults(local, remote) { out.memory = normalizeUserMemoryStore({ ...(local.memory || {}), records: [...byId.values()] }); out.tombstones = tombstones; const providerIds = new Set([...Object.keys(local.providers || {}), ...Object.keys(remote.providers || {}), ...Object.keys(lm.providerItemsAt || {}), ...Object.keys(rm.providerItemsAt || {})]); - const auxiliaryIds = new Set(['visionModel', 'transcriptionModel', ...Object.keys(lm.auxiliaryItemsAt || {}), ...Object.keys(rm.auxiliaryItemsAt || {})]); + const auxiliaryIds = new Set(['visionModel', 'transcriptionModel', 'imageGenModel', ...Object.keys(lm.auxiliaryItemsAt || {}), ...Object.keys(rm.auxiliaryItemsAt || {})]); out.meta = { providersAt: Math.max(lm.providersAt || 0, rm.providersAt || 0), providerItemsAt: Object.fromEntries([...providerIds].map(id => [id, Math.max(itemTimestamp(lm, 'providerItemsAt', id), itemTimestamp(rm, 'providerItemsAt', id))])), activeProviderAt: Math.max(lm.activeProviderAt || 0, rm.activeProviderAt || 0), auxiliaryItemsAt: Object.fromEntries([...auxiliaryIds].map(id => [id, Math.max(itemTimestamp(lm, 'auxiliaryItemsAt', id), itemTimestamp(rm, 'auxiliaryItemsAt', id))])), profileAt: Math.max(lm.profileAt || 0, rm.profileAt || 0), memoryAt: Math.max(lm.memoryAt || 0, rm.memoryAt || 0) }; return { vault: out, conflicts }; } @@ -217,7 +218,7 @@ export class ProfileSyncManager { meta: rawMeta, }, s[PORTABLE_ACTIVE_PROVIDER_KEY]); const meta = portable.meta; - return { version: 1, memory: normalizeUserMemoryStore(s[USER_MEMORY_STORAGE_KEY]), tombstones: meta.tombstones || {}, providers: portable.providers, activeProvider: portable.activeProvider, auxiliaryProviders: { visionModel: s.visionModel || null, transcriptionModel: s.transcriptionModel || null }, profile: { enabled: !!s.profileEnabled, text: s.profileText || '' }, meta: { providersAt: meta.providersAt || 0, providerItemsAt: meta.providerItemsAt, activeProviderAt: meta.activeProviderAt, auxiliaryItemsAt: meta.auxiliaryItemsAt, profileAt: meta.profileAt || 0, memoryAt: meta.memoryAt || 0 } }; + return { version: 1, memory: normalizeUserMemoryStore(s[USER_MEMORY_STORAGE_KEY]), tombstones: meta.tombstones || {}, providers: portable.providers, activeProvider: portable.activeProvider, auxiliaryProviders: { visionModel: s.visionModel || null, transcriptionModel: s.transcriptionModel || null, imageGenModel: s.imageGenModel || null }, profile: { enabled: !!s.profileEnabled, text: s.profileText || '' }, meta: { providersAt: meta.providersAt || 0, providerItemsAt: meta.providerItemsAt, activeProviderAt: meta.activeProviderAt, auxiliaryItemsAt: meta.auxiliaryItemsAt, profileAt: meta.profileAt || 0, memoryAt: meta.memoryAt || 0 } }; } async request(path, options = {}) { const s = await this.storage.get(PROFILE_SYNC_KEYS.token); const token = s[PROFILE_SYNC_KEYS.token]; @@ -286,7 +287,7 @@ export class ProfileSyncManager { } if (changes.activeProvider) { meta.providersAt = now; meta.activeProviderAt = now; } if (changes.activeProvider) syncRelevantChange = true; - for (const id of ['visionModel', 'transcriptionModel']) if (changes[id]) { meta.providersAt = now; meta.auxiliaryItemsAt = meta.auxiliaryItemsAt || {}; meta.auxiliaryItemsAt[id] = now; syncRelevantChange = true; } + for (const id of ['visionModel', 'transcriptionModel', 'imageGenModel']) if (changes[id]) { meta.providersAt = now; meta.auxiliaryItemsAt = meta.auxiliaryItemsAt || {}; meta.auxiliaryItemsAt[id] = now; syncRelevantChange = true; } if (changes.profileEnabled || changes.profileText) { meta.profileAt = now; syncRelevantChange = true; } if (changes[USER_MEMORY_STORAGE_KEY]) { syncRelevantChange = true; @@ -305,7 +306,7 @@ export class ProfileSyncManager { if (syncRelevantChange) this.schedule(); } schedule() { if (this.applying || !this.password) return; clearTimeout(this.timer); this.timer = setTimeout(() => this.sync().catch((e) => { this.status = [402, 403].includes(e.status) ? 'subscription' : e instanceof TypeError ? 'offline' : 'error'; }), 1500); } - async apply(vault, conflicts) { this.applying = true; try { const current = await this.storage.get(['providers', 'activeProvider', PORTABLE_ACTIVE_PROVIDER_KEY]); const currentProviders = current.providers || {}; const localOnlyIds = nonPortableProviderIds(currentProviders); const portable = portableVault(vault, current[PORTABLE_ACTIVE_PROVIDER_KEY]); const providers = { ...portable.providers }; for (const id of localOnlyIds) if (currentProviders[id]) providers[id] = currentProviders[id]; const preserveLocalActive = localOnlyIds.has(current.activeProvider) && !!currentProviders[current.activeProvider]; const portableActive = portable.activeProvider || DEFAULT_PORTABLE_ACTIVE_PROVIDER; await this.storage.set({ [USER_MEMORY_STORAGE_KEY]: portable.memory, providers, activeProvider: preserveLocalActive ? current.activeProvider : portableActive, visionModel: portable.auxiliaryProviders?.visionModel || null, transcriptionModel: portable.auxiliaryProviders?.transcriptionModel || null, profileEnabled: portable.profile.enabled, profileText: portable.profile.text, [PORTABLE_ACTIVE_PROVIDER_KEY]: portableActive, [PROFILE_SYNC_KEYS.metadata]: { ...portable.meta, tombstones: portable.tombstones }, [PROFILE_SYNC_KEYS.recovery]: conflicts }); } finally { this.applying = false; } } + async apply(vault, conflicts) { this.applying = true; try { const current = await this.storage.get(['providers', 'activeProvider', PORTABLE_ACTIVE_PROVIDER_KEY]); const currentProviders = current.providers || {}; const localOnlyIds = nonPortableProviderIds(currentProviders); const portable = portableVault(vault, current[PORTABLE_ACTIVE_PROVIDER_KEY]); const providers = { ...portable.providers }; for (const id of localOnlyIds) if (currentProviders[id]) providers[id] = currentProviders[id]; const preserveLocalActive = localOnlyIds.has(current.activeProvider) && !!currentProviders[current.activeProvider]; const portableActive = portable.activeProvider || DEFAULT_PORTABLE_ACTIVE_PROVIDER; await this.storage.set({ [USER_MEMORY_STORAGE_KEY]: portable.memory, providers, activeProvider: preserveLocalActive ? current.activeProvider : portableActive, visionModel: portable.auxiliaryProviders?.visionModel || null, transcriptionModel: portable.auxiliaryProviders?.transcriptionModel || null, imageGenModel: portable.auxiliaryProviders?.imageGenModel || null, profileEnabled: portable.profile.enabled, profileText: portable.profile.text, [PORTABLE_ACTIVE_PROVIDER_KEY]: portableActive, [PROFILE_SYNC_KEYS.metadata]: { ...portable.meta, tombstones: portable.tombstones }, [PROFILE_SYNC_KEYS.recovery]: conflicts }); } finally { this.applying = false; } } sync(options = {}) { if (this.syncPromise) { this.syncAgain = true; return this.syncPromise; } this.syncAgain = false; diff --git a/src/chrome/src/ui/locales/ar.js b/src/chrome/src/ui/locales/ar.js index d01156655..23f38e78d 100644 --- a/src/chrome/src/ui/locales/ar.js +++ b/src/chrome/src/ui/locales/ar.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "تم الاتصال! النموذج: {model}", "st.transcription.failed": "فشل: {error}", "st.transcription.fill_required": "املأ عنوان API الأساسي والنموذج أولًا.", + "st.imagegen.heading": "الوسائط التوليدية (fal.ai)", + "st.imagegen.desc": "تُستخدَم بواسطة أداة generate_image لإنشاء الصور وغيرها من الوسائط من وصف نصي عبر واجهة قائمة الانتظار في fal.ai. احصل على مفتاح من fal.ai/dashboard/keys. لا تُستخدم للمحادثة.", + "st.imagegen.saved": "تم الحفظ!", + "st.imagegen.cleared": "تم المسح.", + "st.imagegen.testing": "جارٍ الاختبار...", + "st.imagegen.connected": "تم الاتصال! النموذج: {model}", + "st.imagegen.failed": "فشل: {error}", + "st.imagegen.fill_required": "املأ مفتاح API والنموذج أولًا.", "st.captcha.desc_html": "دع الوكيل يحلّ اختبارات CAPTCHA تلقائيًا عبر واجهة CapSolver. يدعم reCAPTCHA v2/v3 وhCaptcha وCloudflare Turnstile. يؤدي حفظ مفتاح API صالح إلى تمكين CapSolver تلقائيًا؛ وبدونه، يتوقّف الوكيل ويطلب منك حلّ الاختبار بنفسك. تتقاضى CapSolver رسومًا لكل عملية حلّ (~$0.001–$0.003)؛ تستخدم حسابك ومفتاح API الخاص بك.", "st.captcha.enabled.label": "تفعيل CapSolver", "st.captcha.enabled.desc": "عندما يصادف الوكيل اختبار CAPTCHA فإنه يستدعي CapSolver مرّة واحدة قبل اللجوء إلى سؤالك. يتطلّب مفتاح API أدناه.", diff --git a/src/chrome/src/ui/locales/bn.js b/src/chrome/src/ui/locales/bn.js index 343e3aded..c0b7db103 100644 --- a/src/chrome/src/ui/locales/bn.js +++ b/src/chrome/src/ui/locales/bn.js @@ -898,6 +898,14 @@ export default { 'st.transcription.connected': "সংযুক্ত ! মডেল: {model}", 'st.transcription.failed': "ব্যর্থ হয়েছে: {error}", 'st.transcription.fill_required': "প্রথমে বেস URL এবং মডেল পূরণ করুন।", + "st.imagegen.heading": "জেনারেটিভ মিডিয়া (fal.ai)", + "st.imagegen.desc": "generate_image এজেন্ট টুল দ্বারা fal.ai-র কিউ API-র মাধ্যমে টেক্সট প্রম্পট থেকে ছবি ও অন্যান্য মিডিয়া তৈরিতে ব্যবহৃত হয়। fal.ai/dashboard/keys থেকে কী নিন। চ্যাটের জন্য ব্যবহৃত হয় না।", + "st.imagegen.saved": "সংরক্ষিত হয়েছে!", + "st.imagegen.cleared": "মুছে ফেলা হয়েছে।", + "st.imagegen.testing": "পরীক্ষা চলছে...", + "st.imagegen.connected": "সংযুক্ত! মডেল: {model}", + "st.imagegen.failed": "ব্যর্থ: {error}", + "st.imagegen.fill_required": "প্রথমে API কী এবং মডেল পূরণ করুন।", 'st.imageBudget.heading': "ছবির বাজেট", 'st.imageBudget.desc': "স্ক্রিনশটের আকার এবং প্রতি টার্নে ভিশনের জন্য এজেন্ট কতটি স্ক্রিনশট নেবে তা নিয়ন্ত্রণ করুন। কম বিস্তারিত ও ছোট মাত্রা ছোট এন্ডপয়েন্টে খরচ ও বিলম্ব কমায়; বেশি মান বিশ্বস্ততা বজায় রাখে। ডিফল্ট মান আগের আচরণের সঙ্গে মেলে।", diff --git a/src/chrome/src/ui/locales/de.js b/src/chrome/src/ui/locales/de.js index dda2f21a6..29fb55146 100644 --- a/src/chrome/src/ui/locales/de.js +++ b/src/chrome/src/ui/locales/de.js @@ -838,6 +838,14 @@ export default { 'st.transcription.connected': 'Verbunden! Modell: {model}', 'st.transcription.failed': 'Fehlgeschlagen: {error}', 'st.transcription.fill_required': 'Füllen Sie zuerst die Basis-URL und das Modell aus.', + "st.imagegen.heading": "Generative Medien (fal.ai)", + "st.imagegen.desc": "Wird vom Agent-Tool generate_image verwendet, um aus einem Textprompt Bilder und andere Medien über die Queue-API von fal.ai zu erzeugen. Schlüssel unter fal.ai/dashboard/keys erhalten. Wird nicht für Chat verwendet.", + "st.imagegen.saved": "Gespeichert!", + "st.imagegen.cleared": "Gelöscht.", + "st.imagegen.testing": "Wird getestet...", + "st.imagegen.connected": "Verbunden! Modell: {model}", + "st.imagegen.failed": "Fehlgeschlagen: {error}", + "st.imagegen.fill_required": "Bitte zuerst API-Schlüssel und Modell ausfüllen.", // --- Image budget settings --- 'st.imageBudget.heading': 'Bildbudget', diff --git a/src/chrome/src/ui/locales/en.js b/src/chrome/src/ui/locales/en.js index ac6bc84f6..8fa0c100a 100644 --- a/src/chrome/src/ui/locales/en.js +++ b/src/chrome/src/ui/locales/en.js @@ -900,6 +900,16 @@ export default { 'st.transcription.failed': 'Failed: {error}', 'st.transcription.fill_required': 'Fill in Base URL and Model first.', + // Generative media (fal.ai) — Assistive Models section + 'st.imagegen.heading': 'Generative media (fal.ai)', + 'st.imagegen.desc': 'Used by the generate_image agent tool to create images and other media from a text prompt via fal.ai\'s queue API. Get a key at fal.ai/dashboard/keys. Not used for chat.', + 'st.imagegen.saved': 'Saved!', + 'st.imagegen.cleared': 'Cleared.', + 'st.imagegen.testing': 'Testing...', + 'st.imagegen.connected': 'Connected! Model: {model}', + 'st.imagegen.failed': 'Failed: {error}', + 'st.imagegen.fill_required': 'Fill in API Key and Model first.', + // Image budget (issue #311): tune screenshot quality + how many // screenshots the agent may capture/send per turn, and how large each // image may be. All controls live on the Assistive Models tab. diff --git a/src/chrome/src/ui/locales/es.js b/src/chrome/src/ui/locales/es.js index 19f8f23e7..5deebcac1 100644 --- a/src/chrome/src/ui/locales/es.js +++ b/src/chrome/src/ui/locales/es.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "¡Conectado! Modelo: {model}", "st.transcription.failed": "Falló: {error}", "st.transcription.fill_required": "Rellena primero la URL base y el modelo.", + "st.imagegen.heading": "Medios generativos (fal.ai)", + "st.imagegen.desc": "Usada por la herramienta de agente generate_image para crear imágenes y otros medios a partir de un prompt de texto mediante la API de cola de fal.ai. Obtén una clave en fal.ai/dashboard/keys. No se usa para chat.", + "st.imagegen.saved": "¡Guardado!", + "st.imagegen.cleared": "Borrado.", + "st.imagegen.testing": "Probando...", + "st.imagegen.connected": "¡Conectado! Modelo: {model}", + "st.imagegen.failed": "Error: {error}", + "st.imagegen.fill_required": "Rellena primero la clave de API y el modelo.", "st.captcha.desc_html": "Deja que el agente resuelva CAPTCHAs automáticamente mediante la API de CapSolver. Admite reCAPTCHA v2/v3, hCaptcha y Cloudflare Turnstile. Al guardar una clave de API válida, CapSolver se activa automáticamente; sin una, el agente se detiene y te pide que resuelvas el captcha tú mismo. CapSolver cobra por cada resolución (~$0.001–$0.003); utilizas tu propia cuenta y clave de API.", "st.captcha.enabled.label": "Activar CapSolver", "st.captcha.enabled.desc": "Cuando el agente se encuentra con un CAPTCHA, llamará a CapSolver una vez antes de recurrir a pedírtelo a ti. Requiere una clave de API más abajo.", diff --git a/src/chrome/src/ui/locales/fa.js b/src/chrome/src/ui/locales/fa.js index 65337c470..fdd3fed79 100644 --- a/src/chrome/src/ui/locales/fa.js +++ b/src/chrome/src/ui/locales/fa.js @@ -898,6 +898,14 @@ export default { 'st.transcription.connected': "متصل است! مدل: {model}", 'st.transcription.failed': "ناموفق: {error}", 'st.transcription.fill_required': "ابتدا Base URL و Model را پر کنید.", + "st.imagegen.heading": "رسانه‌های مولد (fal.ai)", + "st.imagegen.desc": "ابزار ایجنت generate_image برای ساخت تصویر و سایر رسانه‌ها از یک پرامپت متنی از طریق API صف fal.ai استفاده می‌کند. کلید را از fal.ai/dashboard/keys بگیرید. برای چت استفاده نمی‌شود.", + "st.imagegen.saved": "ذخیره شد!", + "st.imagegen.cleared": "پاک شد.", + "st.imagegen.testing": "در حال آزمایش...", + "st.imagegen.connected": "متصل شد! مدل: {model}", + "st.imagegen.failed": "خطا: {error}", + "st.imagegen.fill_required": "ابتدا کلید API و مدل را پر کنید.", 'st.imageBudget.heading': "بودجه تصویر", 'st.imageBudget.desc': "اندازه اسکرین‌شات و تعداد تصاویری را که عامل در هر نوبت برای بینایی می‌گیرد کنترل کنید. جزئیات و ابعاد کمتر، هزینه و تأخیر را برای نقاط پایانی کوچک‌تر کاهش می‌دهد؛ مقادیر بیشتر وفاداری را حفظ می‌کند. پیش‌فرض‌ها با رفتار قبلی مطابقت دارند.", diff --git a/src/chrome/src/ui/locales/fr.js b/src/chrome/src/ui/locales/fr.js index 95c686859..3e711d49b 100644 --- a/src/chrome/src/ui/locales/fr.js +++ b/src/chrome/src/ui/locales/fr.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "Connecté ! Modèle : {model}", "st.transcription.failed": "Échec : {error}", "st.transcription.fill_required": "Remplissez d'abord l'URL de base et le modèle.", + "st.imagegen.heading": "Médias génératifs (fal.ai)", + "st.imagegen.desc": "Utilisée par l'outil d'agent generate_image pour créer des images et d'autres médias à partir d'un prompt texte via l'API de file d'attente de fal.ai. Obtenez une clé sur fal.ai/dashboard/keys. Non utilisée pour le chat.", + "st.imagegen.saved": "Enregistré !", + "st.imagegen.cleared": "Effacé.", + "st.imagegen.testing": "Test en cours...", + "st.imagegen.connected": "Connecté ! Modèle : {model}", + "st.imagegen.failed": "Échec : {error}", + "st.imagegen.fill_required": "Remplissez d'abord la clé API et le modèle.", "st.captcha.desc_html": "Laissez l'agent résoudre les CAPTCHA automatiquement via l'API CapSolver. Prend en charge reCAPTCHA v2/v3, hCaptcha et Cloudflare Turnstile. L'enregistrement d'une clé API valide active automatiquement CapSolver ; sans clé, l'agent s'arrête et vous demande de résoudre le CAPTCHA vous-même. CapSolver facture chaque résolution (~$0.001–$0.003) ; vous utilisez votre propre compte et votre propre clé API.", "st.captcha.enabled.label": "Activer CapSolver", "st.captcha.enabled.desc": "Lorsque l'agent rencontre un CAPTCHA, il appellera CapSolver une fois avant de se rabattre sur une demande de votre part. Nécessite une clé d'API ci-dessous.", diff --git a/src/chrome/src/ui/locales/he.js b/src/chrome/src/ui/locales/he.js index 19e71e557..33f6eca79 100644 --- a/src/chrome/src/ui/locales/he.js +++ b/src/chrome/src/ui/locales/he.js @@ -819,6 +819,14 @@ export default { "st.transcription.connected": "מחובר! מודל: {model}", "st.transcription.failed": "נכשל: {error}", "st.transcription.fill_required": "יש למלא תחילה כתובת URL בסיסית ומודל.", + "st.imagegen.heading": "מדיה גנרטיבית (fal.ai)", + "st.imagegen.desc": "בשימוש על ידי כלי הסוכן generate_image ליצירת תמונות ומדיה אחרת מתיאור טקסטואלי דרך ה-Queue API של fal.ai. אפשר לקבל מפתח ב-fal.ai/dashboard/keys. אינו משמש לצ׳אט.", + "st.imagegen.saved": "נשמר!", + "st.imagegen.cleared": "נמחק.", + "st.imagegen.testing": "בודק...", + "st.imagegen.connected": "מחובר! דגם: {model}", + "st.imagegen.failed": "נכשל: {error}", + "st.imagegen.fill_required": "מלא קודם מפתח API ודגם.", "st.profile.desc_html": "שמור ביוגרפיה קצרה שבה הסוכן יכול להשתמש למילוי טופסי הרשמה בלי לשאול בכל פעם — השם שלך, כתובת דוא״ל לעבודה, החברה וסיסמה ייעודית להרשמות בעלות סיכון נמוך. כאשר האפשרות מופעלת, הטקסט שלהלן מצורף להנחיית המערכת של הסוכן בכל שיחה.", "st.profile.enabled.label": "הפעל מילוי אוטומטי של פרופיל", "st.profile.enabled.desc": "הכנס את טקסט הפרופיל להנחיית המערכת של הסוכן. מושבת = הסוכן לעולם לא רואה את הטקסט הזה.", diff --git a/src/chrome/src/ui/locales/hi.js b/src/chrome/src/ui/locales/hi.js index e51e954c6..09384a8cc 100644 --- a/src/chrome/src/ui/locales/hi.js +++ b/src/chrome/src/ui/locales/hi.js @@ -898,6 +898,14 @@ export default { 'st.transcription.connected': "जुड़ा हुआ! मॉडल: {model}", 'st.transcription.failed': "विफल: {error}", 'st.transcription.fill_required': "सबसे पहले बेस यूआरएल और मॉडल भरें।", + "st.imagegen.heading": "जनरेटिव मीडिया (fal.ai)", + "st.imagegen.desc": "generate_image एजेंट टूल द्वारा fal.ai की क्यू API के ज़रिए टेक्स्ट प्रॉम्प्ट से इमेज और अन्य मीडिया बनाने के लिए उपयोग किया जाता है। fal.ai/dashboard/keys से की लें। चैट के लिए उपयोग नहीं होता।", + "st.imagegen.saved": "सहेजा गया!", + "st.imagegen.cleared": "साफ़ कर दिया गया।", + "st.imagegen.testing": "जाँच जारी...", + "st.imagegen.connected": "कनेक्ट हो गया! मॉडल: {model}", + "st.imagegen.failed": "विफल: {error}", + "st.imagegen.fill_required": "पहले API की और मॉडल भरें।", 'st.imageBudget.heading': "छवि बजट", 'st.imageBudget.desc': "स्क्रीनशॉट का आकार और हर टर्न में विज़न के लिए एजेंट द्वारा लिए जाने वाले स्क्रीनशॉट की संख्या नियंत्रित करें। कम विवरण और छोटे आयाम छोटे एंडपॉइंट के लिए लागत और विलंबता घटाते हैं; अधिक मान गुणवत्ता बनाए रखते हैं। डिफ़ॉल्ट पिछले व्यवहार से मेल खाते हैं।", diff --git a/src/chrome/src/ui/locales/id.js b/src/chrome/src/ui/locales/id.js index d0c0d754e..329bb8f7f 100644 --- a/src/chrome/src/ui/locales/id.js +++ b/src/chrome/src/ui/locales/id.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "Terhubung! Model: {model}", "st.transcription.failed": "Gagal: {error}", "st.transcription.fill_required": "Isi URL dasar dan Model terlebih dahulu.", + "st.imagegen.heading": "Media generatif (fal.ai)", + "st.imagegen.desc": "Digunakan oleh alat agen generate_image untuk membuat gambar dan media lain dari prompt teks melalui API antrean fal.ai. Dapatkan kunci di fal.ai/dashboard/keys. Tidak digunakan untuk obrolan.", + "st.imagegen.saved": "Tersimpan!", + "st.imagegen.cleared": "Dihapus.", + "st.imagegen.testing": "Menguji...", + "st.imagegen.connected": "Terhubung! Model: {model}", + "st.imagegen.failed": "Gagal: {error}", + "st.imagegen.fill_required": "Isi Kunci API dan Model terlebih dahulu.", "st.captcha.desc_html": "Biarkan agen menyelesaikan CAPTCHA secara otomatis melalui API CapSolver. Mendukung reCAPTCHA v2/v3, hCaptcha, dan Cloudflare Turnstile. Menyimpan kunci API yang valid akan mengaktifkan CapSolver secara otomatis; tanpa kunci, agen berhenti dan meminta Anda menyelesaikan CAPTCHA sendiri. CapSolver mengenakan biaya per penyelesaian (~$0.001–$0.003); Anda menggunakan akun dan kunci API sendiri.", "st.captcha.enabled.label": "Aktifkan CapSolver", "st.captcha.enabled.desc": "Saat agen menemui CAPTCHA, ia akan memanggil CapSolver sekali sebelum kembali bertanya kepada Anda. Memerlukan kunci API di bawah.", diff --git a/src/chrome/src/ui/locales/ja.js b/src/chrome/src/ui/locales/ja.js index ec91ad59e..1fe8db3a5 100644 --- a/src/chrome/src/ui/locales/ja.js +++ b/src/chrome/src/ui/locales/ja.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "接続しました!モデル: {model}", "st.transcription.failed": "失敗: {error}", "st.transcription.fill_required": "先にベース URL とモデルを入力してください。", + "st.imagegen.heading": "生成メディア (fal.ai)", + "st.imagegen.desc": "generate_image エージェントツールが fal.ai のキュー API を通じてテキストプロンプトから画像などのメディアを生成するために使用します。キーは fal.ai/dashboard/keys で取得できます。チャットには使用されません。", + "st.imagegen.saved": "保存しました!", + "st.imagegen.cleared": "クリアしました。", + "st.imagegen.testing": "テスト中...", + "st.imagegen.connected": "接続しました!モデル: {model}", + "st.imagegen.failed": "失敗: {error}", + "st.imagegen.fill_required": "先に API キーとモデルを入力してください。", "st.captcha.desc_html": "CapSolver API を使って、エージェントに CAPTCHA を自動で解かせます。reCAPTCHA v2/v3、hCaptcha、Cloudflare Turnstile に対応しています。有効な API キーを保存すると CapSolver が自動的に有効になります。キーがない場合、エージェントは停止し、CAPTCHA を手動で解くよう求めます。CapSolver は解決ごとに課金します(約 ~$0.001–$0.003)。ご自身のアカウントと API キーを使用します。", "st.captcha.enabled.label": "CapSolver を有効化", "st.captcha.enabled.desc": "エージェントが CAPTCHA に遭遇すると、あなたに尋ねる前にまず CapSolver を 1 回呼び出します。下記の API キーが必要です。", diff --git a/src/chrome/src/ui/locales/ko.js b/src/chrome/src/ui/locales/ko.js index e779ed039..9af426bd9 100644 --- a/src/chrome/src/ui/locales/ko.js +++ b/src/chrome/src/ui/locales/ko.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "연결됨! 모델: {model}", "st.transcription.failed": "실패: {error}", "st.transcription.fill_required": "먼저 기본 URL과 모델을 입력하세요.", + "st.imagegen.heading": "생성 미디어 (fal.ai)", + "st.imagegen.desc": "generate_image 에이전트 도구가 fal.ai 큐 API를 통해 텍스트 프롬프트에서 이미지 등 미디어를 생성하는 데 사용됩니다. fal.ai/dashboard/keys에서 키를 발급받으세요. 채팅에는 사용되지 않습니다.", + "st.imagegen.saved": "저장되었습니다!", + "st.imagegen.cleared": "지워졌습니다.", + "st.imagegen.testing": "테스트 중...", + "st.imagegen.connected": "연결됨! 모델: {model}", + "st.imagegen.failed": "실패: {error}", + "st.imagegen.fill_required": "먼저 API 키와 모델을 입력하세요.", "st.captcha.desc_html": "에이전트가 CapSolver API를 통해 CAPTCHA를 자동으로 풀게 합니다. reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile을 지원합니다. 유효한 API 키를 저장하면 CapSolver가 자동으로 활성화됩니다. 키가 없으면 에이전트가 멈추고 CAPTCHA를 직접 풀어 달라고 요청합니다. CapSolver는 풀이당 요금을 부과하며(~$0.001–$0.003), 본인의 계정과 API 키를 사용합니다.", "st.captcha.enabled.label": "CapSolver 사용", "st.captcha.enabled.desc": "에이전트가 CAPTCHA를 만나면 사용자에게 묻기로 폴백하기 전에 CapSolver를 한 번 호출합니다. 아래에 API 키가 필요합니다.", diff --git a/src/chrome/src/ui/locales/ms.js b/src/chrome/src/ui/locales/ms.js index 5c9e6da1e..fefaba6e3 100644 --- a/src/chrome/src/ui/locales/ms.js +++ b/src/chrome/src/ui/locales/ms.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "Tersambung! Model: {model}", "st.transcription.failed": "Gagal: {error}", "st.transcription.fill_required": "Isi URL Asas dan Model terlebih dahulu.", + "st.imagegen.heading": "Media generatif (fal.ai)", + "st.imagegen.desc": "Digunakan oleh alat ejen generate_image untuk mencipta imej dan media lain daripada geseran teks melalui API baris giliran fal.ai. Dapatkan kunci di fal.ai/dashboard/keys. Tidak digunakan untuk sembang.", + "st.imagegen.saved": "Disimpan!", + "st.imagegen.cleared": "Dibersihkan.", + "st.imagegen.testing": "Menguji...", + "st.imagegen.connected": "Berjaya disambung! Model: {model}", + "st.imagegen.failed": "Gagal: {error}", + "st.imagegen.fill_required": "Isi Kunci API dan Model dahulu.", "st.captcha.desc_html": "Biarkan ejen menyelesaikan CAPTCHA secara automatik melalui API CapSolver. Menyokong reCAPTCHA v2/v3, hCaptcha dan Cloudflare Turnstile. Menyimpan kunci API yang sah akan mengaktifkan CapSolver secara automatik; tanpa kunci, ejen berhenti dan meminta anda menyelesaikan CAPTCHA sendiri. CapSolver mengenakan bayaran bagi setiap penyelesaian (~$0.001–$0.003); anda menggunakan akaun dan kunci API sendiri.", "st.captcha.enabled.label": "Dayakan CapSolver", "st.captcha.enabled.desc": "Apabila ejen menemui CAPTCHA, ia akan memanggil CapSolver sekali sebelum berundur untuk bertanya kepada anda. Memerlukan kunci API di bawah.", diff --git a/src/chrome/src/ui/locales/nl.js b/src/chrome/src/ui/locales/nl.js index d79d629bf..f4aae4c2b 100644 --- a/src/chrome/src/ui/locales/nl.js +++ b/src/chrome/src/ui/locales/nl.js @@ -806,6 +806,14 @@ export default { 'st.transcription.connected': 'Verbonden! Model: {model}', 'st.transcription.failed': 'Mislukt: {error}', 'st.transcription.fill_required': 'Vul eerst basis-URL en model in.', + "st.imagegen.heading": "Generatieve media (fal.ai)", + "st.imagegen.desc": "Wordt gebruikt door de agenttool generate_image om afbeeldingen en andere media te maken vanuit een tekstprompt via de queue-API van fal.ai. Vraag een sleutel aan op fal.ai/dashboard/keys. Niet voor chat.", + "st.imagegen.saved": "Opgeslagen!", + "st.imagegen.cleared": "Wissen gelukt.", + "st.imagegen.testing": "Testen...", + "st.imagegen.connected": "Verbonden! Model: {model}", + "st.imagegen.failed": "Mislukt: {error}", + "st.imagegen.fill_required": "Vul eerst de API-sleutel en het model in.", 'st.imageBudget.heading': 'Afbeeldingsbudget', 'st.imageBudget.desc': 'Beheer schermafbeeldingsgrootte en hoeveel de agent vastlegt voor visie per beurt...', 'st.imageBudget.detail.label': 'Afbeeldingsdetail', diff --git a/src/chrome/src/ui/locales/pl.js b/src/chrome/src/ui/locales/pl.js index 91c14e54e..8f6d7b9fd 100644 --- a/src/chrome/src/ui/locales/pl.js +++ b/src/chrome/src/ui/locales/pl.js @@ -636,6 +636,14 @@ export default { 'st.transcription.connected': 'Połączono! Model: {model}', 'st.transcription.failed': 'Niepowodzenie: {error}', 'st.transcription.fill_required': 'Najpierw wypełnij bazowy adres URL i model.', + "st.imagegen.heading": "Media generatywne (fal.ai)", + "st.imagegen.desc": "Używane przez narzędzie agenta generate_image do tworzenia obrazów i innych mediów z promptu tekstowego przez API kolejki fal.ai. Klucz uzyskasz na fal.ai/dashboard/keys. Nieużywane do czatu.", + "st.imagegen.saved": "Zapisano!", + "st.imagegen.cleared": "Wyczyszczono.", + "st.imagegen.testing": "Testowanie...", + "st.imagegen.connected": "Połączono! Model: {model}", + "st.imagegen.failed": "Niepowodzenie: {error}", + "st.imagegen.fill_required": "Najpierw wypełnij klucz API i model.", 'st.profile.desc_html': 'Przechowuj krótki opis, którego agent może użyć do wypełniania formularzy rejestracji bez pytania za każdym razem — Twoje imię, służbowy e-mail, firmę i jednorazowe hasło do mało istotnych rejestracji. Po włączeniu poniższy tekst jest dołączany do promptu systemowego agenta w każdej rozmowie.', 'st.profile.enabled.label': 'Włącz automatyczne wypełnianie profilu', 'st.profile.enabled.desc': 'Wstrzykuj tekst profilu do promptu systemowego agenta. Wyłączone = agent nigdy nie widzi tego tekstu.', diff --git a/src/chrome/src/ui/locales/pt.js b/src/chrome/src/ui/locales/pt.js index b8c078b1f..a7c8d0ba5 100644 --- a/src/chrome/src/ui/locales/pt.js +++ b/src/chrome/src/ui/locales/pt.js @@ -898,6 +898,14 @@ export default { 'st.transcription.connected': "Conectado! Modelo: {model}", 'st.transcription.failed': "Falha: {error}", 'st.transcription.fill_required': "Preencha primeiro o URL base e o modelo.", + "st.imagegen.heading": "Mídia generativa (fal.ai)", + "st.imagegen.desc": "Usada pela ferramenta de agente generate_image para criar imagens e outras mídias a partir de um prompt de texto pela API de fila da fal.ai. Obtenha uma chave em fal.ai/dashboard/keys. Não usada para chat.", + "st.imagegen.saved": "Salvo!", + "st.imagegen.cleared": "Limpo.", + "st.imagegen.testing": "Testando...", + "st.imagegen.connected": "Conectado! Modelo: {model}", + "st.imagegen.failed": "Falhou: {error}", + "st.imagegen.fill_required": "Preencha primeiro a chave de API e o modelo.", 'st.imageBudget.heading': "Orçamento de imagens", 'st.imageBudget.desc': "Controle o tamanho das capturas de tela e quantas o agente faz para visão por turno. Menos detalhes e dimensões menores reduzem custo e latência em endpoints menores; valores maiores preservam a fidelidade. Os padrões correspondem ao comportamento anterior.", diff --git a/src/chrome/src/ui/locales/ru.js b/src/chrome/src/ui/locales/ru.js index fc21b576c..4fe847f44 100644 --- a/src/chrome/src/ui/locales/ru.js +++ b/src/chrome/src/ui/locales/ru.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "Подключено! Модель: {model}", "st.transcription.failed": "Сбой: {error}", "st.transcription.fill_required": "Сначала заполните базовый URL и модель.", + "st.imagegen.heading": "Генеративные медиа (fal.ai)", + "st.imagegen.desc": "Используется инструментом агента generate_image для создания изображений и других медиа из текстового запроса через очередь API fal.ai. Ключ можно получить на fal.ai/dashboard/keys. Не используется для чата.", + "st.imagegen.saved": "Сохранено!", + "st.imagegen.cleared": "Очищено.", + "st.imagegen.testing": "Проверка...", + "st.imagegen.connected": "Подключено! Модель: {model}", + "st.imagegen.failed": "Ошибка: {error}", + "st.imagegen.fill_required": "Сначала заполните ключ API и модель.", "st.captcha.desc_html": "Позвольте агенту автоматически решать CAPTCHA через API CapSolver. Поддерживает reCAPTCHA v2/v3, hCaptcha и Cloudflare Turnstile. Сохранение действительного API-ключа автоматически включает CapSolver; без ключа агент останавливается и просит вас решить CAPTCHA самостоятельно. CapSolver берёт плату за каждое решение (~$0.001–$0.003); используется ваш аккаунт и API-ключ.", "st.captcha.enabled.label": "Включить CapSolver", "st.captcha.enabled.desc": "Когда агент сталкивается с CAPTCHA, он один раз обращается к CapSolver, прежде чем перейти к запросу к вам. Требуется API-ключ ниже.", diff --git a/src/chrome/src/ui/locales/th.js b/src/chrome/src/ui/locales/th.js index 87cb25715..82dc98447 100644 --- a/src/chrome/src/ui/locales/th.js +++ b/src/chrome/src/ui/locales/th.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "เชื่อมต่อแล้ว! โมเดล: {model}", "st.transcription.failed": "ล้มเหลว: {error}", "st.transcription.fill_required": "กรอก URL ฐานและโมเดลก่อน", + "st.imagegen.heading": "สื่อเชิงสร้างสรรค์ (fal.ai)", + "st.imagegen.desc": "เครื่องมือเอเจนต์ generate_image ใช้สร้างรูปภาพและสื่ออื่น ๆ จากพรอมต์ข้อความผ่านคิว API ของ fal.ai รับคีย์ได้ที่ fal.ai/dashboard/keys ไม่ได้ใช้สำหรับแชท", + "st.imagegen.saved": "บันทึกแล้ว!", + "st.imagegen.cleared": "ล้างแล้ว.", + "st.imagegen.testing": "กำลังทดสอบ...", + "st.imagegen.connected": "เชื่อมต่อแล้ว! โมเดล: {model}", + "st.imagegen.failed": "ล้มเหลว: {error}", + "st.imagegen.fill_required": "กรอกคีย์ API และโมเดลก่อน", "st.captcha.desc_html": "ให้เอเจนต์แก้ CAPTCHA โดยอัตโนมัติผ่าน API ของ CapSolver รองรับ reCAPTCHA v2/v3, hCaptcha และ Cloudflare Turnstile การบันทึกคีย์ API ที่ถูกต้องจะเปิดใช้ CapSolver โดยอัตโนมัติ หากไม่มีคีย์ เอเจนต์จะหยุดและขอให้คุณแก้ CAPTCHA เอง CapSolver คิดค่าบริการต่อการแก้หนึ่งครั้ง (~$0.001–$0.003) โดยใช้บัญชีและคีย์ API ของคุณเอง", "st.captcha.enabled.label": "เปิดใช้งาน CapSolver", "st.captcha.enabled.desc": "เมื่อเอเจนต์เจอ CAPTCHA มันจะเรียก CapSolver หนึ่งครั้งก่อนถอยไปถามคุณ ต้องมีคีย์ API ด้านล่าง", diff --git a/src/chrome/src/ui/locales/tl.js b/src/chrome/src/ui/locales/tl.js index df9c2b767..c7f395025 100644 --- a/src/chrome/src/ui/locales/tl.js +++ b/src/chrome/src/ui/locales/tl.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "Konektado! Modelo: {model}", "st.transcription.failed": "Nabigo: {error}", "st.transcription.fill_required": "Punan muna ang Base URL at Modelo.", + "st.imagegen.heading": "Generative media (fal.ai)", + "st.imagegen.desc": "Ginagamit ng generate_image agent tool para gumawa ng mga larawan at iba pang media mula sa text prompt sa pamamagitan ng fal.ai queue API. Kumuha ng key sa fal.ai/dashboard/keys. Hindi ginagamit sa chat.", + "st.imagegen.saved": "Na-save!", + "st.imagegen.cleared": "Na-clear.", + "st.imagegen.testing": "Sinusuri...", + "st.imagegen.connected": "Nakakonekta! Model: {model}", + "st.imagegen.failed": "Nabigo: {error}", + "st.imagegen.fill_required": "Punan muna ang API Key at Model.", "st.captcha.desc_html": "Hayaan ang ahente na awtomatikong lutasin ang mga CAPTCHA sa pamamagitan ng CapSolver API. Sinusuportahan ang reCAPTCHA v2/v3, hCaptcha, at Cloudflare Turnstile. Awtomatikong pinapagana ang CapSolver kapag nag-save ka ng wastong API key; kung walang key, hihinto ang ahente at hihilingin sa iyong lutasin mismo ang CAPTCHA. Naniningil ang CapSolver sa bawat solve (~$0.001–$0.003); ginagamit mo ang sarili mong account at API key.", "st.captcha.enabled.label": "I-enable ang CapSolver", "st.captcha.enabled.desc": "Kapag may naabot na CAPTCHA ang ahente, tatawag ito sa CapSolver nang isang beses bago bumalik sa pagtatanong sa iyo. Kailangan ng API key sa ibaba.", diff --git a/src/chrome/src/ui/locales/tr.js b/src/chrome/src/ui/locales/tr.js index e16320bb4..3fa204062 100644 --- a/src/chrome/src/ui/locales/tr.js +++ b/src/chrome/src/ui/locales/tr.js @@ -649,6 +649,14 @@ export default { "st.transcription.connected": "Bağlandı! Model: {model}", "st.transcription.failed": "Başarısız: {error}", "st.transcription.fill_required": "Önce Temel URL ve Model alanlarını doldur.", + "st.imagegen.heading": "Üretken medya (fal.ai)", + "st.imagegen.desc": "generate_image ajan aracı tarafından metin isteminden fal.ai'nin kuyruk API'si üzerinden görsel ve diğer medyaları oluşturmak için kullanılır. Anahtar alın: fal.ai/dashboard/keys. Sohbet için kullanılmaz.", + "st.imagegen.saved": "Kaydedildi!", + "st.imagegen.cleared": "Temizlendi.", + "st.imagegen.testing": "Test ediliyor...", + "st.imagegen.connected": "Bağlandı! Model: {model}", + "st.imagegen.failed": "Başarısız: {error}", + "st.imagegen.fill_required": "Önce API anahtarını ve modeli doldurun.", "st.captcha.desc_html": "Aracının CAPTCHA'ları CapSolver API'si üzerinden otomatik çözmesine izin ver. reCAPTCHA v2/v3, hCaptcha ve Cloudflare Turnstile desteklenir. Geçerli bir API anahtarı kaydedildiğinde CapSolver otomatik olarak etkinleşir; anahtar yoksa aracı durur ve CAPTCHA'yı senin çözmeni ister. CapSolver her çözüm için ücret alır (~$0.001–$0.003); kendi hesabını ve API anahtarını kullanırsın.", "st.captcha.enabled.label": "CapSolver'ı etkinleştir", "st.captcha.enabled.desc": "Aracı bir CAPTCHA ile karşılaştığında, sana sormaya geri dönmeden önce bir kez CapSolver'ı çağırır. Aşağıda bir API anahtarı gerektirir.", diff --git a/src/chrome/src/ui/locales/uk.js b/src/chrome/src/ui/locales/uk.js index 1e84c34a3..760a395c3 100644 --- a/src/chrome/src/ui/locales/uk.js +++ b/src/chrome/src/ui/locales/uk.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "Підключено! Модель: {model}", "st.transcription.failed": "Збій: {error}", "st.transcription.fill_required": "Спочатку заповніть базовий URL і модель.", + "st.imagegen.heading": "Генеративні медіа (fal.ai)", + "st.imagegen.desc": "Використовується інструментом агента generate_image для створення зображень та інших медіа з текстового запиту через API черги fal.ai. Ключ можна отримати на fal.ai/dashboard/keys. Не використовується для чату.", + "st.imagegen.saved": "Збережено!", + "st.imagegen.cleared": "Очищено.", + "st.imagegen.testing": "Перевірка...", + "st.imagegen.connected": "Підключено! Модель: {model}", + "st.imagegen.failed": "Помилка: {error}", + "st.imagegen.fill_required": "Спочатку заповніть ключ API і модель.", "st.captcha.desc_html": "Дозвольте агенту автоматично розв'язувати CAPTCHA через API CapSolver. Підтримує reCAPTCHA v2/v3, hCaptcha та Cloudflare Turnstile. Збереження дійсного API-ключа автоматично вмикає CapSolver; без ключа агент зупиняється й просить вас розв'язати CAPTCHA самостійно. CapSolver стягує плату за кожне розв'язання (~$0.001–$0.003); ви використовуєте власний акаунт і API-ключ.", "st.captcha.enabled.label": "Увімкнути CapSolver", "st.captcha.enabled.desc": "Коли агент натрапляє на CAPTCHA, він один раз викличе CapSolver, перш ніж повернутися до запиту до вас. Потрібен API-ключ нижче.", diff --git a/src/chrome/src/ui/locales/vi.js b/src/chrome/src/ui/locales/vi.js index c04b9885f..db659ce7e 100644 --- a/src/chrome/src/ui/locales/vi.js +++ b/src/chrome/src/ui/locales/vi.js @@ -898,6 +898,14 @@ export default { 'st.transcription.connected': "Đã kết nối! Model: {model}", 'st.transcription.failed': "Không thành công: {error}", 'st.transcription.fill_required': "Trước tiên hãy điền URL cơ sở và Mô hình.", + "st.imagegen.heading": "Media tạo sinh (fal.ai)", + "st.imagegen.desc": "Được công cụ tác nhân generate_image sử dụng để tạo hình ảnh và phương tiện khác từ lời nhắc văn bản thông qua API hàng đợi của fal.ai. Lấy khóa tại fal.ai/dashboard/keys. Không dùng cho trò chuyện.", + "st.imagegen.saved": "Đã lưu!", + "st.imagegen.cleared": "Đã xóa.", + "st.imagegen.testing": "Đang kiểm tra...", + "st.imagegen.connected": "Đã kết nối! Mô hình: {model}", + "st.imagegen.failed": "Thất bại: {error}", + "st.imagegen.fill_required": "Điền Khóa API và Mô hình trước.", 'st.imageBudget.heading': "Ngân sách hình ảnh", 'st.imageBudget.desc': "Kiểm soát kích thước ảnh chụp màn hình và số ảnh tác nhân chụp cho thị giác trong mỗi lượt. Mức chi tiết và kích thước thấp hơn giúp giảm chi phí và độ trễ cho các endpoint nhỏ; mức cao hơn giữ độ trung thực. Giá trị mặc định khớp với hành vi trước đây.", diff --git a/src/chrome/src/ui/locales/zh.js b/src/chrome/src/ui/locales/zh.js index bac0e4df9..533cfbc0f 100644 --- a/src/chrome/src/ui/locales/zh.js +++ b/src/chrome/src/ui/locales/zh.js @@ -609,6 +609,14 @@ export default { "st.transcription.connected": "连接成功!模型:{model}", "st.transcription.failed": "失败:{error}", "st.transcription.fill_required": "请先填写基础 URL 和模型。", + "st.imagegen.heading": "生成式媒体 (fal.ai)", + "st.imagegen.desc": "generate_image 代理工具通过 fal.ai 的队列 API 根据文本提示生成图片等媒体。请前往 fal.ai/dashboard/keys 获取密钥。不用于聊天。", + "st.imagegen.saved": "已保存!", + "st.imagegen.cleared": "已清除。", + "st.imagegen.testing": "测试中...", + "st.imagegen.connected": "已连接!模型:{model}", + "st.imagegen.failed": "失败:{error}", + "st.imagegen.fill_required": "请先填写 API 密钥和模型。", "st.captcha.desc_html": "让代理通过 CapSolver API 自动解决 CAPTCHA。支持 reCAPTCHA v2/v3、hCaptcha 和 Cloudflare Turnstile。保存有效的 API 密钥后会自动启用 CapSolver;没有密钥时,代理会停止并请你自行解决 CAPTCHA。CapSolver 按每次解决计费(约 ~$0.001–$0.003);使用你自己的账号和 API 密钥。", "st.captcha.enabled.label": "启用 CapSolver", "st.captcha.enabled.desc": "当代理遇到 CAPTCHA 时,会先调用一次 CapSolver,然后再回退到询问你。需要下方的 API 密钥。", diff --git a/src/chrome/src/ui/settings.html b/src/chrome/src/ui/settings.html index f1ac29d1d..b6a4e8a3a 100644 --- a/src/chrome/src/ui/settings.html +++ b/src/chrome/src/ui/settings.html @@ -1987,6 +1987,32 @@

+

+ + +

+
+
+
+ + +
+
+ + +
+
+ + + +
+
+
+

ייעודית להרשמות בעלות סיכון נמוך. כאשר האפשרות מופעלת, הטקסט שלהלן מצורף להנחיית המערכת של הסוכן בכל שיחה.", "st.profile.enabled.label": "הפעל מילוי אוטומטי של פרופיל", "st.profile.enabled.desc": "הכנס את טקסט הפרופיל להנחיית המערכת של הסוכן. מושבת = הסוכן לעולם לא רואה את הטקסט הזה.", diff --git a/src/firefox/src/ui/locales/hi.js b/src/firefox/src/ui/locales/hi.js index 9905ecca7..adbe69001 100644 --- a/src/firefox/src/ui/locales/hi.js +++ b/src/firefox/src/ui/locales/hi.js @@ -877,6 +877,14 @@ export default { 'st.transcription.connected': "जुड़ा हुआ! मॉडल: {model}", 'st.transcription.failed': "विफल: {error}", 'st.transcription.fill_required': "सबसे पहले बेस यूआरएल और मॉडल भरें।", + "st.imagegen.heading": "जनरेटिव मीडिया (fal.ai)", + "st.imagegen.desc": "generate_image एजेंट टूल द्वारा fal.ai की क्यू API के ज़रिए टेक्स्ट प्रॉम्प्ट से इमेज और अन्य मीडिया बनाने के लिए उपयोग किया जाता है। fal.ai/dashboard/keys से की लें। चैट के लिए उपयोग नहीं होता।", + "st.imagegen.saved": "सहेजा गया!", + "st.imagegen.cleared": "साफ़ कर दिया गया।", + "st.imagegen.testing": "जाँच जारी...", + "st.imagegen.connected": "कनेक्ट हो गया! मॉडल: {model}", + "st.imagegen.failed": "विफल: {error}", + "st.imagegen.fill_required": "पहले API की और मॉडल भरें।", 'st.imageBudget.heading': "छवि बजट", 'st.imageBudget.desc': "स्क्रीनशॉट का आकार और हर टर्न में विज़न के लिए एजेंट द्वारा लिए जाने वाले स्क्रीनशॉट की संख्या नियंत्रित करें। कम विवरण और छोटे आयाम छोटे एंडपॉइंट के लिए लागत और विलंबता घटाते हैं; अधिक मान गुणवत्ता बनाए रखते हैं। डिफ़ॉल्ट पिछले व्यवहार से मेल खाते हैं।", diff --git a/src/firefox/src/ui/locales/id.js b/src/firefox/src/ui/locales/id.js index ffb29b091..58ec1cc67 100644 --- a/src/firefox/src/ui/locales/id.js +++ b/src/firefox/src/ui/locales/id.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "Terhubung! Model: {model}", "st.transcription.failed": "Gagal: {error}", "st.transcription.fill_required": "Isi URL dasar dan Model terlebih dahulu.", + "st.imagegen.heading": "Media generatif (fal.ai)", + "st.imagegen.desc": "Digunakan oleh alat agen generate_image untuk membuat gambar dan media lain dari prompt teks melalui API antrean fal.ai. Dapatkan kunci di fal.ai/dashboard/keys. Tidak digunakan untuk obrolan.", + "st.imagegen.saved": "Tersimpan!", + "st.imagegen.cleared": "Dihapus.", + "st.imagegen.testing": "Menguji...", + "st.imagegen.connected": "Terhubung! Model: {model}", + "st.imagegen.failed": "Gagal: {error}", + "st.imagegen.fill_required": "Isi Kunci API dan Model terlebih dahulu.", "st.captcha.desc_html": "Biarkan agen menyelesaikan CAPTCHA secara otomatis melalui API CapSolver. Mendukung reCAPTCHA v2/v3, hCaptcha, dan Cloudflare Turnstile. Menyimpan kunci API yang valid akan mengaktifkan CapSolver secara otomatis; tanpa kunci, agen berhenti dan meminta Anda menyelesaikan CAPTCHA sendiri. CapSolver mengenakan biaya per penyelesaian (~$0.001–$0.003); Anda menggunakan akun dan kunci API sendiri.", "st.captcha.enabled.label": "Aktifkan CapSolver", "st.captcha.enabled.desc": "Saat agen menemui CAPTCHA, ia akan memanggil CapSolver sekali sebelum kembali bertanya kepada Anda. Memerlukan kunci API di bawah.", diff --git a/src/firefox/src/ui/locales/ja.js b/src/firefox/src/ui/locales/ja.js index 1e1c55ae5..a05887113 100644 --- a/src/firefox/src/ui/locales/ja.js +++ b/src/firefox/src/ui/locales/ja.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "接続しました!モデル: {model}", "st.transcription.failed": "失敗: {error}", "st.transcription.fill_required": "先にベース URL とモデルを入力してください。", + "st.imagegen.heading": "生成メディア (fal.ai)", + "st.imagegen.desc": "generate_image エージェントツールが fal.ai のキュー API を通じてテキストプロンプトから画像などのメディアを生成するために使用します。キーは fal.ai/dashboard/keys で取得できます。チャットには使用されません。", + "st.imagegen.saved": "保存しました!", + "st.imagegen.cleared": "クリアしました。", + "st.imagegen.testing": "テスト中...", + "st.imagegen.connected": "接続しました!モデル: {model}", + "st.imagegen.failed": "失敗: {error}", + "st.imagegen.fill_required": "先に API キーとモデルを入力してください。", "st.captcha.desc_html": "CapSolver API を使って、エージェントに CAPTCHA を自動で解かせます。reCAPTCHA v2/v3、hCaptcha、Cloudflare Turnstile に対応しています。有効な API キーを保存すると CapSolver が自動的に有効になります。キーがない場合、エージェントは停止し、CAPTCHA を手動で解くよう求めます。CapSolver は解決ごとに課金します(約 ~$0.001–$0.003)。ご自身のアカウントと API キーを使用します。", "st.captcha.enabled.label": "CapSolver を有効化", "st.captcha.enabled.desc": "エージェントが CAPTCHA に遭遇すると、あなたに尋ねる前にまず CapSolver を 1 回呼び出します。下記の API キーが必要です。", diff --git a/src/firefox/src/ui/locales/ko.js b/src/firefox/src/ui/locales/ko.js index ec940ad2f..50da23829 100644 --- a/src/firefox/src/ui/locales/ko.js +++ b/src/firefox/src/ui/locales/ko.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "연결됨! 모델: {model}", "st.transcription.failed": "실패: {error}", "st.transcription.fill_required": "먼저 기본 URL과 모델을 입력하세요.", + "st.imagegen.heading": "생성 미디어 (fal.ai)", + "st.imagegen.desc": "generate_image 에이전트 도구가 fal.ai 큐 API를 통해 텍스트 프롬프트에서 이미지 등 미디어를 생성하는 데 사용됩니다. fal.ai/dashboard/keys에서 키를 발급받으세요. 채팅에는 사용되지 않습니다.", + "st.imagegen.saved": "저장되었습니다!", + "st.imagegen.cleared": "지워졌습니다.", + "st.imagegen.testing": "테스트 중...", + "st.imagegen.connected": "연결됨! 모델: {model}", + "st.imagegen.failed": "실패: {error}", + "st.imagegen.fill_required": "먼저 API 키와 모델을 입력하세요.", "st.captcha.desc_html": "에이전트가 CapSolver API를 통해 CAPTCHA를 자동으로 풀게 합니다. reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile을 지원합니다. 유효한 API 키를 저장하면 CapSolver가 자동으로 활성화됩니다. 키가 없으면 에이전트가 멈추고 CAPTCHA를 직접 풀어 달라고 요청합니다. CapSolver는 풀이당 요금을 부과하며(~$0.001–$0.003), 본인의 계정과 API 키를 사용합니다.", "st.captcha.enabled.label": "CapSolver 사용", "st.captcha.enabled.desc": "에이전트가 CAPTCHA를 만나면 사용자에게 묻기로 폴백하기 전에 CapSolver를 한 번 호출합니다. 아래에 API 키가 필요합니다.", diff --git a/src/firefox/src/ui/locales/ms.js b/src/firefox/src/ui/locales/ms.js index 3c555ec2d..6a66f7dac 100644 --- a/src/firefox/src/ui/locales/ms.js +++ b/src/firefox/src/ui/locales/ms.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "Tersambung! Model: {model}", "st.transcription.failed": "Gagal: {error}", "st.transcription.fill_required": "Isi URL Asas dan Model terlebih dahulu.", + "st.imagegen.heading": "Media generatif (fal.ai)", + "st.imagegen.desc": "Digunakan oleh alat ejen generate_image untuk mencipta imej dan media lain daripada geseran teks melalui API baris giliran fal.ai. Dapatkan kunci di fal.ai/dashboard/keys. Tidak digunakan untuk sembang.", + "st.imagegen.saved": "Disimpan!", + "st.imagegen.cleared": "Dibersihkan.", + "st.imagegen.testing": "Menguji...", + "st.imagegen.connected": "Berjaya disambung! Model: {model}", + "st.imagegen.failed": "Gagal: {error}", + "st.imagegen.fill_required": "Isi Kunci API dan Model dahulu.", "st.captcha.desc_html": "Biarkan ejen menyelesaikan CAPTCHA secara automatik melalui API CapSolver. Menyokong reCAPTCHA v2/v3, hCaptcha dan Cloudflare Turnstile. Menyimpan kunci API yang sah akan mengaktifkan CapSolver secara automatik; tanpa kunci, ejen berhenti dan meminta anda menyelesaikan CAPTCHA sendiri. CapSolver mengenakan bayaran bagi setiap penyelesaian (~$0.001–$0.003); anda menggunakan akaun dan kunci API sendiri.", "st.captcha.enabled.label": "Dayakan CapSolver", "st.captcha.enabled.desc": "Apabila ejen menemui CAPTCHA, ia akan memanggil CapSolver sekali sebelum berundur untuk bertanya kepada anda. Memerlukan kunci API di bawah.", diff --git a/src/firefox/src/ui/locales/nl.js b/src/firefox/src/ui/locales/nl.js index b3f065ced..1090924e7 100644 --- a/src/firefox/src/ui/locales/nl.js +++ b/src/firefox/src/ui/locales/nl.js @@ -785,6 +785,14 @@ export default { 'st.transcription.connected': 'Verbonden! Model: {model}', 'st.transcription.failed': 'Mislukt: {error}', 'st.transcription.fill_required': 'Vul eerst basis-URL en model in.', + "st.imagegen.heading": "Generatieve media (fal.ai)", + "st.imagegen.desc": "Wordt gebruikt door de agenttool generate_image om afbeeldingen en andere media te maken vanuit een tekstprompt via de queue-API van fal.ai. Vraag een sleutel aan op fal.ai/dashboard/keys. Niet voor chat.", + "st.imagegen.saved": "Opgeslagen!", + "st.imagegen.cleared": "Wissen gelukt.", + "st.imagegen.testing": "Testen...", + "st.imagegen.connected": "Verbonden! Model: {model}", + "st.imagegen.failed": "Mislukt: {error}", + "st.imagegen.fill_required": "Vul eerst de API-sleutel en het model in.", 'st.imageBudget.heading': 'Afbeeldingsbudget', 'st.imageBudget.desc': 'Beheer schermafbeeldingsgrootte en hoeveel de agent vastlegt voor visie per beurt...', 'st.imageBudget.detail.label': 'Afbeeldingsdetail', diff --git a/src/firefox/src/ui/locales/pl.js b/src/firefox/src/ui/locales/pl.js index 942aae174..b04fee860 100644 --- a/src/firefox/src/ui/locales/pl.js +++ b/src/firefox/src/ui/locales/pl.js @@ -620,6 +620,14 @@ export default { 'st.transcription.connected': 'Połączono! Model: {model}', 'st.transcription.failed': 'Niepowodzenie: {error}', 'st.transcription.fill_required': 'Najpierw wypełnij bazowy adres URL i model.', + "st.imagegen.heading": "Media generatywne (fal.ai)", + "st.imagegen.desc": "Używane przez narzędzie agenta generate_image do tworzenia obrazów i innych mediów z promptu tekstowego przez API kolejki fal.ai. Klucz uzyskasz na fal.ai/dashboard/keys. Nieużywane do czatu.", + "st.imagegen.saved": "Zapisano!", + "st.imagegen.cleared": "Wyczyszczono.", + "st.imagegen.testing": "Testowanie...", + "st.imagegen.connected": "Połączono! Model: {model}", + "st.imagegen.failed": "Niepowodzenie: {error}", + "st.imagegen.fill_required": "Najpierw wypełnij klucz API i model.", 'st.profile.desc_html': 'Przechowuj krótki opis, którego agent może użyć do wypełniania formularzy rejestracji bez pytania za każdym razem — Twoje imię, służbowy e-mail, firmę i jednorazowe hasło do mało istotnych rejestracji. Po włączeniu poniższy tekst jest dołączany do promptu systemowego agenta w każdej rozmowie.', 'st.profile.enabled.label': 'Włącz automatyczne wypełnianie profilu', 'st.profile.enabled.desc': 'Wstrzykuj tekst profilu do promptu systemowego agenta. Wyłączone = agent nigdy nie widzi tego tekstu.', diff --git a/src/firefox/src/ui/locales/pt.js b/src/firefox/src/ui/locales/pt.js index 9a6d50804..db62986bc 100644 --- a/src/firefox/src/ui/locales/pt.js +++ b/src/firefox/src/ui/locales/pt.js @@ -877,6 +877,14 @@ export default { 'st.transcription.connected': "Conectado! Modelo: {model}", 'st.transcription.failed': "Falha: {error}", 'st.transcription.fill_required': "Preencha primeiro o URL base e o modelo.", + "st.imagegen.heading": "Mídia generativa (fal.ai)", + "st.imagegen.desc": "Usada pela ferramenta de agente generate_image para criar imagens e outras mídias a partir de um prompt de texto pela API de fila da fal.ai. Obtenha uma chave em fal.ai/dashboard/keys. Não usada para chat.", + "st.imagegen.saved": "Salvo!", + "st.imagegen.cleared": "Limpo.", + "st.imagegen.testing": "Testando...", + "st.imagegen.connected": "Conectado! Modelo: {model}", + "st.imagegen.failed": "Falhou: {error}", + "st.imagegen.fill_required": "Preencha primeiro a chave de API e o modelo.", 'st.imageBudget.heading': "Orçamento de imagens", 'st.imageBudget.desc': "Controle o tamanho das capturas de tela e quantas o agente faz para visão por turno. Menos detalhes e dimensões menores reduzem custo e latência em endpoints menores; valores maiores preservam a fidelidade. Os padrões correspondem ao comportamento anterior.", diff --git a/src/firefox/src/ui/locales/ru.js b/src/firefox/src/ui/locales/ru.js index f77112b74..3256fa9ce 100644 --- a/src/firefox/src/ui/locales/ru.js +++ b/src/firefox/src/ui/locales/ru.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "Подключено! Модель: {model}", "st.transcription.failed": "Сбой: {error}", "st.transcription.fill_required": "Сначала заполните базовый URL и модель.", + "st.imagegen.heading": "Генеративные медиа (fal.ai)", + "st.imagegen.desc": "Используется инструментом агента generate_image для создания изображений и других медиа из текстового запроса через очередь API fal.ai. Ключ можно получить на fal.ai/dashboard/keys. Не используется для чата.", + "st.imagegen.saved": "Сохранено!", + "st.imagegen.cleared": "Очищено.", + "st.imagegen.testing": "Проверка...", + "st.imagegen.connected": "Подключено! Модель: {model}", + "st.imagegen.failed": "Ошибка: {error}", + "st.imagegen.fill_required": "Сначала заполните ключ API и модель.", "st.captcha.desc_html": "Позвольте агенту автоматически решать CAPTCHA через API CapSolver. Поддерживает reCAPTCHA v2/v3, hCaptcha и Cloudflare Turnstile. Сохранение действительного API-ключа автоматически включает CapSolver; без ключа агент останавливается и просит вас решить CAPTCHA самостоятельно. CapSolver берёт плату за каждое решение (~$0.001–$0.003); используется ваш аккаунт и API-ключ.", "st.captcha.enabled.label": "Включить CapSolver", "st.captcha.enabled.desc": "Когда агент сталкивается с CAPTCHA, он один раз обращается к CapSolver, прежде чем перейти к запросу к вам. Требуется API-ключ ниже.", diff --git a/src/firefox/src/ui/locales/th.js b/src/firefox/src/ui/locales/th.js index 8ae956728..9f3aa3384 100644 --- a/src/firefox/src/ui/locales/th.js +++ b/src/firefox/src/ui/locales/th.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "เชื่อมต่อแล้ว! โมเดล: {model}", "st.transcription.failed": "ล้มเหลว: {error}", "st.transcription.fill_required": "กรอก URL ฐานและโมเดลก่อน", + "st.imagegen.heading": "สื่อเชิงสร้างสรรค์ (fal.ai)", + "st.imagegen.desc": "เครื่องมือเอเจนต์ generate_image ใช้สร้างรูปภาพและสื่ออื่น ๆ จากพรอมต์ข้อความผ่านคิว API ของ fal.ai รับคีย์ได้ที่ fal.ai/dashboard/keys ไม่ได้ใช้สำหรับแชท", + "st.imagegen.saved": "บันทึกแล้ว!", + "st.imagegen.cleared": "ล้างแล้ว.", + "st.imagegen.testing": "กำลังทดสอบ...", + "st.imagegen.connected": "เชื่อมต่อแล้ว! โมเดล: {model}", + "st.imagegen.failed": "ล้มเหลว: {error}", + "st.imagegen.fill_required": "กรอกคีย์ API และโมเดลก่อน", "st.captcha.desc_html": "ให้เอเจนต์แก้ CAPTCHA โดยอัตโนมัติผ่าน API ของ CapSolver รองรับ reCAPTCHA v2/v3, hCaptcha และ Cloudflare Turnstile การบันทึกคีย์ API ที่ถูกต้องจะเปิดใช้ CapSolver โดยอัตโนมัติ หากไม่มีคีย์ เอเจนต์จะหยุดและขอให้คุณแก้ CAPTCHA เอง CapSolver คิดค่าบริการต่อการแก้หนึ่งครั้ง (~$0.001–$0.003) โดยใช้บัญชีและคีย์ API ของคุณเอง", "st.captcha.enabled.label": "เปิดใช้งาน CapSolver", "st.captcha.enabled.desc": "เมื่อเอเจนต์เจอ CAPTCHA มันจะเรียก CapSolver หนึ่งครั้งก่อนถอยไปถามคุณ ต้องมีคีย์ API ด้านล่าง", diff --git a/src/firefox/src/ui/locales/tl.js b/src/firefox/src/ui/locales/tl.js index 1a75a20f2..ae9ee6406 100644 --- a/src/firefox/src/ui/locales/tl.js +++ b/src/firefox/src/ui/locales/tl.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "Konektado! Modelo: {model}", "st.transcription.failed": "Nabigo: {error}", "st.transcription.fill_required": "Punan muna ang Base URL at Modelo.", + "st.imagegen.heading": "Generative media (fal.ai)", + "st.imagegen.desc": "Ginagamit ng generate_image agent tool para gumawa ng mga larawan at iba pang media mula sa text prompt sa pamamagitan ng fal.ai queue API. Kumuha ng key sa fal.ai/dashboard/keys. Hindi ginagamit sa chat.", + "st.imagegen.saved": "Na-save!", + "st.imagegen.cleared": "Na-clear.", + "st.imagegen.testing": "Sinusuri...", + "st.imagegen.connected": "Nakakonekta! Model: {model}", + "st.imagegen.failed": "Nabigo: {error}", + "st.imagegen.fill_required": "Punan muna ang API Key at Model.", "st.captcha.desc_html": "Hayaan ang ahente na awtomatikong lutasin ang mga CAPTCHA sa pamamagitan ng CapSolver API. Sinusuportahan ang reCAPTCHA v2/v3, hCaptcha, at Cloudflare Turnstile. Awtomatikong pinapagana ang CapSolver kapag nag-save ka ng wastong API key; kung walang key, hihinto ang ahente at hihilingin sa iyong lutasin mismo ang CAPTCHA. Naniningil ang CapSolver sa bawat solve (~$0.001–$0.003); ginagamit mo ang sarili mong account at API key.", "st.captcha.enabled.label": "I-enable ang CapSolver", "st.captcha.enabled.desc": "Kapag may naabot na CAPTCHA ang ahente, tatawag ito sa CapSolver nang isang beses bago bumalik sa pagtatanong sa iyo. Kailangan ng API key sa ibaba.", diff --git a/src/firefox/src/ui/locales/tr.js b/src/firefox/src/ui/locales/tr.js index 8c7739266..a31fa7584 100644 --- a/src/firefox/src/ui/locales/tr.js +++ b/src/firefox/src/ui/locales/tr.js @@ -633,6 +633,14 @@ export default { "st.transcription.connected": "Bağlandı! Model: {model}", "st.transcription.failed": "Başarısız: {error}", "st.transcription.fill_required": "Önce Temel URL ve Model alanlarını doldur.", + "st.imagegen.heading": "Üretken medya (fal.ai)", + "st.imagegen.desc": "generate_image ajan aracı tarafından metin isteminden fal.ai'nin kuyruk API'si üzerinden görsel ve diğer medyaları oluşturmak için kullanılır. Anahtar alın: fal.ai/dashboard/keys. Sohbet için kullanılmaz.", + "st.imagegen.saved": "Kaydedildi!", + "st.imagegen.cleared": "Temizlendi.", + "st.imagegen.testing": "Test ediliyor...", + "st.imagegen.connected": "Bağlandı! Model: {model}", + "st.imagegen.failed": "Başarısız: {error}", + "st.imagegen.fill_required": "Önce API anahtarını ve modeli doldurun.", "st.captcha.desc_html": "Aracının CAPTCHA'ları CapSolver API'si üzerinden otomatik çözmesine izin ver. reCAPTCHA v2/v3, hCaptcha ve Cloudflare Turnstile desteklenir. Geçerli bir API anahtarı kaydedildiğinde CapSolver otomatik olarak etkinleşir; anahtar yoksa aracı durur ve CAPTCHA'yı senin çözmeni ister. CapSolver her çözüm için ücret alır (~$0.001–$0.003); kendi hesabını ve API anahtarını kullanırsın.", "st.captcha.enabled.label": "CapSolver'ı etkinleştir", "st.captcha.enabled.desc": "Aracı bir CAPTCHA ile karşılaştığında, sana sormaya geri dönmeden önce bir kez CapSolver'ı çağırır. Aşağıda bir API anahtarı gerektirir.", diff --git a/src/firefox/src/ui/locales/uk.js b/src/firefox/src/ui/locales/uk.js index 1a66ff607..7aa423ebe 100644 --- a/src/firefox/src/ui/locales/uk.js +++ b/src/firefox/src/ui/locales/uk.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "Підключено! Модель: {model}", "st.transcription.failed": "Збій: {error}", "st.transcription.fill_required": "Спочатку заповніть базовий URL і модель.", + "st.imagegen.heading": "Генеративні медіа (fal.ai)", + "st.imagegen.desc": "Використовується інструментом агента generate_image для створення зображень та інших медіа з текстового запиту через API черги fal.ai. Ключ можна отримати на fal.ai/dashboard/keys. Не використовується для чату.", + "st.imagegen.saved": "Збережено!", + "st.imagegen.cleared": "Очищено.", + "st.imagegen.testing": "Перевірка...", + "st.imagegen.connected": "Підключено! Модель: {model}", + "st.imagegen.failed": "Помилка: {error}", + "st.imagegen.fill_required": "Спочатку заповніть ключ API і модель.", "st.captcha.desc_html": "Дозвольте агенту автоматично розв'язувати CAPTCHA через API CapSolver. Підтримує reCAPTCHA v2/v3, hCaptcha та Cloudflare Turnstile. Збереження дійсного API-ключа автоматично вмикає CapSolver; без ключа агент зупиняється й просить вас розв'язати CAPTCHA самостійно. CapSolver стягує плату за кожне розв'язання (~$0.001–$0.003); ви використовуєте власний акаунт і API-ключ.", "st.captcha.enabled.label": "Увімкнути CapSolver", "st.captcha.enabled.desc": "Коли агент натрапляє на CAPTCHA, він один раз викличе CapSolver, перш ніж повернутися до запиту до вас. Потрібен API-ключ нижче.", diff --git a/src/firefox/src/ui/locales/vi.js b/src/firefox/src/ui/locales/vi.js index 200a7b6cc..d946d1ad3 100644 --- a/src/firefox/src/ui/locales/vi.js +++ b/src/firefox/src/ui/locales/vi.js @@ -877,6 +877,14 @@ export default { 'st.transcription.connected': "Đã kết nối! Model: {model}", 'st.transcription.failed': "Không thành công: {error}", 'st.transcription.fill_required': "Trước tiên hãy điền URL cơ sở và Mô hình.", + "st.imagegen.heading": "Media tạo sinh (fal.ai)", + "st.imagegen.desc": "Được công cụ tác nhân generate_image sử dụng để tạo hình ảnh và phương tiện khác từ lời nhắc văn bản thông qua API hàng đợi của fal.ai. Lấy khóa tại fal.ai/dashboard/keys. Không dùng cho trò chuyện.", + "st.imagegen.saved": "Đã lưu!", + "st.imagegen.cleared": "Đã xóa.", + "st.imagegen.testing": "Đang kiểm tra...", + "st.imagegen.connected": "Đã kết nối! Mô hình: {model}", + "st.imagegen.failed": "Thất bại: {error}", + "st.imagegen.fill_required": "Điền Khóa API và Mô hình trước.", 'st.imageBudget.heading': "Ngân sách hình ảnh", 'st.imageBudget.desc': "Kiểm soát kích thước ảnh chụp màn hình và số ảnh tác nhân chụp cho thị giác trong mỗi lượt. Mức chi tiết và kích thước thấp hơn giúp giảm chi phí và độ trễ cho các endpoint nhỏ; mức cao hơn giữ độ trung thực. Giá trị mặc định khớp với hành vi trước đây.", diff --git a/src/firefox/src/ui/locales/zh.js b/src/firefox/src/ui/locales/zh.js index 990494942..cdc12bf54 100644 --- a/src/firefox/src/ui/locales/zh.js +++ b/src/firefox/src/ui/locales/zh.js @@ -593,6 +593,14 @@ export default { "st.transcription.connected": "连接成功!模型:{model}", "st.transcription.failed": "失败:{error}", "st.transcription.fill_required": "请先填写基础 URL 和模型。", + "st.imagegen.heading": "生成式媒体 (fal.ai)", + "st.imagegen.desc": "generate_image 代理工具通过 fal.ai 的队列 API 根据文本提示生成图片等媒体。请前往 fal.ai/dashboard/keys 获取密钥。不用于聊天。", + "st.imagegen.saved": "已保存!", + "st.imagegen.cleared": "已清除。", + "st.imagegen.testing": "测试中...", + "st.imagegen.connected": "已连接!模型:{model}", + "st.imagegen.failed": "失败:{error}", + "st.imagegen.fill_required": "请先填写 API 密钥和模型。", "st.captcha.desc_html": "让代理通过 CapSolver API 自动解决 CAPTCHA。支持 reCAPTCHA v2/v3、hCaptcha 和 Cloudflare Turnstile。保存有效的 API 密钥后会自动启用 CapSolver;没有密钥时,代理会停止并请你自行解决 CAPTCHA。CapSolver 按每次解决计费(约 ~$0.001–$0.003);使用你自己的账号和 API 密钥。", "st.captcha.enabled.label": "启用 CapSolver", "st.captcha.enabled.desc": "当代理遇到 CAPTCHA 时,会先调用一次 CapSolver,然后再回退到询问你。需要下方的 API 密钥。", diff --git a/src/firefox/src/ui/settings.html b/src/firefox/src/ui/settings.html index 5301982f4..052e14113 100644 --- a/src/firefox/src/ui/settings.html +++ b/src/firefox/src/ui/settings.html @@ -1771,6 +1771,32 @@

+

+ + +

+
+
+
+ + +
+
+ + +
+
+ + + +
+
+
+