mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-11 01:45:58 +00:00
Fix dev startup and channel connect popup
This commit is contained in:
@@ -15,19 +15,17 @@ import {
|
|||||||
useChannelProviders,
|
useChannelProviders,
|
||||||
useConnectChannelProvider,
|
useConnectChannelProvider,
|
||||||
} from "@/core/channels/hooks";
|
} from "@/core/channels/hooks";
|
||||||
|
import {
|
||||||
|
closeConnectWindow,
|
||||||
|
openConnectUrl,
|
||||||
|
prepareConnectWindow,
|
||||||
|
} from "@/core/channels/open-connect-url";
|
||||||
import type { ChannelProvider } from "@/core/channels/types";
|
import type { ChannelProvider } from "@/core/channels/types";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { ChannelProviderIcon } from "./channel-provider-icon";
|
import { ChannelProviderIcon } from "./channel-provider-icon";
|
||||||
|
|
||||||
function openConnectUrl(url: string) {
|
|
||||||
const opened = window.open(url, "_blank", "noopener,noreferrer");
|
|
||||||
if (!opened) {
|
|
||||||
window.location.assign(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function providerCanConnect(provider: ChannelProvider): boolean {
|
function providerCanConnect(provider: ChannelProvider): boolean {
|
||||||
return (
|
return (
|
||||||
provider.enabled &&
|
provider.enabled &&
|
||||||
@@ -97,8 +95,11 @@ export function WorkspaceChannelsList() {
|
|||||||
!provider.configured ? t.channels.unconfigured : undefined
|
!provider.configured ? t.channels.unconfigured : undefined
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const connectWindow = prepareConnectWindow();
|
||||||
connectMutation.mutate(provider.provider, {
|
connectMutation.mutate(provider.provider, {
|
||||||
onSuccess: (result) => openConnectUrl(result.url),
|
onSuccess: (result) =>
|
||||||
|
openConnectUrl(result.url, connectWindow),
|
||||||
|
onError: () => closeConnectWindow(connectWindow),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ import {
|
|||||||
useConnectChannelProvider,
|
useConnectChannelProvider,
|
||||||
useDisconnectChannelConnection,
|
useDisconnectChannelConnection,
|
||||||
} from "@/core/channels/hooks";
|
} from "@/core/channels/hooks";
|
||||||
|
import {
|
||||||
|
closeConnectWindow,
|
||||||
|
openConnectUrl,
|
||||||
|
prepareConnectWindow,
|
||||||
|
} from "@/core/channels/open-connect-url";
|
||||||
import type { ChannelConnection, ChannelProvider } from "@/core/channels/types";
|
import type { ChannelConnection, ChannelProvider } from "@/core/channels/types";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -32,13 +37,6 @@ import { ChannelProviderIcon } from "../channels/channel-provider-icon";
|
|||||||
|
|
||||||
import { SettingsSection } from "./settings-section";
|
import { SettingsSection } from "./settings-section";
|
||||||
|
|
||||||
function openConnectUrl(url: string) {
|
|
||||||
const opened = window.open(url, "_blank", "noopener,noreferrer");
|
|
||||||
if (!opened) {
|
|
||||||
window.location.assign(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProviderDescription(
|
function getProviderDescription(
|
||||||
provider: ChannelProvider,
|
provider: ChannelProvider,
|
||||||
descriptions: Record<string, string>,
|
descriptions: Record<string, string>,
|
||||||
@@ -144,8 +142,11 @@ function ChannelProviderItem({
|
|||||||
disabled={!canConnect || isConnecting}
|
disabled={!canConnect || isConnecting}
|
||||||
title={!provider.configured ? t.channels.unconfigured : undefined}
|
title={!provider.configured ? t.channels.unconfigured : undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const connectWindow = prepareConnectWindow();
|
||||||
connectMutation.mutate(provider.provider, {
|
connectMutation.mutate(provider.provider, {
|
||||||
onSuccess: (result) => openConnectUrl(result.url),
|
onSuccess: (result) =>
|
||||||
|
openConnectUrl(result.url, connectWindow),
|
||||||
|
onError: () => closeConnectWindow(connectWindow),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export type ChannelConnectWindow = Window | null;
|
||||||
|
|
||||||
|
export function prepareConnectWindow(): ChannelConnectWindow {
|
||||||
|
const opened = window.open("about:blank", "_blank");
|
||||||
|
if (opened) {
|
||||||
|
opened.opener = null;
|
||||||
|
}
|
||||||
|
return opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openConnectUrl(
|
||||||
|
url: string,
|
||||||
|
connectWindow: ChannelConnectWindow = prepareConnectWindow(),
|
||||||
|
) {
|
||||||
|
if (connectWindow && !connectWindow.closed) {
|
||||||
|
connectWindow.location.replace(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.assign(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeConnectWindow(connectWindow: ChannelConnectWindow) {
|
||||||
|
if (connectWindow && !connectWindow.closed) {
|
||||||
|
connectWindow.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,19 @@ function mockChannelsAPI(page: Page) {
|
|||||||
body: JSON.stringify({ connections: [] }),
|
body: JSON.stringify({ connections: [] }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void page.route("**/api/channels/slack/connect", (route) => {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider: "slack",
|
||||||
|
mode: "oauth",
|
||||||
|
url: "http://localhost:3000/mock-slack-oauth?client_id=dev&state=test",
|
||||||
|
expires_in: 600,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe("IM channels", () => {
|
test.describe("IM channels", () => {
|
||||||
@@ -73,5 +86,16 @@ test.describe("IM channels", () => {
|
|||||||
await expect(page.getByText("Telegram direct messages")).toBeVisible();
|
await expect(page.getByText("Telegram direct messages")).toBeVisible();
|
||||||
await expect(page.getByText("Slack workspace messages")).toBeVisible();
|
await expect(page.getByText("Slack workspace messages")).toBeVisible();
|
||||||
await expect(page.getByText("Discord server messages")).toBeVisible();
|
await expect(page.getByText("Discord server messages")).toBeVisible();
|
||||||
|
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Settings" });
|
||||||
|
const connectButtons = dialog.getByRole("button", { name: "Connect" });
|
||||||
|
await expect(connectButtons).toHaveCount(3);
|
||||||
|
|
||||||
|
const popupPromise = page.waitForEvent("popup");
|
||||||
|
await connectButtons.nth(1).click();
|
||||||
|
const popup = await popupPromise;
|
||||||
|
await expect(page).toHaveURL(/\/workspace\/chats\/new/);
|
||||||
|
await expect(popup).toHaveURL(/\/mock-slack-oauth/);
|
||||||
|
await popup.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
closeConnectWindow,
|
||||||
|
openConnectUrl,
|
||||||
|
prepareConnectWindow,
|
||||||
|
} from "@/core/channels/open-connect-url";
|
||||||
|
|
||||||
|
type PopupStub = {
|
||||||
|
closed: boolean;
|
||||||
|
close: ReturnType<typeof vi.fn>;
|
||||||
|
location: {
|
||||||
|
replace: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
opener: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function stubWindow(openResult: PopupStub | null) {
|
||||||
|
const assign = vi.fn();
|
||||||
|
const open = vi.fn(() => openResult);
|
||||||
|
vi.stubGlobal("window", {
|
||||||
|
open,
|
||||||
|
location: { assign },
|
||||||
|
});
|
||||||
|
return { assign, open };
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePopup(): PopupStub {
|
||||||
|
return {
|
||||||
|
closed: false,
|
||||||
|
close: vi.fn(),
|
||||||
|
location: { replace: vi.fn() },
|
||||||
|
opener: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("channel connect window helpers", () => {
|
||||||
|
test("opens a blank tab synchronously and detaches opener", () => {
|
||||||
|
const popup = makePopup();
|
||||||
|
const { open } = stubWindow(popup);
|
||||||
|
|
||||||
|
const prepared = prepareConnectWindow();
|
||||||
|
|
||||||
|
expect(open).toHaveBeenCalledWith("about:blank", "_blank");
|
||||||
|
expect(prepared).toBe(popup);
|
||||||
|
expect(popup.opener).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigates a prepared popup without opening another window", () => {
|
||||||
|
const popup = makePopup();
|
||||||
|
const { assign, open } = stubWindow(null);
|
||||||
|
|
||||||
|
openConnectUrl(
|
||||||
|
"https://t.me/deerflow_bot?start=state",
|
||||||
|
popup as unknown as Window,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(open).not.toHaveBeenCalled();
|
||||||
|
expect(assign).not.toHaveBeenCalled();
|
||||||
|
expect(popup.location.replace).toHaveBeenCalledWith(
|
||||||
|
"https://t.me/deerflow_bot?start=state",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to current-window navigation when no popup is available", () => {
|
||||||
|
const { assign } = stubWindow(null);
|
||||||
|
|
||||||
|
openConnectUrl("https://slack.com/oauth/v2/authorize");
|
||||||
|
|
||||||
|
expect(assign).toHaveBeenCalledWith("https://slack.com/oauth/v2/authorize");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("closes a prepared popup on connect failure", () => {
|
||||||
|
const popup = makePopup();
|
||||||
|
|
||||||
|
closeConnectWindow(popup as unknown as Window);
|
||||||
|
|
||||||
|
expect(popup.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
+88
-9
@@ -29,14 +29,6 @@ set -e
|
|||||||
REPO_ROOT="$(builtin cd "$(dirname "${BASH_SOURCE[0]}")/.." >/dev/null 2>&1 && pwd -P)"
|
REPO_ROOT="$(builtin cd "$(dirname "${BASH_SOURCE[0]}")/.." >/dev/null 2>&1 && pwd -P)"
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
# ── Load .env ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
if [ -f "$REPO_ROOT/.env" ]; then
|
|
||||||
set -a
|
|
||||||
source "$REPO_ROOT/.env"
|
|
||||||
set +a
|
|
||||||
fi
|
|
||||||
|
|
||||||
_pick_python() {
|
_pick_python() {
|
||||||
local candidate
|
local candidate
|
||||||
for candidate in python3 python py; do
|
for candidate in python3 python py; do
|
||||||
@@ -48,6 +40,61 @@ _pick_python() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_load_dotenv_file() {
|
||||||
|
local env_file=$1
|
||||||
|
local python_bin
|
||||||
|
|
||||||
|
[ -f "$env_file" ] || return 0
|
||||||
|
|
||||||
|
if ! python_bin="$(_pick_python)"; then
|
||||||
|
echo "Python is required to load $env_file safely."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
eval "$("$python_bin" - "$env_file" <<'PY'
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
env_path = Path(sys.argv[1])
|
||||||
|
assign_re = re.compile(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$")
|
||||||
|
|
||||||
|
|
||||||
|
def strip_unquoted_comment(value: str) -> str:
|
||||||
|
for index, char in enumerate(value):
|
||||||
|
if char == "#" and (index == 0 or value[index - 1].isspace()):
|
||||||
|
return value[:index].rstrip()
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = assign_re.match(line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key, value = match.groups()
|
||||||
|
value = value.strip()
|
||||||
|
try:
|
||||||
|
parsed = shlex.split(value, comments=True, posix=True)
|
||||||
|
except ValueError:
|
||||||
|
value = strip_unquoted_comment(value)
|
||||||
|
else:
|
||||||
|
value = parsed[0] if parsed else ""
|
||||||
|
|
||||||
|
print(f"export {key}={shlex.quote(value)}")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Load .env ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_load_dotenv_file "$REPO_ROOT/.env"
|
||||||
|
|
||||||
# ── Argument parsing ─────────────────────────────────────────────────────────
|
# ── Argument parsing ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
DEV_MODE=true
|
DEV_MODE=true
|
||||||
@@ -179,7 +226,10 @@ _is_port_listening() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v netstat >/dev/null 2>&1; then
|
if command -v netstat >/dev/null 2>&1; then
|
||||||
if netstat -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "(^|[.:])${port}$"; then
|
if netstat -ltn 2>/dev/null | awk -v port="$port" '
|
||||||
|
toupper($NF) == "LISTEN" && $4 ~ "(^|[.:])" port "$" { found = 1 }
|
||||||
|
END { exit found ? 0 : 1 }
|
||||||
|
'; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -187,6 +237,21 @@ _is_port_listening() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wait_for_port_free() {
|
||||||
|
local port=$1
|
||||||
|
local timeout=${2:-10}
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
while _is_port_listening "$port"; do
|
||||||
|
if [ "$elapsed" -ge "$timeout" ]; then
|
||||||
|
echo " ⚠ Port $port is still in use after ${timeout}s"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
elapsed=$((elapsed + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
_is_repo_nginx_pid() {
|
_is_repo_nginx_pid() {
|
||||||
local pid=$1
|
local pid=$1
|
||||||
local command
|
local command
|
||||||
@@ -239,9 +304,12 @@ stop_all() {
|
|||||||
echo "Stopping all services..."
|
echo "Stopping all services..."
|
||||||
_report_reclaimed_ports
|
_report_reclaimed_ports
|
||||||
_kill_repo_processes "uvicorn app.gateway.app:app"
|
_kill_repo_processes "uvicorn app.gateway.app:app"
|
||||||
|
_kill_repo_processes "pnpm .*run dev"
|
||||||
_kill_repo_processes "next dev"
|
_kill_repo_processes "next dev"
|
||||||
_kill_repo_processes "next start"
|
_kill_repo_processes "next start"
|
||||||
_kill_repo_processes "next-server"
|
_kill_repo_processes "next-server"
|
||||||
|
_kill_repo_processes "next/dist"
|
||||||
|
_kill_repo_processes "turbopack"
|
||||||
nginx -c "$REPO_ROOT/docker/nginx/nginx.local.conf" -p "$REPO_ROOT" -s quit 2>/dev/null || true
|
nginx -c "$REPO_ROOT/docker/nginx/nginx.local.conf" -p "$REPO_ROOT" -s quit 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
_kill_repo_nginx
|
_kill_repo_nginx
|
||||||
@@ -252,6 +320,9 @@ stop_all() {
|
|||||||
_kill_repo_port 8001
|
_kill_repo_port 8001
|
||||||
_kill_repo_port 3000
|
_kill_repo_port 3000
|
||||||
_kill_repo_port 2026
|
_kill_repo_port 2026
|
||||||
|
_wait_for_port_free 8001 30 || true
|
||||||
|
_wait_for_port_free 3000 30 || true
|
||||||
|
_wait_for_port_free 2026 30 || true
|
||||||
./scripts/cleanup-containers.sh deer-flow-sandbox 2>/dev/null || true
|
./scripts/cleanup-containers.sh deer-flow-sandbox 2>/dev/null || true
|
||||||
echo "✓ All services stopped"
|
echo "✓ All services stopped"
|
||||||
}
|
}
|
||||||
@@ -414,6 +485,7 @@ trap 'cleanup 143' TERM
|
|||||||
# In daemon mode, wraps with nohup. Waits for port to be ready.
|
# In daemon mode, wraps with nohup. Waits for port to be ready.
|
||||||
run_service() {
|
run_service() {
|
||||||
local name="$1" cmd="$2" port="$3" timeout="$4"
|
local name="$1" cmd="$2" port="$3" timeout="$4"
|
||||||
|
local service_pid
|
||||||
|
|
||||||
if _is_port_listening "$port"; then
|
if _is_port_listening "$port"; then
|
||||||
echo "✗ $name cannot start because port $port is already in use."
|
echo "✗ $name cannot start because port $port is already in use."
|
||||||
@@ -427,6 +499,7 @@ run_service() {
|
|||||||
else
|
else
|
||||||
sh -c "$cmd" &
|
sh -c "$cmd" &
|
||||||
fi
|
fi
|
||||||
|
service_pid=$!
|
||||||
|
|
||||||
./scripts/wait-for-port.sh "$port" "$timeout" "$name" || {
|
./scripts/wait-for-port.sh "$port" "$timeout" "$name" || {
|
||||||
local logfile="logs/$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-').log"
|
local logfile="logs/$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-').log"
|
||||||
@@ -434,6 +507,12 @@ run_service() {
|
|||||||
[ -f "$logfile" ] && tail -20 "$logfile"
|
[ -f "$logfile" ] && tail -20 "$logfile"
|
||||||
cleanup 1
|
cleanup 1
|
||||||
}
|
}
|
||||||
|
if ! kill -0 "$service_pid" 2>/dev/null; then
|
||||||
|
local logfile="logs/$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-').log"
|
||||||
|
echo "✗ $name process exited after port $port became available."
|
||||||
|
[ -f "$logfile" ] && tail -20 "$logfile"
|
||||||
|
cleanup 1
|
||||||
|
fi
|
||||||
echo "✓ $name started on localhost:$port"
|
echo "✓ $name started on localhost:$port"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user