Merge branch 'likun' into master

This commit is contained in:
吉鑫升 committed 2026-05-13 17:02:22 +08:00
commit faca2c9a2d
21 files changed
+617 -19

No files matched your search

+6
View File
@@ -2,9 +2,13 @@
.vscode/ .vscode/
frontend-web/.vscode/ frontend-web/.vscode/
# Local runtime logs and pid files
.runtime/
# Frontend local/runtime artifacts # Frontend local/runtime artifacts
frontend-web/.env frontend-web/.env
frontend-web/node_modules/ frontend-web/node_modules/
frontend-web/.pnpm-store/
frontend-web/tsconfig.tsbuildinfo frontend-web/tsconfig.tsbuildinfo
frontend-web/vite-dev.log frontend-web/vite-dev.log
frontend-web/vite-dev.err.log frontend-web/vite-dev.err.log
@@ -51,6 +55,8 @@ offline-backend-20260512/deerflow-backend-offline-image-20260505.tar
offline-backend-20260512/extensions_config.json offline-backend-20260512/extensions_config.json
offline-backend-20260512/skills/ offline-backend-20260512/skills/
offline-backend-20260512/wheelhouse/ offline-backend-20260512/wheelhouse/
offline-backend-20260512/backend/.deer-flow/data/*.db
offline-backend-20260512/backend/.deer-flow/data/*.db-*
offline-backend-20260512/start_backend.sh offline-backend-20260512/start_backend.sh
offline-backend-20260512/stop_backend.sh offline-backend-20260512/stop_backend.sh
offline-backend-20260512/check_backend.sh offline-backend-20260512/check_backend.sh
@@ -1,9 +1,13 @@
import type { BaseStream } from "@langchain/langgraph-sdk/react"; import type { BaseStream } from "@langchain/langgraph-sdk/react";
import { SendIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { import {
Conversation, Conversation,
ConversationContent, ConversationContent,
} from "@/components/ai-elements/conversation"; } from "@/components/ai-elements/conversation";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { import {
extractContentFromMessage, extractContentFromMessage,
@@ -40,12 +44,14 @@ export function MessageList({
thread, thread,
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM, paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
tokenUsageEnabled = false, tokenUsageEnabled = false,
onClarificationSubmit,
}: { }: {
className?: string; className?: string;
threadId: string; threadId: string;
thread: BaseStream<AgentThreadState>; thread: BaseStream<AgentThreadState>;
paddingBottom?: number; paddingBottom?: number;
tokenUsageEnabled?: boolean; tokenUsageEnabled?: boolean;
onClarificationSubmit?: (answer: string) => void | Promise<void>;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading); const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
@@ -77,9 +83,11 @@ export function MessageList({
if (message && hasContent(message)) { if (message && hasContent(message)) {
return ( return (
<div key={group.id} className="w-full"> <div key={group.id} className="w-full">
<MarkdownContent <ClarificationCard
content={extractContentFromMessage(message)} fallbackContent={extractContentFromMessage(message)}
isLoading={thread.isLoading} isLoading={thread.isLoading}
message={message}
onSubmit={onClarificationSubmit}
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}
/> />
<MessageTokenUsageList <MessageTokenUsageList
@@ -240,3 +248,148 @@ export function MessageList({
</Conversation> </Conversation>
); );
} }
type ClarificationOption = {
id: string;
label: string;
description?: string;
};
type ClarificationPayload = {
question?: string;
context?: string | null;
clarification_type?: string;
options?: ClarificationOption[];
allow_custom?: boolean;
};
function getClarificationPayload(message: unknown): ClarificationPayload | null {
if (typeof message !== "object" || message === null) {
return null;
}
const additionalKwargs = Reflect.get(message, "additional_kwargs");
if (typeof additionalKwargs !== "object" || additionalKwargs === null) {
return null;
}
const clarification = Reflect.get(additionalKwargs, "clarification");
if (typeof clarification !== "object" || clarification === null) {
return null;
}
return clarification as ClarificationPayload;
}
function ClarificationCard({
fallbackContent,
isLoading,
message,
onSubmit,
rehypePlugins,
}: {
fallbackContent: string;
isLoading: boolean;
message: unknown;
onSubmit?: (answer: string) => void | Promise<void>;
rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
}) {
const payload = getClarificationPayload(message);
const [customAnswer, setCustomAnswer] = useState("");
const [submitting, setSubmitting] = useState(false);
const options = useMemo(
() =>
(payload?.options ?? []).filter(
(option) => typeof option.label === "string" && option.label.trim(),
),
[payload?.options],
);
if (!payload) {
return (
<MarkdownContent
content={fallbackContent}
isLoading={isLoading}
rehypePlugins={rehypePlugins}
/>
);
}
const disabled = isLoading || submitting || !onSubmit;
async function submitAnswer(answer: string) {
const trimmed = answer.trim();
if (!trimmed || disabled) {
return;
}
setSubmitting(true);
try {
await onSubmit?.(trimmed);
} finally {
setSubmitting(false);
}
}
return (
<div className="border-border bg-card text-card-foreground w-full rounded-lg border p-4 shadow-sm">
<div className="space-y-2">
<div className="text-muted-foreground text-xs font-medium">
需要你的确认
</div>
{payload.context ? (
<p className="text-muted-foreground text-sm leading-6">
{payload.context}
</p>
) : null}
<div className="text-sm leading-6 font-medium">
{payload.question || fallbackContent}
</div>
</div>
{options.length > 0 ? (
<div className="mt-4 grid gap-2">
{options.map((option) => (
<Button
key={option.id || option.label}
type="button"
variant="outline"
className="h-auto justify-start whitespace-normal px-3 py-2 text-left"
disabled={disabled}
onClick={() => void submitAnswer(`我选择:${option.label}`)}
>
<span className="flex min-w-0 flex-col items-start gap-1">
<span>{option.label}</span>
{option.description ? (
<span className="text-muted-foreground text-xs font-normal leading-5">
{option.description}
</span>
) : null}
</span>
</Button>
))}
</div>
) : null}
{payload.allow_custom !== false ? (
<div className="mt-4 flex flex-col gap-2">
<Textarea
value={customAnswer}
onChange={(event) => setCustomAnswer(event.currentTarget.value)}
placeholder="如果以上选项都不合适,可以在这里输入你的回答"
disabled={disabled}
className="min-h-20 resize-none"
/>
<div className="flex justify-end">
<Button
type="button"
size="sm"
disabled={disabled || !customAnswer.trim()}
onClick={() => void submitAnswer(customAnswer)}
>
<SendIcon className="h-4 w-4" />
发送回答
</Button>
</div>
</div>
) : null}
</div>
);
}
+9
View File
@@ -98,6 +98,14 @@ export default function AgentChatPage() {
[sendMessage, threadId, agent_id], [sendMessage, threadId, agent_id],
); );
const handleClarificationSubmit = useCallback(
async (answer: string) => {
setHasStartedConversation(true);
await sendMessage(threadId, { text: answer, files: [] }, { agent_id });
},
[sendMessage, threadId, agent_id],
);
const handleStop = useCallback(async () => { const handleStop = useCallback(async () => {
await thread.stop(); await thread.stop();
}, [thread]); }, [thread]);
@@ -160,6 +168,7 @@ export default function AgentChatPage() {
thread={thread} thread={thread}
paddingBottom={messageListPaddingBottom} paddingBottom={messageListPaddingBottom}
tokenUsageEnabled={tokenUsageEnabled} tokenUsageEnabled={tokenUsageEnabled}
onClarificationSubmit={handleClarificationSubmit}
/> />
</div> </div>
+8
View File
@@ -95,6 +95,13 @@ export default function ChatPage() {
}, },
[sendMessage, threadId], [sendMessage, threadId],
); );
const handleClarificationSubmit = useCallback(
async (answer: string) => {
setHasStartedConversation(true);
await sendMessage(threadId, { text: answer, files: [] });
},
[sendMessage, threadId],
);
const handleStop = useCallback(async () => { const handleStop = useCallback(async () => {
await thread.stop(); await thread.stop();
}, [thread]); }, [thread]);
@@ -137,6 +144,7 @@ export default function ChatPage() {
thread={thread} thread={thread}
paddingBottom={messageListPaddingBottom} paddingBottom={messageListPaddingBottom}
tokenUsageEnabled={tokenUsageEnabled} tokenUsageEnabled={tokenUsageEnabled}
onClarificationSubmit={handleClarificationSubmit}
/> />
</div> </div>
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4"> <div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
+1
View File
@@ -465,6 +465,7 @@ export default function NewAgentPage() {
className={cn("size-full", showSaveHint ? "pt-4" : "pt-10")} className={cn("size-full", showSaveHint ? "pt-4" : "pt-10")}
threadId={threadId} threadId={threadId}
thread={thread} thread={thread}
onClarificationSubmit={handleChatSubmit}
/> />
</div> </div>
@@ -13,6 +13,7 @@ static/browser_history/*.gif
log/ log/
log/* log/*
.deer-flow/users/
# Virtual environments # Virtual environments
.venv .venv
@@ -442,6 +442,8 @@ You are {agent_name}, an open-source super agent.
- ✅ If you identify the need for clarification in your thinking, you MUST call the tool IMMEDIATELY - ✅ If you identify the need for clarification in your thinking, you MUST call the tool IMMEDIATELY
- ✅ After calling ask_clarification, execution will be interrupted automatically - ✅ After calling ask_clarification, execution will be interrupted automatically
- ✅ Wait for user response - do NOT continue with assumptions - ✅ Wait for user response - do NOT continue with assumptions
- ✅ When practical, provide 3-4 clear, mutually distinct options so the user can choose quickly
- ✅ Keep options concise and actionable; the user can still provide a custom answer if none fit
**How to Use:** **How to Use:**
```python ```python
@@ -449,7 +451,7 @@ ask_clarification(
question="Your specific question here?", question="Your specific question here?",
clarification_type="missing_info", # or other type clarification_type="missing_info", # or other type
context="Why you need this information", # optional but recommended context="Why you need this information", # optional but recommended
options=["option1", "option2"] # optional, for choices options=["option1", "option2", "option3"] # use 3-4 options when practical
) )
``` ```
@@ -67,21 +67,7 @@ class ClarificationMiddleware(AgentMiddleware[ClarificationMiddlewareState]):
question = args.get("question", "") question = args.get("question", "")
clarification_type = args.get("clarification_type", "missing_info") clarification_type = args.get("clarification_type", "missing_info")
context = args.get("context") context = args.get("context")
options = args.get("options", []) options = self._normalize_options(args.get("options", []))
# Some models (e.g. Qwen3-Max) serialize array parameters as JSON strings
# instead of native arrays. Deserialize and normalize so `options`
# is always a list for the rendering logic below.
if isinstance(options, str):
try:
options = json.loads(options)
except (json.JSONDecodeError, TypeError):
options = [options]
if options is None:
options = []
elif not isinstance(options, list):
options = [options]
# Type-specific icons # Type-specific icons
type_icons = { type_icons = {
@@ -110,10 +96,62 @@ class ClarificationMiddleware(AgentMiddleware[ClarificationMiddlewareState]):
if options and len(options) > 0: if options and len(options) > 0:
message_parts.append("") # blank line for spacing message_parts.append("") # blank line for spacing
for i, option in enumerate(options, 1): for i, option in enumerate(options, 1):
message_parts.append(f" {i}. {option}") label = option.get("label") or option.get("id") or ""
message_parts.append(f" {i}. {label}")
return "\n".join(message_parts) return "\n".join(message_parts)
def _normalize_options(self, raw_options: object) -> list[dict[str, str]]:
"""Normalize model-produced options into a stable UI-friendly shape."""
options = raw_options
# Some models serialize array parameters as JSON strings instead of native arrays.
if isinstance(options, str):
try:
options = json.loads(options)
except (json.JSONDecodeError, TypeError):
options = [options]
if options is None:
values: list[object] = []
elif isinstance(options, list):
values = options
else:
values = [options]
normalized: list[dict[str, str]] = []
for index, value in enumerate(values, 1):
if isinstance(value, dict):
raw_label = value.get("label") or value.get("title") or value.get("name") or value.get("value") or value.get("id")
label = str(raw_label).strip() if raw_label is not None else ""
if not label:
continue
raw_id = value.get("id") or value.get("value") or f"option-{index}"
option = {
"id": str(raw_id).strip() or f"option-{index}",
"label": label,
}
description = value.get("description")
if description is not None and str(description).strip():
option["description"] = str(description).strip()
normalized.append(option)
else:
label = str(value).strip()
if label:
normalized.append({"id": f"option-{index}", "label": label})
return normalized
def _build_payload(self, args: dict) -> dict:
"""Build structured clarification data for rich clients."""
return {
"question": str(args.get("question", "")).strip(),
"clarification_type": args.get("clarification_type", "missing_info"),
"context": args.get("context"),
"options": self._normalize_options(args.get("options", [])),
"allow_custom": True,
}
def _handle_clarification(self, request: ToolCallRequest) -> Command: def _handle_clarification(self, request: ToolCallRequest) -> Command:
"""Handle clarification request and return command to interrupt execution. """Handle clarification request and return command to interrupt execution.
@@ -143,6 +181,7 @@ class ClarificationMiddleware(AgentMiddleware[ClarificationMiddlewareState]):
content=formatted_message, content=formatted_message,
tool_call_id=tool_call_id, tool_call_id=tool_call_id,
name="ask_clarification", name="ask_clarification",
additional_kwargs={"clarification": self._build_payload(args)},
) )
# Return a Command that: # Return a Command that:
@@ -0,0 +1,135 @@
---
name: intent-clarification-policy
description: Use this skill to analyze a higher-level task directive before planning. It helps infer the superior intent, assess whether the task is clear enough to proceed, and ask concise clarification questions when key purpose, audience, effect, boundary, coordination, or evaluation details are missing.
---
# Intent Clarification Policy
Use this skill before drafting a plan for a higher-level task directive, action requirement, special work arrangement, cognitive-domain task, public opinion/information/psychological/influence-related task, or cross-department mission.
This skill does not draft the full plan. It prepares the intent analysis and decides whether clarification is needed first.
## Workflow
1. Restate the task briefly to avoid misreading.
2. Assess clarity across seven dimensions.
3. Assign a clarity level: A, B, C, or D.
4. If level A or B, produce a concise intent analysis.
5. If level C or D, call `ask_clarification` before planning.
## Clarity Dimensions
Check whether the task clearly states:
- Goal: what higher-level purpose or end state is expected.
- Object: who or what the task is directed toward.
- Effect: what cognitive, attitude, behavior, posture, or environment change is desired.
- Time window: when results are expected.
- Boundary: legal, policy, ethical, authorization, escalation, geographic, and resource limits.
- Coordination: who leads, supports, approves, or assesses.
- Evaluation: how completion and effect will be judged.
## Clarity Levels
- **A - Highly clear**: goal, object, effect, time, and boundary are mostly clear. Proceed to intent analysis.
- **B - Basically clear**: core purpose is clear, but some boundary or metric details are missing. Provide tentative analysis and list a few follow-up questions.
- **C - Clearly vague**: only a broad direction is given; object, effect, or boundary is missing. Ask clarification first.
- **D - Not enough context**: original task text or background is absent. Ask the user to provide the task material.
## Intent Analysis Mode
Use this mode for level A or B tasks.
Start with:
```markdown
上级任务可概括为:
在【时间/阶段】内,围绕【背景/事件】,面向【目标对象】,通过【主要行动方向】,达成【期望效果】,服务于【上级目标】。
```
Then analyze only the points supported by the task text:
- Superior purpose: why the task was assigned and what higher objective it serves.
- Task nature: shaping, deterrence, stabilization, protection, weakening, exposure, support, or assessment.
- Target objects: primary objects, secondary objects, protected groups, and possible unintended affected groups.
- Expected effects: desired changes in cognition, attitude, judgment, behavior, posture, or environment.
- Action boundaries: authorization, policy, legal, ethical, timing, geography, and resource constraints.
- Evaluation method: indicators, feedback channels, acceptance criteria, or effect assessment.
If the task is cognitive-domain related, identify one or more task types:
- Shaping: influence the narrative or perception environment in advance.
- Deterrence: affect an opponent's cost-benefit judgment.
- Stabilization: reduce panic, misunderstanding, uncertainty, or loss of trust.
- Protection: resist false information, psychological pressure, or cognitive manipulation.
- Weakening: reduce an opponent's credibility, mobilization, or influence capacity.
- Exposure: reveal facts, attribute responsibility, or strengthen credibility.
- Support: support military, diplomatic, public affairs, cyber, or civil action.
- Assessment: monitor the cognitive environment or action effect.
Summarize inferred intent with:
```markdown
## 上级意图推断
上级希望在【时间窗口】内,围绕【事件/议题/背景】,通过【本级行动方向】,影响【主要对象】对【关键问题】的理解、判断或行为,使其从【当前状态】转向【期望状态】,从而支撑【上级战略/战役/行动/业务目标】。
本任务的核心不是单纯完成【具体动作】,而是通过该动作形成【认知效果/行为效果/环境效果】。
```
## Clarification Mode
Use this mode for level C or D tasks. Do not draft a full plan yet.
Diagnose missing information in these categories:
- Purpose unclear: higher-level objective or desired end state is missing.
- Object unclear: primary audience, affected group, area, platform, or stakeholder is missing.
- Effect unclear: expected cognitive, attitude, behavior, or environment change is missing.
- Means unclear: allowed and prohibited action methods are missing.
- Boundary unclear: legal, policy, ethical, authorization, or escalation limits are missing.
- Coordination unclear: lead, support, approval, and assessment roles are missing.
- Evaluation unclear: completion criteria and effect metrics are missing.
Ask the minimum number of questions needed to proceed. Prefer one main question with 2-4 options. If several key fields are missing, ask for the most blocking one first.
Use `ask_clarification`:
```python
ask_clarification(
question="一个用户可以快速回答的具体问题。",
clarification_type="missing_info",
context="简要说明为什么这个信息会影响后续计划拟制。",
options=["选项一", "选项二", "选项三"],
)
```
Choose `clarification_type`:
- `missing_info`: required task material or key detail is absent.
- `ambiguous_requirement`: the directive has multiple plausible interpretations.
- `approach_choice`: several valid task directions exist and user preference matters.
- `risk_confirmation`: the action may be sensitive, irreversible, escalatory, or policy-relevant.
- `suggestion`: you recommend a framing and need user approval.
## High-Value Questions
Use or adapt these questions when needed:
- 上级下发该任务主要服务于什么目标?
- 任务完成后,上级希望看到什么状态变化?
- 本任务主要面向哪些对象?
- 是否有不能影响、不能触碰或需要特别保护的对象?
- 希望目标对象在认知、态度、判断或行为上发生什么变化?
- 该效果是短期即时效果,还是长期环境塑造效果?
- 本级在任务中是主责、支援、协同、评估,还是审批角色?
- 有哪些法律、政策、授权、伦理或升级风险边界?
- 如何判断任务完成,如何评估实际效果?
## Style Rules
- Use Chinese when the user writes Chinese.
- Be concise and structured.
- Do not invent unsupported intent; label uncertain points as assumptions.
- Do not proceed to full planning when clarity is C or D.
- For sensitive influence, public opinion, psychological, or cognitive-domain work, keep analysis lawful, ethical, defensive, and policy-bounded.
+2
View File
@@ -745,6 +745,7 @@ version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "bcrypt" }, { name = "bcrypt" },
{ name = "croniter" },
{ name = "deerflow-harness" }, { name = "deerflow-harness" },
{ name = "dingtalk-stream" }, { name = "dingtalk-stream" },
{ name = "email-validator" }, { name = "email-validator" },
@@ -778,6 +779,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "bcrypt", specifier = ">=4.0.0" }, { name = "bcrypt", specifier = ">=4.0.0" },
{ name = "croniter", specifier = ">=6.2.2" },
{ name = "deerflow-harness", editable = "packages/harness" }, { name = "deerflow-harness", editable = "packages/harness" },
{ name = "deerflow-harness", extras = ["postgres"], marker = "extra == 'postgres'", editable = "packages/harness" }, { name = "deerflow-harness", extras = ["postgres"], marker = "extra == 'postgres'", editable = "packages/harness" },
{ name = "dingtalk-stream", specifier = ">=0.24.3" }, { name = "dingtalk-stream", specifier = ">=0.24.3" },
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BACKEND_DIR="${ROOT_DIR}/offline-backend-20260512/backend"
FRONTEND_DIR="${ROOT_DIR}/frontend-web"
RUNTIME_DIR="${ROOT_DIR}/.runtime"
LOG_DIR="${RUNTIME_DIR}/logs"
PID_DIR="${RUNTIME_DIR}/pids"
BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}"
BACKEND_PORT="${BACKEND_PORT:-8001}"
FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}"
FRONTEND_PORT="${FRONTEND_PORT:-5174}"
detect_host_ip() {
if command -v hostname >/dev/null 2>&1; then
hostname -I 2>/dev/null | awk '{print $1}'
fi
}
HOST_IP="${HOST_IP:-$(detect_host_ip)}"
HOST_IP="${HOST_IP:-127.0.0.1}"
BACKEND_PUBLIC_BASE_URL="${BACKEND_PUBLIC_BASE_URL:-http://${HOST_IP}:${BACKEND_PORT}}"
FRONTEND_PUBLIC_BASE_URL="${FRONTEND_PUBLIC_BASE_URL:-http://${HOST_IP}:${FRONTEND_PORT}}"
GATEWAY_CORS_ORIGINS="${GATEWAY_CORS_ORIGINS:-http://localhost:${FRONTEND_PORT},http://127.0.0.1:${FRONTEND_PORT},http://${HOST_IP}:${FRONTEND_PORT}}"
mkdir -p "${LOG_DIR}" "${PID_DIR}"
is_running() {
local pid_file="$1"
[[ -f "${pid_file}" ]] || return 1
local pid
pid="$(cat "${pid_file}" 2>/dev/null || true)"
[[ -n "${pid}" ]] || return 1
kill -0 "${pid}" >/dev/null 2>&1
}
stop_by_pid_file() {
local name="$1"
local pid_file="$2"
if ! [[ -f "${pid_file}" ]]; then
echo "${name} is not running (no pid file)."
return 0
fi
local pid
pid="$(cat "${pid_file}" 2>/dev/null || true)"
if [[ -z "${pid}" ]] || ! kill -0 "${pid}" >/dev/null 2>&1; then
echo "${name} is not running (stale pid file removed)."
rm -f "${pid_file}"
return 0
fi
echo "Stopping ${name} (pid ${pid})..."
# Services are started with setsid, so the pid is also the process group id.
# Stop the whole group to avoid leaving Vite/esbuild/uvicorn child processes.
kill "-${pid}" >/dev/null 2>&1 || true
kill "${pid}" >/dev/null 2>&1 || true
for _ in {1..30}; do
if ! kill -0 "${pid}" >/dev/null 2>&1; then
rm -f "${pid_file}"
echo "${name} stopped."
return 0
fi
sleep 0.2
done
echo "${name} did not stop gracefully; killing..."
kill -9 "-${pid}" >/dev/null 2>&1 || true
kill -9 "${pid}" >/dev/null 2>&1 || true
rm -f "${pid_file}"
echo "${name} stopped."
}
stop_by_pattern() {
local name="$1"
local pattern="$2"
if ! command -v pgrep >/dev/null 2>&1; then
return 0
fi
local pids
pids="$(pgrep -f "${pattern}" || true)"
if [[ -z "${pids}" ]]; then
return 0
fi
echo "Stopping ${name} processes matching: ${pattern}"
while IFS= read -r pid; do
[[ -n "${pid}" ]] || continue
[[ "${pid}" != "$$" ]] || continue
kill "${pid}" >/dev/null 2>&1 || true
done <<<"${pids}"
}
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/stop-all.sh"
"${SCRIPT_DIR}/start-all.sh"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/stop-backend.sh"
"${SCRIPT_DIR}/start-backend.sh"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/stop-frontend.sh"
"${SCRIPT_DIR}/start-frontend.sh"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/start-backend.sh"
"${SCRIPT_DIR}/start-frontend.sh"
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
PID_FILE="${PID_DIR}/backend.pid"
LOG_FILE="${LOG_DIR}/backend.log"
if is_running "${PID_FILE}"; then
echo "Backend is already running (pid $(cat "${PID_FILE}"))."
echo "URL: http://${HOST_IP}:${BACKEND_PORT}"
exit 0
fi
rm -f "${PID_FILE}"
echo "Starting backend..."
echo "Log: ${LOG_FILE}"
setsid bash -c '
cd "$0" || exit 1
exec env \
GATEWAY_CORS_ORIGINS="$1" \
PYTHONPATH=. \
uv run uvicorn app.gateway.app:app \
--host "$2" \
--port "$3" \
--reload
' "${BACKEND_DIR}" "${GATEWAY_CORS_ORIGINS}" "${BACKEND_HOST}" "${BACKEND_PORT}" \
>"${LOG_FILE}" 2>&1 &
echo "$!" >"${PID_FILE}"
echo "Backend started (pid $(cat "${PID_FILE}"))."
echo "URL: http://${HOST_IP}:${BACKEND_PORT}"
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
PID_FILE="${PID_DIR}/frontend.pid"
LOG_FILE="${LOG_DIR}/frontend.log"
if is_running "${PID_FILE}"; then
echo "Frontend is already running (pid $(cat "${PID_FILE}"))."
echo "URL: ${FRONTEND_PUBLIC_BASE_URL}"
exit 0
fi
rm -f "${PID_FILE}"
echo "Starting frontend..."
echo "Log: ${LOG_FILE}"
setsid bash -c '
cd "$0" || exit 1
exec env \
VITE_BACKEND_BASE_URL="$1" \
VITE_LANGGRAPH_BASE_URL="$1/api" \
pnpm exec vite \
--host "$2" \
--port "$3"
' "${FRONTEND_DIR}" "${BACKEND_PUBLIC_BASE_URL}" "${FRONTEND_HOST}" "${FRONTEND_PORT}" \
>"${LOG_FILE}" 2>&1 &
echo "$!" >"${PID_FILE}"
echo "Frontend started (pid $(cat "${PID_FILE}"))."
echo "URL: ${FRONTEND_PUBLIC_BASE_URL}"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
print_status() {
local name="$1"
local pid_file="$2"
local url="$3"
local pattern="$4"
if is_running "${pid_file}"; then
echo "${name}: running (pid $(cat "${pid_file}"))"
echo " ${url}"
elif command -v pgrep >/dev/null 2>&1 && pgrep -f "${pattern}" >/dev/null 2>&1; then
echo "${name}: running (unmanaged by scripts)"
echo " ${url}"
else
echo "${name}: stopped"
fi
}
print_status "Backend" "${PID_DIR}/backend.pid" "http://${HOST_IP}:${BACKEND_PORT}" "${BACKEND_DIR}.*uvicorn app.gateway.app:app.*--port ${BACKEND_PORT}"
print_status "Frontend" "${PID_DIR}/frontend.pid" "${FRONTEND_PUBLIC_BASE_URL}" "${FRONTEND_DIR}.*vite.*--port ${FRONTEND_PORT}"
echo "Logs: ${LOG_DIR}"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/stop-frontend.sh"
"${SCRIPT_DIR}/stop-backend.sh"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
stop_by_pid_file "Backend" "${PID_DIR}/backend.pid"
stop_by_pattern "Backend" "${BACKEND_DIR}.*uvicorn app.gateway.app:app.*--port ${BACKEND_PORT}"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
stop_by_pid_file "Frontend" "${PID_DIR}/frontend.pid"
stop_by_pattern "Frontend" "${FRONTEND_DIR}.*vite.*--port ${FRONTEND_PORT}"