Compare commits
3 Commits
109dbc7e16
...
642fa42561
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
642fa42561 | ||
|
|
f3d34bab4d | ||
|
|
c21c5b225f |
@@ -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;
|
||||
|
||||
@@ -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 []):
|
||||
|
||||
@@ -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, "
|
||||
|
||||
@@ -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 " (탭 없음)"
|
||||
|
||||
Reference in New Issue
Block a user