diff --git a/config.yaml.example b/config.yaml.example index ea4bef2..217bdf2 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -224,8 +224,11 @@ subtasks: # browser: # page_timeout: 60000 # ms # action_timeout: 30000 # ms -# captcha_solve: novnc # 'skip' (default) / 'novnc' -# max_captcha_pages: 5 +# display_mode: novnc # 'headless' (default) / 'novnc' +# # novnc = Browser タブのライブ表示 / InteractiveBrowse / +# # CAPTCHA 解決を有効化 (ホストに Xvfb/x11vnc/websockify 必須)。 +# # 旧 'captcha_solve' は自動で display_mode に移行される (非推奨)。 +# max_captcha_pages: 5 # CAPTCHA Pool の同時ページ上限 (display_mode: novnc 時のみ) # channel: chrome # 'chromium' (default) / 'chrome' / 'msedge' # executable_path: /usr/bin/google-chrome # channel と排他 diff --git a/docs/tools/browseweb.md b/docs/tools/browseweb.md index 0334782..614a1c4 100644 --- a/docs/tools/browseweb.md +++ b/docs/tools/browseweb.md @@ -242,7 +242,7 @@ BrowseWithSession({ ### 制約 - `InteractiveBrowse` は **ローカルタスク経由のジョブ** でのみ使える(`taskId` が必要)。Gitea Issue 直接実行や taskId が立たない subtask root では使えない -- noVNC が orchestrator にインストール / 設定されていない環境ではエラー(Xvfb / x11vnc / websockify が必要、`config.yaml` の `browser.captcha_solve: novnc` 設定) +- noVNC が orchestrator にインストール / 設定されていない環境ではエラー(Xvfb / x11vnc / websockify が必要、`config.yaml` の `browser.display_mode: novnc` 設定。旧 `captcha_solve` は自動移行) - ユーザーが release を押さない限りジョブは進まない。長時間放置すると `browser.auth_timeout`(デフォルト 10 分)で timeout ### 既存の Browser Sessions 機能との違い diff --git a/scripts/setup-novnc.sh b/scripts/setup-novnc.sh index 3960ca2..22fd554 100755 --- a/scripts/setup-novnc.sh +++ b/scripts/setup-novnc.sh @@ -64,24 +64,25 @@ else fi fi -# 5. config.yaml に captcha_solve 設定を追加(まだなければ) +# 5. config.yaml に display_mode 設定を追加(まだなければ) echo "" echo "[5/5] config.yaml 確認..." CONFIG="$(cd "$(dirname "$0")/.." && pwd)/config.yaml" if [ -f "$CONFIG" ]; then - if grep -q "captcha_solve" "$CONFIG"; then - echo " ✓ captcha_solve 設定は既に存在します" + if grep -qE "display_mode|captcha_solve" "$CONFIG"; then + # 旧 captcha_solve があっても normalizeConfig が display_mode に自動移行するのでそのまま + echo " ✓ browser display_mode (または旧 captcha_solve) 設定は既に存在します" else echo " → browser セクションを追加中..." # browser セクションが存在するか確認 if grep -q "^browser:" "$CONFIG"; then # 既存 browser セクションに追加 - sed -i '/^browser:/a\ captcha_solve: novnc\n max_captcha_pages: 5' "$CONFIG" + sed -i '/^browser:/a\ display_mode: novnc\n max_captcha_pages: 5' "$CONFIG" else # browser セクションを新規追加 - printf '\nbrowser:\n captcha_solve: novnc\n max_captcha_pages: 5\n' >> "$CONFIG" + printf '\nbrowser:\n display_mode: novnc\n max_captcha_pages: 5\n' >> "$CONFIG" fi - echo " ✓ captcha_solve: novnc を追加しました" + echo " ✓ display_mode: novnc を追加しました" fi else echo " ⚠ config.yaml が見つかりません。config.yaml.example からコピーして設定してください" diff --git a/src/bridge/browser-api.test.ts b/src/bridge/browser-api.test.ts index b7f7b2e..501520a 100644 --- a/src/bridge/browser-api.test.ts +++ b/src/bridge/browser-api.test.ts @@ -3,7 +3,7 @@ import express from 'express'; import request from 'supertest'; import { Repository } from '../db/repository.js'; import { runMigrations } from '../db/migrate.js'; -import { createBrowserApi } from './browser-api.js'; +import { createBrowserApi, unavailableReason } from './browser-api.js'; import { type SessionManager, type BrowserSession, @@ -23,6 +23,17 @@ vi.mock('./novnc-proxy.js', async () => { }); const novncProxyMock = await import('./novnc-proxy.js'); +// browser.displayMode を per-test で切り替えられるようにする。 +// createBrowserApi の各ハンドラは loadConfig().browser?.displayMode を読む。 +let mockDisplayMode: 'headless' | 'novnc' | undefined = 'novnc'; +vi.mock('../config.js', async () => { + const actual = await vi.importActual('../config.js'); + return { + ...actual, + loadConfig: vi.fn(() => ({ browser: { displayMode: mockDisplayMode } })), + }; +}); + /** * 2026-05 redesign: API は CAPTCHA Pool (admin only) と Task Session * (visibility ベース) を分離。テストダブルも kind / taskId / captchaPending @@ -115,6 +126,7 @@ describe('Browser API', () => { const dbPath = './_test_browser_api.db'; beforeEach(() => { + mockDisplayMode = 'novnc'; // 既存テストは session が見える前提なので novnc 既定 repo = new Repository(dbPath); runMigrations(repo.getDb()); }); @@ -130,7 +142,7 @@ describe('Browser API', () => { const app = makeApp(sm, repo, { id: 'admin-1', role: 'admin' }); const res = await request(app).get('/api/local/browser/sessions/captcha-pool'); expect(res.status).toBe(200); - expect(res.body).toEqual({ available: false }); + expect(res.body).toEqual({ available: false, reason: 'no_session' }); }); it('returns pool info to admin', async () => { @@ -180,7 +192,7 @@ describe('Browser API', () => { const app = makeApp(sm, repo, { id: 'alice', role: 'user' }); const res = await request(app).get(`/api/local/browser/sessions/task-session/${taskId}`); expect(res.status).toBe(200); - expect(res.body).toEqual({ available: false }); + expect(res.body).toEqual({ available: false, reason: 'no_session' }); }); it('owner can see their own task session', async () => { @@ -201,7 +213,7 @@ describe('Browser API', () => { const app = makeApp(sm, repo, { id: 'bob', role: 'user' }); const res = await request(app).get(`/api/local/browser/sessions/task-session/${taskId}`); expect(res.status).toBe(200); - expect(res.body).toEqual({ available: false }); + expect(res.body).toEqual({ available: false, reason: 'no_session' }); }); it('admin can see any task session', async () => { @@ -240,6 +252,108 @@ describe('Browser API', () => { }); }); + describe('unavailableReason helper', () => { + it('returns headless_mode when displayMode is not novnc', () => { + expect(unavailableReason('headless', true)).toBe('headless_mode'); + expect(unavailableReason('headless', false)).toBe('headless_mode'); + expect(unavailableReason(undefined, true)).toBe('headless_mode'); // default headless + }); + + it('returns display_unavailable when novnc but sessionManager absent', () => { + expect(unavailableReason('novnc', false)).toBe('display_unavailable'); + }); + + it('returns no_session when novnc and sessionManager present', () => { + expect(unavailableReason('novnc', true)).toBe('no_session'); + }); + }); + + describe('reason codes on available:false responses', () => { + it('headless mode → task-session returns reason:headless_mode (even with sessionManager)', async () => { + mockDisplayMode = 'headless'; + const sm = new FakeSessionManager(); + const taskId = await createTask(repo, 'alice'); + const app = makeApp(sm, repo, { id: 'alice', role: 'user' }); + const res = await request(app).get(`/api/local/browser/sessions/task-session/${taskId}`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ available: false, reason: 'headless_mode' }); + }); + + it('headless mode → captcha-pool returns reason:headless_mode', async () => { + mockDisplayMode = 'headless'; + const sm = new FakeSessionManager(); + const app = makeApp(sm, repo, { id: 'admin-1', role: 'admin' }); + const res = await request(app).get('/api/local/browser/sessions/captcha-pool'); + expect(res.status).toBe(200); + expect(res.body).toEqual({ available: false, reason: 'headless_mode' }); + }); + + it('novnc + sessionManager null → task-session returns reason:display_unavailable', async () => { + mockDisplayMode = 'novnc'; + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + (req as unknown as { user: unknown }).user = { + id: 'alice', role: 'user', status: 'active', orgIds: [], + email: 'a@example.com', name: 'alice', avatarUrl: null, + defaultVisibility: 'private', defaultVisibilityOrgId: null, + }; + next(); + }); + app.use('/api/local/browser/sessions', createBrowserApi(null, repo)); + const taskId = await createTask(repo, 'alice'); + const res = await request(app).get(`/api/local/browser/sessions/task-session/${taskId}`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ available: false, reason: 'display_unavailable' }); + }); + + it('novnc + sessionManager null → captcha-pool returns reason:display_unavailable', async () => { + mockDisplayMode = 'novnc'; + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + (req as unknown as { user: unknown }).user = { + id: 'admin-1', role: 'admin', status: 'active', orgIds: [], + email: 'a@example.com', name: 'admin', avatarUrl: null, + defaultVisibility: 'private', defaultVisibilityOrgId: null, + }; + next(); + }); + app.use('/api/local/browser/sessions', createBrowserApi(null, repo)); + const res = await request(app).get('/api/local/browser/sessions/captcha-pool'); + expect(res.status).toBe(200); + expect(res.body).toEqual({ available: false, reason: 'display_unavailable' }); + }); + + it('headless + sessionManager null → reason:headless_mode', async () => { + mockDisplayMode = 'headless'; + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + (req as unknown as { user: unknown }).user = { + id: 'admin-1', role: 'admin', status: 'active', orgIds: [], + email: 'a@example.com', name: 'admin', avatarUrl: null, + defaultVisibility: 'private', defaultVisibilityOrgId: null, + }; + next(); + }); + app.use('/api/local/browser/sessions', createBrowserApi(null, repo)); + const res = await request(app).get('/api/local/browser/sessions/captcha-pool'); + expect(res.status).toBe(200); + expect(res.body).toEqual({ available: false, reason: 'headless_mode' }); + }); + + it('novnc + sessionManager present + no session → reason:no_session', async () => { + mockDisplayMode = 'novnc'; + const sm = new FakeSessionManager(); + const taskId = await createTask(repo, 'alice'); + const app = makeApp(sm, repo, { id: 'alice', role: 'user' }); + const res = await request(app).get(`/api/local/browser/sessions/task-session/${taskId}`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ available: false, reason: 'no_session' }); + }); + }); + describe('POST /task-session/:taskId/release', () => { it('owner can release their own task session', async () => { const sm = new FakeSessionManager(); diff --git a/src/bridge/browser-api.ts b/src/bridge/browser-api.ts index d9c4830..28f9cdd 100644 --- a/src/bridge/browser-api.ts +++ b/src/bridge/browser-api.ts @@ -4,6 +4,25 @@ import type { Repository } from '../db/repository.js'; import { logger } from '../logger.js'; import { buildNovncPath, isNovncStaticInstalled } from './novnc-proxy.js'; import { canUserSeeTask, canEditEntity } from './visibility.js'; +import { loadConfig } from '../config.js'; + +/** + * `available: false` なレスポンスに添える reason コードを決める純関数。 + * 優先順位: + * 1. displayMode !== 'novnc' → 'headless_mode' (config で機能 OFF) + * 2. SessionManager が null → 'display_unavailable' (noVNC スタック未導入) + * 3. それ以外 → 'no_session' (まだ session が無いだけ) + * + * どのバイナリ (Xvfb/x11vnc/websockify) が欠けているかは漏らさない。 + */ +export function unavailableReason( + displayMode: string | undefined, + sessionManagerPresent: boolean, +): 'headless_mode' | 'display_unavailable' | 'no_session' { + if ((displayMode ?? 'headless') !== 'novnc') return 'headless_mode'; + if (!sessionManagerPresent) return 'display_unavailable'; + return 'no_session'; +} /** * 2026-05 redesign: CAPTCHA Pool (admin 専用) と Task Session (タスク @@ -102,10 +121,12 @@ export function createBrowserApi(sessionManager: SessionManager | null, repo: Re // は available: false を返したい。503 にしてしまうと UI 側でエラー扱いされてしまう。 if (!sessionManager) { router.get('/captcha-pool', (_req: Request, res: Response) => { - res.json({ available: false }); + const reason = unavailableReason(loadConfig().browser?.displayMode, false); + res.json({ available: false, reason }); }); router.get('/task-session/:taskId', (_req: Request, res: Response) => { - res.json({ available: false }); + const reason = unavailableReason(loadConfig().browser?.displayMode, false); + res.json({ available: false, reason }); }); router.all('*', (_req: Request, res: Response) => { res.status(503).json({ error: 'Browser sessions not available (missing system dependencies)' }); @@ -122,7 +143,8 @@ export function createBrowserApi(sessionManager: SessionManager | null, repo: Re } const pool = sessionManager.getSession(CAPTCHA_POOL_SESSION_ID); if (!pool) { - res.json({ available: false }); + const reason = unavailableReason(loadConfig().browser?.displayMode, true); + res.json({ available: false, reason }); return; } if (!isNovncStaticInstalled()) { @@ -162,12 +184,15 @@ export function createBrowserApi(sessionManager: SessionManager | null, repo: Re .listSessions() .find((s) => s.kind === 'task' && s.taskId === taskId); if (!session) { - res.json({ available: false }); + const reason = unavailableReason(loadConfig().browser?.displayMode, true); + res.json({ available: false, reason }); return; } if (!(await canViewSession(req, session, repo))) { - // 認可失敗は available: false にして session 存在情報を漏らさない - res.json({ available: false }); + // 認可失敗は available: false にして session 存在情報を漏らさない。 + // reason も session が無いとき相当 (存在を漏らさない) で返す。 + const reason = unavailableReason(loadConfig().browser?.displayMode, true); + res.json({ available: false, reason }); return; } if (!isNovncStaticInstalled()) { diff --git a/src/config-normalize.test.ts b/src/config-normalize.test.ts index eb0c243..0f10665 100644 --- a/src/config-normalize.test.ts +++ b/src/config-normalize.test.ts @@ -545,3 +545,30 @@ describe('normalizeConfig — backwards compat with loadConfig', () => { expect(out.storage?.worktreeDir).toBe('/home/op/explicit-v1-value'); }); }); + +describe('browser.display_mode migration (captcha_solve rename)', () => { + it('legacy captchaSolve=novnc → displayMode=novnc, captchaSolve removed', () => { + const out = normalizeConfig({ browser: { captchaSolve: 'novnc' } }); + expect(out.browser?.displayMode).toBe('novnc'); + expect((out.browser as Record)?.captchaSolve).toBeUndefined(); + }); + + it('legacy captchaSolve=skip → displayMode=headless, captchaSolve removed', () => { + const out = normalizeConfig({ browser: { captchaSolve: 'skip' } }); + expect(out.browser?.displayMode).toBe('headless'); + expect((out.browser as Record)?.captchaSolve).toBeUndefined(); + }); + + it('displayMode wins over captchaSolve when both present; captchaSolve removed', () => { + const out = normalizeConfig({ browser: { displayMode: 'novnc', captchaSolve: 'skip' } }); + expect(out.browser?.displayMode).toBe('novnc'); + expect((out.browser as Record)?.captchaSolve).toBeUndefined(); + }); + + it('neither key → browser block unchanged (displayMode stays undefined)', () => { + const out = normalizeConfig({ browser: { maxSessions: 3 } }); + expect(out.browser?.displayMode).toBeUndefined(); + expect((out.browser as Record)?.captchaSolve).toBeUndefined(); + expect(out.browser?.maxSessions).toBe(3); + }); +}); diff --git a/src/config-normalize.ts b/src/config-normalize.ts index ce6a76a..5ec66e0 100644 --- a/src/config-normalize.ts +++ b/src/config-normalize.ts @@ -103,10 +103,54 @@ export function normalizeConfig(raw: unknown): AppConfig { backfillV2Blocks(out); } + // Browser display-mode migration runs for both v1 and v2 inputs: the + // legacy `browser.captcha_solve` key can appear in either shape. + migrateBrowserDisplayMode(out); + out.configVersion = 2; return out as unknown as AppConfig; } +/** + * Backward-compat for the `browser.captcha_solve` → `browser.display_mode` + * rename (2026-06). `captcha_solve` (values skip|novnc) was a misnamed master + * switch for the whole headed-browser + noVNC subsystem, not just CAPTCHA. + * + * Operates in camelCase (transformKeys already ran): + * - `displayMode` set → wins. If `captchaSolve` also present, drop it + warn. + * - `captchaSolve` set (no displayMode) → derive displayMode, drop captchaSolve, warn. + * - neither → leave as-is (default 'headless' applied at read sites). + * + * The legacy key is deleted every load so it never round-trips into a save. + */ +function migrateBrowserDisplayMode(out: Record): void { + if (out.browser === null || typeof out.browser !== 'object' || Array.isArray(out.browser)) { + return; + } + const browser = out.browser as Record; + const hasDisplayMode = browser.displayMode !== undefined; + const hasCaptchaSolve = browser.captchaSolve !== undefined; + + if (hasDisplayMode) { + if (hasCaptchaSolve) { + delete browser.captchaSolve; + logger.warn( + '[config] browser.captcha_solve is ignored because browser.display_mode is set; remove the deprecated captcha_solve key', + ); + } + return; + } + + if (hasCaptchaSolve) { + const value = browser.captchaSolve === 'novnc' ? 'novnc' : 'headless'; + browser.displayMode = value; + delete browser.captchaSolve; + logger.warn( + `[config] browser.captcha_solve is deprecated; migrated to browser.display_mode='${value}'. Update config.yaml (or run scripts/migrate-config.sh).`, + ); + } +} + function resolveConfigVersion(raw: unknown): 1 | 2 { if (raw === undefined || raw === null) return 1; if (typeof raw !== 'number' || !Number.isInteger(raw)) { diff --git a/src/config.ts b/src/config.ts index 049f32f..1ae8dfe 100644 --- a/src/config.ts +++ b/src/config.ts @@ -275,6 +275,12 @@ export interface BrowserConfig { vncBasePort?: number; // default 5900 sessionDataDir?: string; // default './data/browser-sessions' maxSessions?: number; // default 5 (CAPTCHA Pool は別枠でカウントしない) + /** + * Master switch for the headed-browser + noVNC subsystem (Browser tab live + * view, InteractiveBrowse, CAPTCHA pool). default 'headless'. + */ + displayMode?: 'headless' | 'novnc'; + /** @deprecated use displayMode */ captchaSolve?: 'skip' | 'novnc'; // default 'skip' maxCaptchaPages?: number; // default 5 /** Task Session が job 完了から何秒アイドルしたら GC するか (default 300) */ diff --git a/src/engine/tools/browser.ts b/src/engine/tools/browser.ts index 78a676e..d771055 100644 --- a/src/engine/tools/browser.ts +++ b/src/engine/tools/browser.ts @@ -256,7 +256,7 @@ export { getSessionManager }; // 2. Task Session (kind='task'): タスクごとに分離された noVNC session。 // BrowseWeb / InteractiveBrowse が ctx.taskId をキーに取得・再利用する。 // タスク visibility に基づき認可される。LRU 退避 + idle GC 対象。 -// 3. Headless 共有 (skip mode): config の captchaSolve != 'novnc' の場合、 +// 3. Headless 共有 (headless mode): config の displayMode != 'novnc' の場合、 // または noVNC が立ち上げられない fallback 経路で使う single Browser。 let _headlessBrowser: Browser | null = null; @@ -298,7 +298,7 @@ async function getHeadlessBrowser(): Promise { */ export async function getCaptchaPoolBrowser(): Promise { const config = loadConfig(); - if (config.browser?.captchaSolve === 'novnc') { + if (config.browser?.displayMode === 'novnc') { const sm = getSessionManager(); if (sm) { try { @@ -325,7 +325,7 @@ export async function getCaptchaPoolBrowser(): Promise { */ export async function getTaskSessionBrowser(ctx: ToolContext): Promise { const config = loadConfig(); - if (config.browser?.captchaSolve === 'novnc' && ctx.taskId) { + if (config.browser?.displayMode === 'novnc' && ctx.taskId) { const sm = getSessionManager(); if (sm) { try { @@ -557,7 +557,7 @@ async function getJobContext( ctx: ToolContext, ): Promise { const config = loadConfig(); - if (config.browser?.captchaSolve === 'novnc' && ctx.taskId) { + if (config.browser?.displayMode === 'novnc' && ctx.taskId) { const sm = getSessionManager(); if (sm) { try { diff --git a/src/engine/tools/web.ts b/src/engine/tools/web.ts index e0f7c6a..006d12e 100644 --- a/src/engine/tools/web.ts +++ b/src/engine/tools/web.ts @@ -532,13 +532,12 @@ async function searchViaBrowser( query: string, limit: number, pageTimeout: number, - captchaSolve: 'skip' | 'novnc' = 'skip', + useNovnc: boolean = false, maxCaptchaPages: number = 5, ): Promise<{ results: SearchResult[]; captcha: boolean }> { const url = engine.buildUrl(query, limit); logger.debug(`[WebSearch] ${engine.name} browser search: url=${url}`); - const useNovnc = captchaSolve === 'novnc'; // WebSearch は CAPTCHA Pool の Browser を共有して使う。これにより admin が // 一度 CAPTCHA を解けば Cookie が persistentContexts に残り、別タスクの // WebSearch も同じ Cookie で続行できる (タスク隔離が必要な BrowseWeb と @@ -640,7 +639,7 @@ async function executeWebSearch( const { loadConfig } = await import('../../config.js'); const appConfig = loadConfig(); - const captchaSolve = appConfig.browser?.captchaSolve ?? 'skip'; + const useNovnc = (appConfig.browser?.displayMode ?? 'headless') === 'novnc'; const maxCaptchaPages = appConfig.browser?.maxCaptchaPages ?? 5; // --- ブラウザ検索: Google → Brave → Yahoo の順に試行 --- @@ -652,7 +651,7 @@ async function executeWebSearch( const methodName = engine.name.toLowerCase(); try { - const { results, captcha } = await searchViaBrowser(engine, query, limit, pageTimeout, captchaSolve, maxCaptchaPages); + const { results, captcha } = await searchViaBrowser(engine, query, limit, pageTimeout, useNovnc, maxCaptchaPages); if (captcha) { lastBrowserError = `${engine.name} が CAPTCHA を要求しました`; @@ -673,7 +672,7 @@ async function executeWebSearch( resultCount: results.length, outcome: 'success', fallback: isFallback, }); // 検索が通った = Pool は CAPTCHA を抜けている。フラグを下ろす - if (captchaSolve === 'novnc') markPoolCaptchaPending(false); + if (useNovnc) markPoolCaptchaPending(false); return { output: formatted, isError: false }; } diff --git a/ui/src/components/detail/tabs/BrowserTab.tsx b/ui/src/components/detail/tabs/BrowserTab.tsx index 74142ba..0aeb215 100644 --- a/ui/src/components/detail/tabs/BrowserTab.tsx +++ b/ui/src/components/detail/tabs/BrowserTab.tsx @@ -6,7 +6,7 @@ import { SaveRecordingButton } from '../../browser/SaveRecordingButton.js'; interface TaskSessionInfo { available: boolean; - reason?: 'novnc_not_installed'; + reason?: 'novnc_not_installed' | 'headless_mode' | 'display_unavailable' | 'no_session'; sessionId?: string; novncPath?: string; display?: string; @@ -93,12 +93,31 @@ export function BrowserTab({ taskId }: { taskId: number }) { ); } + if (data?.reason === 'headless_mode') { + return ( +
+

