fix: improve llmwiki search integration

This commit is contained in:
Administrator committed 2026-08-13 16:03:08 +08:00
1 parent 4809ee7db3
commit 56044d9cb7
20 files changed
+482 -44

No files matched your search

+9 -8
View File
@@ -11,8 +11,7 @@
"--host", "--host",
"0.0.0.0", "0.0.0.0",
"--port", "--port",
"8001", "8001"
"--reload"
], ],
"cwd": "${workspaceFolder}/offline-backend-20260512/backend", "cwd": "${workspaceFolder}/offline-backend-20260512/backend",
"python": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe", "python": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
@@ -24,6 +23,7 @@
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX" "WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
}, },
"console": "integratedTerminal", "console": "integratedTerminal",
"redirectOutput": true,
"justMyCode": false, "justMyCode": false,
"subProcess": true "subProcess": true
}, },
@@ -31,7 +31,7 @@
"name": "DeerFlow: Frontend Vite (5174)", "name": "DeerFlow: Frontend Vite (5174)",
"type": "node-terminal", "type": "node-terminal",
"request": "launch", "request": "launch",
"command": "npm run dev -- --host 0.0.0.0 --port 5174", "command": "npm.cmd run dev -- --host 0.0.0.0 --port 5174",
"cwd": "${workspaceFolder}/frontend-web", "cwd": "${workspaceFolder}/frontend-web",
"env": { "env": {
"VITE_BACKEND_BASE_URL": "http://127.0.0.1:8001", "VITE_BACKEND_BASE_URL": "http://127.0.0.1:8001",
@@ -40,21 +40,22 @@
"serverReadyAction": { "serverReadyAction": {
"pattern": "Local:\\s+(https?://[^\\s]+)", "pattern": "Local:\\s+(https?://[^\\s]+)",
"uriFormat": "%s", "uriFormat": "%s",
"action": "debugWithChrome", "action": "openExternally"
"webRoot": "${workspaceFolder}/frontend-web"
} }
}, },
{ {
"name": "DeerFlow: Open Frontend in Chrome", "name": "DeerFlow: Open Frontend in Chrome",
"type": "pwa-chrome", "type": "node-terminal",
"request": "launch", "request": "launch",
"url": "http://127.0.0.1:5174/", "preLaunchTask": "DeerFlow: Start Full Stack and Wait",
"webRoot": "${workspaceFolder}/frontend-web" "command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Start-Process 'http://127.0.0.1:5174/'\"",
"cwd": "${workspaceFolder}"
} }
], ],
"compounds": [ "compounds": [
{ {
"name": "DeerFlow: Full Stack (Backend + Frontend)", "name": "DeerFlow: Full Stack (Backend + Frontend)",
"preLaunchTask": "DeerFlow: Stop Full Stack",
"configurations": [ "configurations": [
"DeerFlow: Backend FastAPI (8001)", "DeerFlow: Backend FastAPI (8001)",
"DeerFlow: Frontend Vite (5174)" "DeerFlow: Frontend Vite (5174)"
+52
View File
@@ -1,6 +1,58 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{
"label": "DeerFlow: Start Full Stack and Wait",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/start-all-and-wait-frontend.ps1"
],
"problemMatcher": []
},
{
"label": "DeerFlow: Start Full Stack",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/start-all.ps1"
],
"problemMatcher": []
},
{
"label": "DeerFlow: Stop Full Stack",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/stop-all.ps1"
],
"problemMatcher": []
},
{
"label": "DeerFlow: Status",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/status.ps1"
],
"problemMatcher": []
},
{ {
"label": "DeerFlow: Check Local Dev Environment", "label": "DeerFlow: Check Local Dev Environment",
"type": "shell", "type": "shell",
@@ -301,8 +301,10 @@ function ToolCall({
if (query) { if (query) {
label = label =
name === "web_search" name === "web_search"
? t.toolCalls.searchOnWebFor(query) ? `知识库检索 "${query}"`
: t.toolCalls.searchFor(query); : t.toolCalls.searchFor(query);
} else if (name === "web_search") {
label = "知识库检索";
} }
return ( return (
<ChainOfThoughtStep key={id} label={label} icon={SearchIcon}> <ChainOfThoughtStep key={id} label={label} icon={SearchIcon}>
@@ -101,9 +101,9 @@ def _agent_bound_ids(body: Any) -> list[str]:
def resolve_requested_llmwiki_knowledge_base_ids(body: Any) -> list[str]: def resolve_requested_llmwiki_knowledge_base_ids(body: Any) -> list[str]:
"""Return selected ids plus custom-agent defaults, de-duplicated in order.""" """Return knowledge bases explicitly selected for the current conversation."""
return list(dict.fromkeys([*_agent_bound_ids(body), *_body_selected_ids(body)])) return _body_selected_ids(body)
def llmwiki_selection_requested(body: Any) -> bool: def llmwiki_selection_requested(body: Any) -> bool:
@@ -1045,6 +1045,10 @@ async def start_run(
body_context["rag_mode_enabled"] = True body_context["rag_mode_enabled"] = True
if "llmwiki_knowledge_base_ids" not in body_context and llmwiki_rag.knowledge_base_ids: if "llmwiki_knowledge_base_ids" not in body_context and llmwiki_rag.knowledge_base_ids:
body_context["llmwiki_knowledge_base_ids"] = llmwiki_rag.knowledge_base_ids body_context["llmwiki_knowledge_base_ids"] = llmwiki_rag.knowledge_base_ids
if not llmwiki_rag.knowledge_base_ids:
existing_excluded = list(body_context.get("excluded_tools") or getattr(body, "excluded_tools", None) or [])
if "llmwiki_search" not in existing_excluded:
body_context["excluded_tools"] = [*existing_excluded, "llmwiki_search"]
# Promote whitelisted top-level body fields into the context map so # Promote whitelisted top-level body fields into the context map so
# callers can pass them either nested under ``context`` or as flat # callers can pass them either nested under ``context`` or as flat
# top-level fields (``excluded_tools``, ``skill_stop_names``, …). # top-level fields (``excluded_tools``, ``skill_stop_names``, …).
@@ -7,6 +7,7 @@ import threading
from datetime import datetime from datetime import datetime
from functools import lru_cache from functools import lru_cache
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from zoneinfo import ZoneInfo
from deerflow.config.agents_config import load_agent_soul from deerflow.config.agents_config import load_agent_soul
from deerflow.skills.storage import get_or_new_skill_storage from deerflow.skills.storage import get_or_new_skill_storage
@@ -23,6 +24,19 @@ logger = logging.getLogger(__name__)
# DEER_FLOW_AUTH_DISABLED so operators don't have to learn yet another # DEER_FLOW_AUTH_DISABLED so operators don't have to learn yet another
# spelling for "yes, turn this on". # spelling for "yes, turn this on".
_TRUTHY = frozenset({"1", "true", "yes", "on", "y"}) _TRUTHY = frozenset({"1", "true", "yes", "on", "y"})
_BEIJING_TZ = ZoneInfo("Asia/Shanghai")
def _current_time_context() -> str:
now = datetime.now(_BEIJING_TZ)
return (
f"\n<current_datetime timezone=\"Asia/Shanghai\">{now.strftime('%Y-%m-%d %H:%M:%S, %A')}</current_datetime>"
"\n<time_grounding>"
"When interpreting relative dates such as 今天, 昨天, 近几天, 最近, this week, or last month, "
"use current_datetime as the only reference point. Before calling web_search, rewrite relative "
"time expressions into concrete dates or date ranges in the query; never infer a stale year."
"</time_grounding>"
)
def _is_offline_mode() -> bool: def _is_offline_mode() -> bool:
@@ -761,7 +775,7 @@ RAG/reference display mode is enabled.
acp_section=acp_and_mounts_section, acp_section=acp_and_mounts_section,
) )
prompt = prompt + rag_mode_section + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>" prompt = prompt + rag_mode_section + _current_time_context()
return prompt return prompt
@@ -1359,7 +1373,7 @@ When a search, knowledge, skill, or MCP tool returns a result list:
acp_section=acp_and_mounts_section, acp_section=acp_and_mounts_section,
) )
prompt = prompt + rag_mode_section + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>" prompt = prompt + rag_mode_section + _current_time_context()
if is_notebook_mode: if is_notebook_mode:
source_ids = notebook_source_ids or [] source_ids = notebook_source_ids or []
@@ -6,8 +6,10 @@ import json
import logging import logging
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from typing import Any from typing import Any
from urllib.parse import quote from urllib.parse import quote
from zoneinfo import ZoneInfo
import httpx import httpx
from langchain.tools import tool from langchain.tools import tool
@@ -16,6 +18,12 @@ from deerflow.config import get_app_config
from deerflow.config.tool_config import ToolConfig from deerflow.config.tool_config import ToolConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_BEIJING_TZ = ZoneInfo("Asia/Shanghai")
def _current_time_hint() -> str:
now = datetime.now(_BEIJING_TZ)
return now.strftime("%Y-%m-%d %H:%M:%S, %A, Asia/Shanghai")
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -87,6 +95,23 @@ def _text(value: Any) -> str:
return str(value) return str(value)
def _metadata(item: dict[str, Any]) -> dict[str, Any]:
value = item.get("metadata")
return value if isinstance(value, dict) else {}
def _first_text(*values: Any) -> str:
for value in values:
text = _text(value).strip()
if text:
return text
return ""
def _looks_like_url(value: str) -> bool:
return value.startswith(("http://", "https://"))
def _error_result(query: str, message: str) -> str: def _error_result(query: str, message: str) -> str:
return json.dumps( return json.dumps(
{ {
@@ -100,10 +125,13 @@ def _error_result(query: str, message: str) -> str:
def _normalize_results(data: Any, settings: ConfigurableSearchSettings) -> list[dict[str, str]]: def _normalize_results(data: Any, settings: ConfigurableSearchSettings) -> list[dict[str, str]]:
if not isinstance(data, dict): if isinstance(data, list):
raise ValueError("search API response must be a JSON object") raw_results = data
elif isinstance(data, dict):
raw_results = data.get("results")
else:
raise ValueError("search API response must be a JSON object or array")
raw_results = data.get("results")
if not isinstance(raw_results, list): if not isinstance(raw_results, list):
raise ValueError("search API response field 'results' must be an array") raise ValueError("search API response field 'results' must be an array")
@@ -113,8 +141,21 @@ def _normalize_results(data: Any, settings: ConfigurableSearchSettings) -> list[
logger.warning("Ignoring non-object search result: %r", item) logger.warning("Ignoring non-object search result: %r", item)
continue continue
rec_uuid = _text(item.get("recUuid")).strip() metadata = _metadata(item)
original_url = _text(item.get("url")).strip() rec_uuid = _first_text(item.get("recUuid"), item.get("rec_uuid"), metadata.get("recUuid"), metadata.get("rec_uuid"))
source = _first_text(item.get("source"), item.get("source1"), metadata.get("source"), metadata.get("source1"))
original_url = _first_text(
item.get("url"),
item.get("link"),
item.get("href"),
item.get("source_url"),
metadata.get("url"),
metadata.get("link"),
metadata.get("href"),
metadata.get("source_url"),
)
if not original_url and _looks_like_url(source):
original_url = source
result_url = original_url result_url = original_url
if rec_uuid and settings.result_url_template: if rec_uuid and settings.result_url_template:
result_url = settings.result_url_template.replace( result_url = settings.result_url_template.replace(
@@ -122,14 +163,19 @@ def _normalize_results(data: Any, settings: ConfigurableSearchSettings) -> list[
quote(rec_uuid, safe=""), quote(rec_uuid, safe=""),
) )
normalized.append( normalized_item = {
{ "recUuid": rec_uuid,
"recUuid": rec_uuid, "content": _first_text(item.get("content"), item.get("page_content"), metadata.get("content")),
"content": _text(item.get("content")), "title": _first_text(item.get("title"), item.get("m_title"), metadata.get("m_title")),
"title": _text(item.get("title")), "url": result_url,
"url": result_url, }
} if source:
) normalized_item["source"] = source
publish_time = _first_text(item.get("time"), item.get("publish_time"), item.get("m_publish"), metadata.get("m_publish"))
if publish_time:
normalized_item["time"] = publish_time
normalized_item["publish_time"] = publish_time
normalized.append(normalized_item)
return normalized return normalized
@@ -193,10 +239,14 @@ async def web_search_tool(query: str) -> str:
Args: Args:
query: Focused search keywords or a natural-language retrieval question. query: Focused search keywords or a natural-language retrieval question.
Interpret relative time expressions against the current time in
Asia/Shanghai, and include concrete dates/date ranges in the query
when the user asks about recent periods.
""" """
try: try:
settings = ConfigurableSearchSettings.from_tool_config(get_app_config().get_tool_config("web_search")) settings = ConfigurableSearchSettings.from_tool_config(get_app_config().get_tool_config("web_search"))
except ValueError as exc: except ValueError as exc:
return _error_result(query, str(exc)) return _error_result(query, str(exc))
return await execute_search(query, settings) grounded_query = f"{query.strip()}\n当前时间: {_current_time_hint()}" if query.strip() else query
return await execute_search(grounded_query, settings)
@@ -71,8 +71,8 @@ async def llmwiki_search_tool(
) -> str: ) -> str:
"""Search the LLMWiki knowledge selected for this conversation. """Search the LLMWiki knowledge selected for this conversation.
Use this tool when an answer can benefit from the user's managed knowledge Use this tool only when the current conversation explicitly selected one
bases or the shared public LLMWiki. It only retrieves passages; it cannot or more LLMWiki knowledge bases. It only retrieves passages; it cannot
upload, edit, delete, publish or change permissions. upload, edit, delete, publish or change permissions.
Args: Args:
@@ -90,26 +90,23 @@ async def llmwiki_search_tool(
selected_ids = selected_knowledge_base_ids(context) selected_ids = selected_knowledge_base_ids(context)
allowed_ids = _agent_allowed_ids(context) allowed_ids = _agent_allowed_ids(context)
if selected_ids is None: if selected_ids is None:
selected_ids = allowed_ids return "No LLMWiki knowledge base is selected for this conversation."
elif allowed_ids is not None and not set(selected_ids).issubset(set(allowed_ids)): elif allowed_ids is not None and not set(selected_ids).issubset(set(allowed_ids)):
return "The conversation requested knowledge outside this agent's configured LLMWiki scope." return "The conversation requested knowledge outside this agent's configured LLMWiki scope."
if selected_ids == []: if selected_ids == []:
return "No LLMWiki knowledge base is selected for this conversation." return "No LLMWiki knowledge base is selected for this conversation."
if selected_ids is not None: rows = []
rows = [] for mapping_id in selected_ids:
for mapping_id in selected_ids: row = await store.get_authorized(
row = await store.get_authorized( mapping_id,
mapping_id, user_id,
user_id, write=False,
write=False, is_admin=is_admin,
is_admin=is_admin, )
) if row is None:
if row is None: return f"Knowledge base {mapping_id!r} is not available to the current user."
return f"Knowledge base {mapping_id!r} is not available to the current user." rows.append(row)
rows.append(row)
else:
rows = await store.list_visible(user_id, scope="all", is_admin=False)
if not rows: if not rows:
return "No authorized LLMWiki knowledge bases are available." return "No authorized LLMWiki knowledge bases are available."
@@ -104,6 +104,82 @@ async def test_configurable_search_handles_empty_results() -> None:
assert result == {"query": "nothing", "total_results": 0, "results": []} assert result == {"query": "nothing", "total_results": 0, "results": []}
@pytest.mark.asyncio
async def test_configurable_search_accepts_root_array_results() -> None:
async def handler(_: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json=[
{
"recUuid": "root-array-record",
"content": "direct array content",
"title": "direct array title",
"url": "https://original.example.test/root-array",
}
],
)
result = json.loads(
await execute_search(
"array response",
_settings(result_url_template=""),
transport=httpx.MockTransport(handler),
)
)
assert result == {
"query": "array response",
"total_results": 1,
"results": [
{
"recUuid": "root-array-record",
"content": "direct array content",
"title": "direct array title",
"url": "https://original.example.test/root-array",
}
],
}
@pytest.mark.asyncio
async def test_configurable_search_normalizes_metadata_wrapped_results() -> None:
async def handler(_: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json=[
{
"page_content": "outer content",
"metadata": {
"m_title": "metadata title",
"content": "metadata content",
"source1": "https://source.example.test/article/1",
"m_publish": "2026-08-13",
},
}
],
)
result = json.loads(
await execute_search(
"metadata response",
_settings(result_url_template=""),
transport=httpx.MockTransport(handler),
)
)
assert result["results"] == [
{
"recUuid": "",
"content": "outer content",
"title": "metadata title",
"source": "https://source.example.test/article/1",
"time": "2026-08-13",
"publish_time": "2026-08-13",
"url": "https://source.example.test/article/1",
}
]
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
("response_factory", "expected_error"), ("response_factory", "expected_error"),
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import json import json
from types import SimpleNamespace
import httpx import httpx
import pytest import pytest
@@ -128,6 +129,18 @@ def test_conversation_selection_is_deduplicated_and_absence_is_distinct() -> Non
assert selected_knowledge_base_ids({"llmwiki_knowledge_base_ids": ["kb-one", "kb-one", "", "kb-two"]}) == ["kb-one", "kb-two"] assert selected_knowledge_base_ids({"llmwiki_knowledge_base_ids": ["kb-one", "kb-one", "", "kb-two"]}) == ["kb-one", "kb-two"]
def test_llmwiki_rag_only_uses_explicit_conversation_selection() -> None:
from app.gateway.llmwiki_rag import resolve_requested_llmwiki_knowledge_base_ids
body = SimpleNamespace(
assistant_id="agent-with-bound-kb",
context={},
metadata={"llmwiki_knowledge_base_ids": ["kb-one", "kb-one", "", "kb-two"]},
)
assert resolve_requested_llmwiki_knowledge_base_ids(body) == ["kb-one", "kb-two"]
def test_weknora_frame_bootstrap_accepts_query_token_only_for_frame_route() -> None: def test_weknora_frame_bootstrap_accepts_query_token_only_for_frame_route() -> None:
from app.gateway.deps import get_request_access_token from app.gateway.deps import get_request_access_token
+55
View File
@@ -0,0 +1,55 @@
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RootDir = Resolve-Path (Join-Path $ScriptDir "..")
$BackendDir = Join-Path $RootDir "offline-backend-20260512\backend"
$FrontendDir = Join-Path $RootDir "frontend-web"
$RuntimeDir = Join-Path $RootDir ".runtime"
$LogDir = Join-Path $RuntimeDir "logs"
$PidDir = Join-Path $RuntimeDir "pids"
$BackendHost = if ($env:BACKEND_HOST) { $env:BACKEND_HOST } else { "0.0.0.0" }
$BackendPort = if ($env:BACKEND_PORT) { $env:BACKEND_PORT } else { "8001" }
$FrontendHost = if ($env:FRONTEND_HOST) { $env:FRONTEND_HOST } else { "0.0.0.0" }
$FrontendPort = if ($env:FRONTEND_PORT) { $env:FRONTEND_PORT } else { "5174" }
$HostIp = if ($env:HOST_IP) { $env:HOST_IP } else { "127.0.0.1" }
$BackendPublicBaseUrl = if ($env:BACKEND_PUBLIC_BASE_URL) { $env:BACKEND_PUBLIC_BASE_URL } else { "http://${HostIp}:${BackendPort}" }
$FrontendPublicBaseUrl = if ($env:FRONTEND_PUBLIC_BASE_URL) { $env:FRONTEND_PUBLIC_BASE_URL } else { "http://${HostIp}:${FrontendPort}" }
$GatewayCorsOrigins = if ($env:GATEWAY_CORS_ORIGINS) {
$env:GATEWAY_CORS_ORIGINS
} else {
"http://localhost:${FrontendPort},http://127.0.0.1:${FrontendPort},http://${HostIp}:${FrontendPort},http://localhost:8080,http://127.0.0.1:8080"
}
$BackendPython = Join-Path $BackendDir ".venv\Scripts\python.exe"
$BackendVendorDir = if ($env:BACKEND_VENDOR_DIR) { $env:BACKEND_VENDOR_DIR } else { Join-Path $BackendDir ".deer-flow\python-vendor" }
New-Item -ItemType Directory -Force -Path $LogDir, $PidDir | Out-Null
function Test-PidRunning {
param([string]$PidFile)
if (-not (Test-Path -LiteralPath $PidFile)) { return $false }
$rawPid = (Get-Content -LiteralPath $PidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
$processId = 0
if (-not [int]::TryParse([string]$rawPid, [ref]$processId)) { return $false }
return [bool](Get-Process -Id $processId -ErrorAction SilentlyContinue)
}
function Stop-ProcessTree {
param([int]$ProcessId)
$children = Get-CimInstance Win32_Process -Filter "ParentProcessId=$ProcessId" -ErrorAction SilentlyContinue
foreach ($child in $children) {
Stop-ProcessTree -ProcessId ([int]$child.ProcessId)
}
$proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
if ($proc) {
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
}
}
function ConvertTo-PSLiteral {
param([string]$Value)
return "'" + ($Value -replace "'", "''") + "'"
}
+4
View File
@@ -0,0 +1,4 @@
. "$PSScriptRoot\env.ps1"
& "$PSScriptRoot\stop-all.ps1"
& "$PSScriptRoot\start-all.ps1"
+21
View File
@@ -0,0 +1,21 @@
. "$PSScriptRoot\env.ps1"
& "$PSScriptRoot\start-all.ps1"
$deadline = (Get-Date).AddSeconds(60)
$url = "$FrontendPublicBaseUrl/"
Write-Host "Waiting for frontend: $url"
while ((Get-Date) -lt $deadline) {
try {
$response = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 2
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
Write-Host "Frontend is ready: $url"
exit 0
}
} catch {
Start-Sleep -Milliseconds 500
}
}
throw "Timed out waiting for frontend: $url"
+4
View File
@@ -0,0 +1,4 @@
. "$PSScriptRoot\env.ps1"
& "$PSScriptRoot\start-backend.ps1"
& "$PSScriptRoot\start-frontend.ps1"
+42
View File
@@ -0,0 +1,42 @@
. "$PSScriptRoot\env.ps1"
$PidFile = Join-Path $PidDir "backend.pid"
$LogFile = Join-Path $LogDir "backend.log"
if (Test-PidRunning $PidFile) {
$existingPid = Get-Content -LiteralPath $PidFile | Select-Object -First 1
Write-Host "Backend is already running (pid $existingPid)."
Write-Host "URL: $BackendPublicBaseUrl"
exit 0
}
if (-not (Test-Path -LiteralPath $BackendPython)) {
throw "Backend Python not found: $BackendPython. Run uv sync in offline-backend-20260512/backend first."
}
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $BackendVendorDir | Out-Null
Write-Host "Starting backend..."
Write-Host "Log: $LogFile"
$RunnerFile = Join-Path $RuntimeDir "run-backend.ps1"
$runner = @"
`$ErrorActionPreference = "Continue"
Set-Location -LiteralPath $(ConvertTo-PSLiteral $BackendDir)
`$env:GATEWAY_CORS_ORIGINS = $(ConvertTo-PSLiteral $GatewayCorsOrigins)
`$env:PYTHONPATH = $(ConvertTo-PSLiteral ".;$BackendVendorDir")
if (-not `$env:WEKNORA_ADMIN_PASSWORD) { `$env:WEKNORA_ADMIN_PASSWORD = "1qaz@WSX" }
cmd.exe /c "`"$(($BackendPython -replace '"', '\"'))`" -m uvicorn app.gateway.app:app --host $(($BackendHost -replace '"', '\"')) --port $(($BackendPort -replace '"', '\"')) --reload > `"$(($LogFile -replace '"', '\"'))`" 2>&1"
"@
Set-Content -LiteralPath $RunnerFile -Value $runner -Encoding UTF8
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $RunnerFile
) -WindowStyle Hidden -PassThru
Set-Content -LiteralPath $PidFile -Value $proc.Id
Write-Host "Backend started (pid $($proc.Id))."
Write-Host "URL: $BackendPublicBaseUrl"
+41
View File
@@ -0,0 +1,41 @@
. "$PSScriptRoot\env.ps1"
$PidFile = Join-Path $PidDir "frontend.pid"
$LogFile = Join-Path $LogDir "frontend.log"
if (Test-PidRunning $PidFile) {
$existingPid = Get-Content -LiteralPath $PidFile | Select-Object -First 1
Write-Host "Frontend is already running (pid $existingPid)."
Write-Host "URL: $FrontendPublicBaseUrl"
exit 0
}
$NpmCmd = (Get-Command npm.cmd -ErrorAction SilentlyContinue).Source
if (-not $NpmCmd) {
throw "npm.cmd was not found in PATH. Install Node.js or add npm.cmd to PATH."
}
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
Write-Host "Starting frontend..."
Write-Host "Log: $LogFile"
$RunnerFile = Join-Path $RuntimeDir "run-frontend.ps1"
$runner = @"
`$ErrorActionPreference = "Continue"
Set-Location -LiteralPath $(ConvertTo-PSLiteral $FrontendDir)
`$env:VITE_BACKEND_BASE_URL = $(ConvertTo-PSLiteral $BackendPublicBaseUrl)
`$env:VITE_LANGGRAPH_BASE_URL = $(ConvertTo-PSLiteral "$BackendPublicBaseUrl/api")
& $(ConvertTo-PSLiteral $NpmCmd) run dev -- --host $(ConvertTo-PSLiteral $FrontendHost) --port $(ConvertTo-PSLiteral $FrontendPort) *> $(ConvertTo-PSLiteral $LogFile)
"@
Set-Content -LiteralPath $RunnerFile -Value $runner -Encoding UTF8
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $RunnerFile
) -WindowStyle Hidden -PassThru
Set-Content -LiteralPath $PidFile -Value $proc.Id
Write-Host "Frontend started (pid $($proc.Id))."
Write-Host "URL: $FrontendPublicBaseUrl"
+18
View File
@@ -0,0 +1,18 @@
. "$PSScriptRoot\env.ps1"
$backendPidFile = Join-Path $PidDir "backend.pid"
$frontendPidFile = Join-Path $PidDir "frontend.pid"
if (Test-PidRunning $backendPidFile) {
$pidValue = Get-Content -LiteralPath $backendPidFile | Select-Object -First 1
Write-Host "Backend: running (pid $pidValue) $BackendPublicBaseUrl"
} else {
Write-Host "Backend: stopped"
}
if (Test-PidRunning $frontendPidFile) {
$pidValue = Get-Content -LiteralPath $frontendPidFile | Select-Object -First 1
Write-Host "Frontend: running (pid $pidValue) $FrontendPublicBaseUrl"
} else {
Write-Host "Frontend: stopped"
}
+4
View File
@@ -0,0 +1,4 @@
. "$PSScriptRoot\env.ps1"
& "$PSScriptRoot\stop-frontend.ps1"
& "$PSScriptRoot\stop-backend.ps1"
+20
View File
@@ -0,0 +1,20 @@
. "$PSScriptRoot\env.ps1"
$PidFile = Join-Path $PidDir "backend.pid"
if (-not (Test-Path -LiteralPath $PidFile)) {
Write-Host "Backend is not running (no pid file)."
exit 0
}
$rawPid = Get-Content -LiteralPath $PidFile | Select-Object -First 1
$processId = 0
if (-not [int]::TryParse([string]$rawPid, [ref]$processId) -or -not (Get-Process -Id $processId -ErrorAction SilentlyContinue)) {
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
Write-Host "Backend is not running (stale pid file removed)."
exit 0
}
Write-Host "Stopping backend (pid $processId)..."
Stop-ProcessTree -ProcessId $processId
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
Write-Host "Backend stopped."
+20
View File
@@ -0,0 +1,20 @@
. "$PSScriptRoot\env.ps1"
$PidFile = Join-Path $PidDir "frontend.pid"
if (-not (Test-Path -LiteralPath $PidFile)) {
Write-Host "Frontend is not running (no pid file)."
exit 0
}
$rawPid = Get-Content -LiteralPath $PidFile | Select-Object -First 1
$processId = 0
if (-not [int]::TryParse([string]$rawPid, [ref]$processId) -or -not (Get-Process -Id $processId -ErrorAction SilentlyContinue)) {
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
Write-Host "Frontend is not running (stale pid file removed)."
exit 0
}
Write-Host "Stopping frontend (pid $processId)..."
Stop-ProcessTree -ProcessId $processId
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
Write-Host "Frontend stopped."