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 \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
LANG=C.UTF-8 \
|
LANG=C.UTF-8 \
|
||||||
DISPLAY=:1 \
|
DISPLAY=:1 \
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
|
||||||
PATH=/opt/venv/bin:/root/.bun/bin:/usr/local/bin:/usr/bin:/bin \
|
PATH=/opt/venv/bin:/root/.bun/bin:/usr/local/bin:/usr/bin:/bin \
|
||||||
NVIDIA_VISIBLE_DEVICES=all \
|
NVIDIA_VISIBLE_DEVICES=all \
|
||||||
NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"libsodium-wrappers": "^0.7.15",
|
"libsodium-wrappers": "^0.7.15",
|
||||||
"opusscript": "^0.1.1",
|
"opusscript": "^0.1.1",
|
||||||
|
"playwright": "^1.60.0",
|
||||||
"prism-media": "^1.3.5",
|
"prism-media": "^1.3.5",
|
||||||
"qrcode": "^1.5.4",
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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",
|
"dotenv": "^16.4.5",
|
||||||
"libsodium-wrappers": "^0.7.15",
|
"libsodium-wrappers": "^0.7.15",
|
||||||
"opusscript": "^0.1.1",
|
"opusscript": "^0.1.1",
|
||||||
|
"playwright": "^1.60.0",
|
||||||
"prism-media": "^1.3.5",
|
"prism-media": "^1.3.5",
|
||||||
"qrcode": "^1.5.4"
|
"qrcode": "^1.5.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
const CDP = process.env.CDP_PORT || '9222';
|
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 query = process.argv[2] || '';
|
||||||
const mode = (process.argv[3] || 'search').toLowerCase();
|
const mode = (process.argv[3] || 'search').toLowerCase();
|
||||||
const out = (o) => { process.stdout.write(JSON.stringify(o)); };
|
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;
|
let b;
|
||||||
try {
|
try {
|
||||||
b = await chromium.connectOverCDP(`http://localhost:${CDP}`);
|
b = await chromium.connectOverCDP(`http://${CDP_HOST}:${CDP}`);
|
||||||
const ctx = b.contexts()[0];
|
const ctx = b.contexts()[0];
|
||||||
const page = ctx.pages()[0] || (await ctx.newPage());
|
const page = ctx.pages()[0] || (await ctx.newPage());
|
||||||
page.setDefaultTimeout(20000);
|
page.setDefaultTimeout(20000);
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ async function handleJoin(i: ChatInputCommandInteraction) {
|
|||||||
|
|
||||||
async function handleLeave(i: ChatInputCommandInteraction) {
|
async function handleLeave(i: ChatInputCommandInteraction) {
|
||||||
const left = leaveGuild(i.guildId!);
|
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 });
|
await i.reply({ content: left ? "음성 채널에서 나갔습니다." : "접속 중인 세션이 없습니다.", ...eph });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,14 +68,17 @@ services:
|
|||||||
- "nvidia.com/gpu=all"
|
- "nvidia.com/gpu=all"
|
||||||
shm_size: "1gb" # Chrome needs a larger /dev/shm
|
shm_size: "1gb" # Chrome needs a larger /dev/shm
|
||||||
ports:
|
ports:
|
||||||
# Host ports are overridable. If the HOST already runs VNC on 5901
|
# All published to loopback only by default — VNC/noVNC use a weak default
|
||||||
# (see docs/vnc-xfce-setup.md), set VNC_PORT=5902 in .env — this server
|
# password and the bridge is an unauthenticated internal API, so none
|
||||||
# does, so .env pins VNC_PORT=5902.
|
# should be reachable off-host. For remote access use an SSH tunnel, or
|
||||||
- "${VNC_PORT:-5901}:5901" # VNC
|
# set a strong VNC_PASSWORD and override the bind (e.g. VNC_BIND=0.0.0.0).
|
||||||
- "${NOVNC_PORT:-6080}:6080" # noVNC (open in a browser)
|
# Host VNC port is overridable; this server already runs Xvnc on 5901 so
|
||||||
# Brain bridge: bind to loopback only — it is an internal bot<->brain API
|
# .env pins VNC_PORT=5902.
|
||||||
# with no auth and must not be reachable off-host.
|
- "${VNC_BIND:-127.0.0.1}:${VNC_PORT:-5901}:5901" # VNC
|
||||||
- "127.0.0.1:${BRIDGE_PORT:-8765}:8765"
|
- "${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:
|
volumes:
|
||||||
- javis_data:/data # jarvis db + memory
|
- javis_data:/data # jarvis db + memory
|
||||||
- whisper_cache:/root/.cache/huggingface # cached Whisper models
|
- whisper_cache:/root/.cache/huggingface # cached Whisper models
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ set -euo pipefail
|
|||||||
: "${WHISPER_DEVICE:=cuda}"
|
: "${WHISPER_DEVICE:=cuda}"
|
||||||
: "${WHISPER_COMPUTE_TYPE:=float16}"
|
: "${WHISPER_COMPUTE_TYPE:=float16}"
|
||||||
: "${JARVIS_DB_PATH:=/data/jarvis.db}"
|
: "${JARVIS_DB_PATH:=/data/jarvis.db}"
|
||||||
: "${BRIDGE_HOST:=0.0.0.0}"
|
: "${BRIDGE_HOST:=127.0.0.1}"
|
||||||
: "${BRIDGE_PORT:=8765}"
|
: "${BRIDGE_PORT:=8765}"
|
||||||
: "${PIPER_VOICE:=en_GB-alan-medium}"
|
: "${PIPER_VOICE:=en_GB-alan-medium}"
|
||||||
: "${PIPER_VOICE_DIR:=/opt/piper-voices}"
|
: "${PIPER_VOICE_DIR:=/opt/piper-voices}"
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ for i in $(seq 1 40); do
|
|||||||
done
|
done
|
||||||
sleep 3
|
sleep 3
|
||||||
export DISPLAY=:1
|
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 \
|
exec google-chrome \
|
||||||
--no-sandbox --no-first-run --disable-dev-shm-usage \
|
--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 \
|
--password-store=basic --start-maximized \
|
||||||
"${CHROME_START_URL:-about:blank}"
|
"${CHROME_START_URL:-about:blank}"
|
||||||
|
|||||||
@@ -975,6 +975,15 @@ def run_reply_engine(db: "Database", cfg, tts: Optional[Any],
|
|||||||
"planning",
|
"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_schema = generate_tools_json_schema(routed_tools, mcp_tools)
|
||||||
_planner_tool_catalog: list[tuple[str, str]] = []
|
_planner_tool_catalog: list[tuple[str, str]] = []
|
||||||
for _schema in (_planner_schema or []):
|
for _schema in (_planner_schema or []):
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ class SetBroadcastTool(Tool):
|
|||||||
@property
|
@property
|
||||||
def description(self) -> str:
|
def description(self) -> str:
|
||||||
return (
|
return (
|
||||||
"Start or stop the live screen-share broadcast (Go-Live / showing your "
|
"Control the live screen-share broadcast (Go-Live). You MUST call this "
|
||||||
"screen on the call). Use when the user asks to turn the broadcast or "
|
"tool — do not just reply in words — whenever the user asks to turn the "
|
||||||
"screen on or off (show/hide your screen, go live, stop streaming). "
|
"broadcast or screen ON or OFF: start/begin/turn on/show your screen/go "
|
||||||
"Only available in screen-share mode."
|
"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
|
@property
|
||||||
|
|||||||
Reference in New Issue
Block a user