headless モードでは Browser のライブ表示は使えません

+

+ Settings → Tools → Browser Runtime で Browser Session Mode を novnc に(Xvfb/x11vnc/websockify 必須)。 +

+
+ ); + } + if (data?.reason === 'display_unavailable') { + return ( +
+

ライブ表示スタックがホストに未導入です

+

+ 管理者に Xvfb/x11vnc/websockify の導入を依頼してください。 +

+
+ ); + } return (
-

このタスクのブラウザセッションは現在アクティブではありません

+

まだセッションがありません

- BrowseWeb / InteractiveBrowse を含むジョブが実行中のときに、このタブから - noVNC でブラウザを操作できます (5 秒ポーリング中)。 + このタスクで BrowseWeb を実行するとここに表示されます (5 秒ポーリング中)。

); diff --git a/ui/src/components/settings/BrowserSettingsForm.tsx b/ui/src/components/settings/BrowserSettingsForm.tsx index 11b3721..5723865 100644 --- a/ui/src/components/settings/BrowserSettingsForm.tsx +++ b/ui/src/components/settings/BrowserSettingsForm.tsx @@ -49,17 +49,18 @@ export function BrowserSettingsForm({ config, onChange }: SectionFormProps) {

Sessions (CDP)

- CAPTCHA Solve Mode - onChange('browser.displayMode', e.target.value)} className="w-full h-9 px-2 text-[13px] border border-hairline rounded-md"> - - + + - novnc にすると WebSearch 等が CAPTCHA を踏んだとき共有 noVNC セッション(CAPTCHA Pool)を立て、 - CAPTCHA タブで手動解決できる。ホストに Xvfb / x11vnc / websockify が必要。 - 未インストールなら自動的に headless にフォールバックする。skip(既定)では CAPTCHA は手動解決されない。 + ライブ表示機能のマスタースイッチ。novnc にすると + Browser タブのライブ表示・InteractiveBrowse・CAPTCHA 解決が有効になる。 + ホストに Xvfb / x11vnc / websockify が必要(未導入なら自動的に headless にフォールバック)。 + headless(既定)ではそれらのライブ表示機能は使えない
diff --git a/ui/src/pages/AdminCaptchaPage.tsx b/ui/src/pages/AdminCaptchaPage.tsx index d7c775f..a9c9692 100644 --- a/ui/src/pages/AdminCaptchaPage.tsx +++ b/ui/src/pages/AdminCaptchaPage.tsx @@ -5,6 +5,7 @@ import { PipButton } from '../components/browser/PipButton.js'; interface CaptchaPoolInfo { available: boolean; + reason?: 'headless_mode' | 'display_unavailable' | 'no_session'; sessionId?: string; novncPath?: string; display?: string; @@ -144,10 +145,21 @@ export function AdminCaptchaPage({ isAdmin }: { isAdmin: boolean }) {

Pool は現在起動していません

-

- WebSearch が CAPTCHA を踏むか、admin が手動でセッションを起動した時点で - noVNC が表示されます (5 秒ポーリング中)。 -

+ {data?.reason === 'headless_mode' ? ( +

+ headless モードでは CAPTCHA Pool は使えません。Settings → Tools → Browser Runtime で + Browser Session Mode を novnc に切り替えてください(ホストに Xvfb/x11vnc/websockify が必要)。 +

+ ) : data?.reason === 'display_unavailable' ? ( +

+ ライブ表示スタック(Xvfb/x11vnc/websockify)がホストに導入されていません。管理者に導入を依頼してください。 +

+ ) : ( +

+ WebSearch が CAPTCHA を踏むか、admin が手動でセッションを起動した時点で + noVNC が表示されます (5 秒ポーリング中)。 +

+ )}
)}