3 Commits

Author SHA1 Message Date
javis-bot
642fa42561 fix: drop webSearch when a site is named in screen-share, forcing controlBrowser
The 3B model kept choosing webSearch over controlBrowser even when offered, so
'네이버에서 X 검색' still used the invisible web path. When broadcasting and the
user explicitly names a site, remove webSearch from the allow-list so the
on-screen browser is the only search route.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:57:51 +09:00
javis-bot
f3d34bab4d fix: always offer controlBrowser in screen-share so on-screen search works
The small router reflexively routed every "search/open" intent to webSearch
and never surfaced controlBrowser, so "네이버에서 X 검색해줘" did nothing on the
broadcast. Union controlBrowser (+browseAndPlay) into the allow-list every
turn in screen-share mode (like setBroadcast), and steer the model in the
system prompt to prefer the on-screen browser over webSearch when available.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:55:33 +09:00
javis-bot
c21c5b225f feat: controlBrowser 'search' action + one-sentence voice replies
- Add a one-shot `search` action (site=naver/google/daum/youtube/bing) that
  navigates the on-screen browser straight to the results page, so a small
  model can satisfy "search X on Naver" in a single tool call instead of a
  fragile navigate->type->enter chain.
- Sharpen the tool description to steer the router to controlBrowser (not
  webSearch) for anything that should happen IN the visible browser.
- System prompt: answer in one short sentence (voice assistant) — also cuts
  TTS time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:52:47 +09:00
4 changed files with 71 additions and 8 deletions

View File

@@ -97,6 +97,32 @@ try {
break;
}
case 'search': {
// One-shot "search on a site": build the engine's results URL so a small
// model doesn't have to chain navigate->type->enter. Visible on screen.
const q = String(cmd.query || '').trim();
if (!q) throw new Error('search: no query');
const site = String(cmd.site || 'google').toLowerCase();
const engines = {
naver: 'https://search.naver.com/search.naver?query=',
google: 'https://www.google.com/search?q=',
daum: 'https://search.daum.net/search?q=',
youtube: 'https://www.youtube.com/results?search_query=',
bing: 'https://www.bing.com/search?q=',
};
const base = engines[site] || engines.google;
const target = base + encodeURIComponent(q);
await front(page);
if (HAS_XDOTOOL && cmd.human !== false) {
try { await human.navigateOmnibox(target); await page.waitForLoadState('domcontentloaded').catch(() => {}); }
catch { await page.goto(target, { waitUntil: 'domcontentloaded' }); }
} else {
await page.goto(target, { waitUntil: 'domcontentloaded' });
}
out({ ok: true, site: engines[site] ? site : 'google', query: q, url: page.url(), title: await page.title().catch(() => '') });
break;
}
case 'back': await front(page); await page.goBack({ waitUntil: 'domcontentloaded' }).catch(() => {}); out({ ok: true, url: page.url() }); break;
case 'forward': await front(page); await page.goForward({ waitUntil: 'domcontentloaded' }).catch(() => {}); out({ ok: true, url: page.url() }); break;
case 'refresh': await front(page); await reload(); out({ ok: true, url: page.url() }); break;

View File

