fix(docker+brain): make container Chrome search work, harden, improve toggle
Addresses review findings on the dockerized stack: - Container Chrome search was dead: add --remote-debugging-port + a non-default --user-data-dir (Chrome 136+ refuses CDP on the default profile), add the playwright dep (browse-search.mjs connectOverCDP) with browser download skipped, and connect to 127.0.0.1 not "localhost" (container localhost -> ::1 while Chrome binds IPv4). Verified: browse-search returns real results. - Broadcast toggle reliability: always offer setBroadcast in screen-share mode (the embedding/keyword router dropped it for non-English utterances) and make its description force a tool call. "방송 꺼줘"->stop now 5/5; no false triggers. - Stop the broadcast on voice leave (no orphaned stream). - Security: bind VNC/noVNC to loopback by default (VNC_BIND override) and the bridge to the container loopback (BRIDGE_HOST=127.0.0.1), not published. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ FROM ubuntu:24.04
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
LANG=C.UTF-8 \
|
||||
DISPLAY=:1 \
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
|
||||
PATH=/opt/venv/bin:/root/.bun/bin:/usr/local/bin:/usr/bin:/bin \
|
||||
NVIDIA_VISIBLE_DEVICES=all \
|
||||
NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"libsodium-wrappers": "^0.7.15",
|
||||
"opusscript": "^0.1.1",
|
||||
"playwright": "^1.60.0",
|
||||
"prism-media": "^1.3.5",
|
||||
"qrcode": "^1.5.4",
|
||||
},
|
||||
@@ -318,6 +319,8 @@
|
||||
|
||||
"fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
@@ -408,6 +411,10 @@
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="],
|
||||
|
||||
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"libsodium-wrappers": "^0.7.15",
|
||||
"opusscript": "^0.1.1",
|
||||
"playwright": "^1.60.0",
|
||||
"prism-media": "^1.3.5",
|
||||
"qrcode": "^1.5.4"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const CDP = process.env.CDP_PORT || '9222';
|
||||
// Use 127.0.0.1, not "localhost": in containers localhost can resolve to IPv6
|
||||
// (::1) first while Chrome's CDP listens on IPv4, giving ECONNREFUSED ::1.
|
||||
const CDP_HOST = process.env.CDP_HOST || '127.0.0.1';
|
||||
const query = process.argv[2] || '';
|
||||
const mode = (process.argv[3] || 'search').toLowerCase();
|
||||
const out = (o) => { process.stdout.write(JSON.stringify(o)); };
|
||||
@@ -17,7 +20,7 @@ if (!query) { out({ ok: false, error: 'no query' }); process.exit(1); }
|
||||
|
||||
let b;
|
||||
try {
|
||||
b = await chromium.connectOverCDP(`http://localhost:${CDP}`);
|
||||
b = await chromium.connectOverCDP(`http://${CDP_HOST}:${CDP}`);
|
||||
const ctx = b.contexts()[0];
|
||||
const page = ctx.pages()[0] || (await ctx.newPage());
|
||||
page.setDefaultTimeout(20000);
|
||||
|
||||
@@ -110,6 +110,16 @@ async function handleJoin(i: ChatInputCommandInteraction) {
|
||||
|
||||
async function handleLeave(i: ChatInputCommandInteraction) {
|
||||
const left = leaveGuild(i.guildId!);
|
||||
// Leaving voice also ends the broadcast — don't leave a stream live with no
|
||||
// session driving it.
|
||||
const streamer = streamers.get(i.guildId!);
|
||||
if (streamer?.isActive()) {
|
||||
try {
|
||||
await streamer.stop();
|
||||
} catch (e) {
|
||||
console.error("[leave] stopping broadcast failed:", e);
|
||||
}
|
||||
}
|
||||
await i.reply({ content: left ? "음성 채널에서 나갔습니다." : "접속 중인 세션이 없습니다.", ...eph });
|
||||
}
|
||||
|
||||
|
||||
@@ -68,14 +68,17 @@ services:
|
||||
- "nvidia.com/gpu=all"
|
||||
shm_size: "1gb" # Chrome needs a larger /dev/shm
|
||||
ports:
|
||||
# Host ports are overridable. If the HOST already runs VNC on 5901
|
||||
# (see docs/vnc-xfce-setup.md), set VNC_PORT=5902 in .env — this server
|
||||
# does, so .env pins VNC_PORT=5902.
|
||||
- "${VNC_PORT:-5901}:5901" # VNC
|
||||
- "${NOVNC_PORT:-6080}:6080" # noVNC (open in a browser)
|
||||
# Brain bridge: bind to loopback only — it is an internal bot<->brain API
|
||||
# with no auth and must not be reachable off-host.
|
||||
- "127.0.0.1:${BRIDGE_PORT:-8765}:8765"
|
||||
# All published to loopback only by default — VNC/noVNC use a weak default
|
||||
# password and the bridge is an unauthenticated internal API, so none
|
||||
# should be reachable off-host. For remote access use an SSH tunnel, or
|
||||
# set a strong VNC_PASSWORD and override the bind (e.g. VNC_BIND=0.0.0.0).
|
||||
# Host VNC port is overridable; this server already runs Xvnc on 5901 so
|
||||
# .env pins VNC_PORT=5902.
|
||||
- "${VNC_BIND:-127.0.0.1}:${VNC_PORT:-5901}:5901" # VNC
|
||||
- "${VNC_BIND:-127.0.0.1}:${NOVNC_PORT:-6080}:6080" # noVNC (browser)
|
||||
# The brain bridge is NOT published: it binds the container's loopback
|
||||
# (BRIDGE_HOST=127.0.0.1) and is only consumed by the bot in this same
|
||||
# container, so it needs no host port and stays unreachable off-container.
|
||||
volumes:
|
||||
- javis_data:/data # jarvis db + memory
|
||||
- whisper_cache:/root/.cache/huggingface # cached Whisper models
|
||||
|
||||
@@ -14,7 +14,7 @@ set -euo pipefail
|
||||
: "${WHISPER_DEVICE:=cuda}"
|
||||
: "${WHISPER_COMPUTE_TYPE:=float16}"
|
||||
: "${JARVIS_DB_PATH:=/data/jarvis.db}"
|
||||
: "${BRIDGE_HOST:=0.0.0.0}"
|
||||
: "${BRIDGE_HOST:=127.0.0.1}"
|
||||
: "${BRIDGE_PORT:=8765}"
|
||||
: "${PIPER_VOICE:=en_GB-alan-medium}"
|
||||
: "${PIPER_VOICE_DIR:=/opt/piper-voices}"
|
||||
|
||||
@@ -8,7 +8,13 @@ for i in $(seq 1 40); do
|
||||
done
|
||||
sleep 3
|
||||
export DISPLAY=:1
|
||||
# --remote-debugging-port exposes CDP so the brain's browse-search.mjs
|
||||
# (playwright connectOverCDP) can drive this on-screen Chrome for the
|
||||
# broadcast-visible Google/YouTube search. Bound to loopback (same container).
|
||||
exec google-chrome \
|
||||
--no-sandbox --no-first-run --disable-dev-shm-usage \
|
||||
--remote-debugging-port="${CDP_PORT:-9222}" \
|
||||
--remote-debugging-address=127.0.0.1 \
|
||||
--user-data-dir="${CHROME_PROFILE_DIR:-/root/chrome-profile}" \
|
||||
--password-store=basic --start-maximized \
|
||||
"${CHROME_START_URL:-about:blank}"
|
||||
|
||||
@@ -975,6 +975,15 @@ def run_reply_engine(db: "Database", cfg, tts: Optional[Any],
|
||||
"planning",
|
||||
)
|
||||
|
||||
# In screen-share mode, always offer setBroadcast. "Turn the broadcast
|
||||
# on/off" is language-agnostic intent the embedding/keyword router won't
|
||||
# reliably surface for a non-English utterance (e.g. "방송 꺼줘"), so the
|
||||
# model would otherwise never get the option. It self-gates and is only
|
||||
# acted on when the model actually calls it.
|
||||
if getattr(cfg, "stream_browser", True) and "setBroadcast" in _full_catalog_names \
|
||||
and "setBroadcast" not in routed_tools:
|
||||
routed_tools = routed_tools + ["setBroadcast"]
|
||||
|
||||
_planner_schema = generate_tools_json_schema(routed_tools, mcp_tools)
|
||||
_planner_tool_catalog: list[tuple[str, str]] = []
|
||||
for _schema in (_planner_schema or []):
|
||||
|
||||
@@ -26,10 +26,12 @@ class SetBroadcastTool(Tool):
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return (
|
||||
"Start or stop the live screen-share broadcast (Go-Live / showing your "
|
||||
"screen on the call). Use when the user asks to turn the broadcast or "
|
||||
"screen on or off (show/hide your screen, go live, stop streaming). "
|
||||
"Only available in screen-share mode."
|
||||
"Control the live screen-share broadcast (Go-Live). You MUST call this "
|
||||
"tool — do not just reply in words — whenever the user asks to turn the "
|
||||
"broadcast or screen ON or OFF: start/begin/turn on/show your screen/go "
|
||||
"live -> action='start'; stop/end/turn off/hide your screen/stop "
|
||||
"streaming -> action='stop'. Saying you did it without calling this tool "
|
||||
"does nothing. Only available in screen-share mode."
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
Reference in New Issue
Block a user