@@ -1036,6 +1036,29 @@ def run_reply_engine(db: "Database", cfg, tts: Optional[Any],
and "setBroadcast" not in routed_tools:
routed_tools = routed_tools + ["setBroadcast"]
# In screen-share mode, always offer the on-screen browser control too. The
# small router reflexively picks webSearch for any "search/open/find" intent
# and never surfaces controlBrowser, so the model never gets the option to
# actually drive the visible browser (e.g. "네이버에서 X 검색해줘"). Offer it
# every turn; it self-gates (no-op when nothing is asked of the browser).
if getattr(cfg, "stream_browser", True):
for _bt in ("controlBrowser", "browseAndPlay"):
if _bt in _full_catalog_names and _bt not in routed_tools:
routed_tools = routed_tools + [_bt]
# When the user explicitly names a website (a proper noun, not a language
# pattern), the on-screen browser is unambiguously what they want — but
# the small router reflexively keeps webSearch and the model picks the
# invisible web path. Drop webSearch for that turn so controlBrowser
# wins. Only fires when a site is named AND we're in screen-share mode.
_site_tokens = (
"naver", "네이버", "google", "구글", "daum", "다음",
"youtube", "유튜브", "유투브", "bing",
)
if "controlBrowser" in routed_tools and "webSearch" in routed_tools \
and any(_tok in redacted.lower() for _tok in _site_tokens):
routed_tools = [t for t in routed_tools if t != "webSearch"]
debug_log("screen-share: site named — dropping webSearch so controlBrowser wins", "tools")
_planner_schema = generate_tools_json_schema(routed_tools, mcp_tools)
_planner_tool_catalog: list[tuple[str, str]] = []
for _schema in (_planner_schema or []):

View File

@@ -36,6 +36,12 @@ _SYSTEM_PROMPT_TEMPLATE: str = (
"butler clichés, and never address the user as 'sir', 'madam', 'my liege', or similar. "
"Never stack multiple jokes in one reply. "
"Be concise, conversational, and actionable. "
"This is a spoken voice assistant: answer in ONE short sentence whenever possible "
"(two at the very most). No lists, no preamble, no 'is there anything else' offers. "
"When a controlBrowser tool is available, use IT (never webSearch) for anything that "
"should happen in the on-screen browser — opening a site, searching on a site "
"(controlBrowser action 'search' with the right site), clicking, typing — because only "
"controlBrowser is visible on the broadcast; webSearch shows nothing on screen. "
"Never claim you performed an action — opening a website, navigating the browser, "
"playing or showing something on screen, changing a setting, sending a message — unless a "
"tool actually did it this turn and reported success. If you have no tool for what was asked, "

View File

@@ -41,12 +41,14 @@ class ControlBrowserTool(Tool):
@property
def description(self) -> str:
return (
"Operate the on-screen Chrome shown on the broadcast like a person: open a "
"website/URL, list/open/close/switch tabs, close popups, click an element, "
"type text, scroll, or screenshot. Use whenever the user asks to go to a site "
"(e.g. 'open Naver'), check what's on screen, or interact with the page. "
"Only available in screen-share mode. Do NOT claim you did any of this unless "
"this tool returns success."
"Drive the on-screen Chrome that is shown on the broadcast, like a person would. "
"Use this (NOT webSearch) whenever the user wants something done or shown IN the "
"browser on screen: open a website or URL, search on a specific site (action "
"'search' with site=naver/google/daum/youtube/bing), go back/forward, refresh, "
"manage tabs (list/new/close/switch), close popups, click, type, scroll, or "
"screenshot. webSearch only returns text and shows nothing on screen; this tool "
"actually navigates the visible browser. Only available in screen-share mode. "
"Never claim you did any of this unless this tool returns success."
)
@property
@@ -57,14 +59,16 @@ class ControlBrowserTool(Tool):
"action": {
"type": "string",
"enum": [
"status", "listTabs", "navigate", "back", "forward",
"refresh", "newTab", "closeTab", "activateTab",
"status", "listTabs", "navigate", "search", "back",
"forward", "refresh", "newTab", "closeTab", "activateTab",
"closePopups", "click", "type", "scroll", "pressKey",
"screenshot",
],
"description": "What to do in the browser.",
},
"url": {"type": "string", "description": "Target URL/site for navigate/newTab (e.g. 'naver.com')."},
"query": {"type": "string", "description": "Search text for action 'search'."},
"site": {"type": "string", "description": "Search site for action 'search': naver, google, daum, youtube, bing."},
"index": {"type": "integer", "description": "Tab index for closeTab/activateTab (from listTabs)."},
"selector": {"type": "string", "description": "CSS selector for click/type."},
"text": {"type": "string", "description": "Text to type."},
@@ -129,6 +133,10 @@ class ControlBrowserTool(Tool):
def _summarise(action: str, args: Dict[str, Any], data: Dict[str, Any]) -> str:
if action == "navigate":
return f"브라우저에서 {data.get('url', args.get('url'))} 로 이동했습니다."
if action == "search":
return f"{data.get('site', '')}에서 '{data.get('query', args.get('query'))}'를 검색해 화면에 띄웠습니다."
if action in ("back", "forward", "refresh"):
return f"브라우저: {action} 완료 ({data.get('url', '')})."
if action in ("status", "listTabs"):
tabs = data.get("tabs", [])
lines = "\n".join(f" [{t['index']}] {t.get('title') or t['url']}" for t in tabs) or " (탭 없음)"