chore: update ji local changes

- frontend: scheduled tasks pages, system-settings api, workspace nav menu
- backend: system_settings router, app_config, log_system_config, config.yaml

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-04 09:18:32 +08:00
1 parent aeef1dca59
commit 925675ab54
11 files changed
+172 -34

No files matched your search

@@ -10,6 +10,7 @@ import {
HistoryIcon, HistoryIcon,
MessageSquareCodeIcon, MessageSquareCodeIcon,
MessageSquarePlusIcon, MessageSquarePlusIcon,
ScrollTextIcon,
Settings2Icon, Settings2Icon,
SettingsIcon, SettingsIcon,
ShieldCheckIcon, ShieldCheckIcon,
@@ -36,6 +37,10 @@ import {
import { getStoredAuth } from "@/core/auth"; import { getStoredAuth } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { useUnreadCount } from "@/core/notifications"; import { useUnreadCount } from "@/core/notifications";
import {
loadLogSystemSettings,
type LogSystemSettings,
} from "@/core/system-settings/api";
import { NotificationDialog } from "./notification-dialog"; import { NotificationDialog } from "./notification-dialog";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -71,6 +76,7 @@ export function WorkspaceNavMenu() {
>("appearance"); >("appearance");
const [notificationOpen, setNotificationOpen] = useState(false); const [notificationOpen, setNotificationOpen] = useState(false);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [logSystem, setLogSystem] = useState<LogSystemSettings | null>(null);
const { open: isSidebarOpen } = useSidebar(); const { open: isSidebarOpen } = useSidebar();
const { t } = useI18n(); const { t } = useI18n();
const { data: unreadCount = 0 } = useUnreadCount(); const { data: unreadCount = 0 } = useUnreadCount();
@@ -79,6 +85,21 @@ export function WorkspaceNavMenu() {
setMounted(true); setMounted(true);
}, []); }, []);
useEffect(() => {
if (!isAdmin) return;
let cancelled = false;
loadLogSystemSettings()
.then((settings) => {
if (!cancelled) setLogSystem(settings);
})
.catch(() => {
if (!cancelled) setLogSystem(null);
});
return () => {
cancelled = true;
};
}, [isAdmin]);
return ( return (
<> <>
<SettingsDialog <SettingsDialog
@@ -208,6 +229,20 @@ export function WorkspaceNavMenu() {
{t.adminTags.title} {t.adminTags.title}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{isAdmin && logSystem?.enabled && logSystem.url && (
<DropdownMenuItem
onClick={() =>
window.open(
logSystem.url,
"_blank",
"noopener,noreferrer",
)
}
>
<ScrollTextIcon />
{logSystem.label || "日志系统"}
</DropdownMenuItem>
)}
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</DropdownMenuContent> </DropdownMenuContent>
@@ -147,6 +147,22 @@ export async function updateCitationDisplaySettings(
return (await response.json()) as CitationDisplaySettings; return (await response.json()) as CitationDisplaySettings;
} }
export interface LogSystemSettings {
enabled: boolean;
url: string;
label: string;
}
export async function loadLogSystemSettings(): Promise<LogSystemSettings> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/system-settings/log-system`,
);
if (!response.ok) {
throw new Error(`Failed to load log system settings: ${response.status}`);
}
return (await response.json()) as LogSystemSettings;
}
export async function updatePrivateSquareSettings( export async function updatePrivateSquareSettings(
body: PrivateSquareUpdate, body: PrivateSquareUpdate,
): Promise<PrivateSquareStatus> { ): Promise<PrivateSquareStatus> {
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { import {
Download, Download,
Edit3, Edit3,
@@ -71,6 +72,7 @@ const emptyForm = {
}; };
export default function AdminScheduledTasksPage() { export default function AdminScheduledTasksPage() {
const queryClient = useQueryClient();
const { theme } = useTheme(); const { theme } = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -178,8 +180,15 @@ export default function AdminScheduledTasksPage() {
}; };
const triggerTask = async (task: ScheduledTask) => { const triggerTask = async (task: ScheduledTask) => {
setSelectedTaskId(task.task_id);
try { try {
await actions.trigger.mutateAsync(task.task_id); await actions.trigger.mutateAsync(task.task_id);
await queryClient.invalidateQueries({
queryKey: ["scheduled-tasks", "admin", task.task_id, "runs"],
});
await queryClient.invalidateQueries({
queryKey: ["scheduled-tasks", "admin", "list"],
});
toast.success("已触发执行"); toast.success("已触发执行");
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "触发失败"); toast.error(error instanceof Error ? error.message : "触发失败");
@@ -208,23 +217,24 @@ export default function AdminScheduledTasksPage() {
<WorkspaceContainer> <WorkspaceContainer>
<WorkspaceHeader /> <WorkspaceHeader />
<WorkspaceBody> <WorkspaceBody>
<div className="grid size-full min-h-0 grid-cols-1 gap-4 p-4 xl:grid-cols-[460px_minmax(0,1fr)]"> <div className="grid size-full min-h-0 grid-cols-1 grid-rows-[minmax(0,1fr)_minmax(0,1fr)] gap-4 p-4 xl:grid-cols-[460px_minmax(0,1fr)] xl:grid-rows-1">
<Card className="flex min-h-0 flex-col"> <Card className="flex min-h-0 min-w-0 flex-col">
<CardHeader className="gap-3"> <CardHeader className="gap-3">
<div> <div className="min-w-0">
<CardTitle>定时任务管理</CardTitle> <CardTitle>定时任务管理</CardTitle>
<p className="text-muted-foreground mt-1 text-sm"> <p className="text-muted-foreground mt-1 text-sm">
管理员视图:查看、编辑、删除全部用户的定时任务 管理员视图:查看、编辑、删除全部用户的定时任务
</p> </p>
</div> </div>
<form <form
className="flex gap-2" className="flex min-w-0 gap-2"
onSubmit={(event) => { onSubmit={(event) => {
event.preventDefault(); event.preventDefault();
setSearch(searchInput.trim()); setSearch(searchInput.trim());
}} }}
> >
<Input <Input
className="min-w-0"
value={searchInput} value={searchInput}
onChange={(event) => setSearchInput(event.target.value)} onChange={(event) => setSearchInput(event.target.value)}
placeholder="按任务名称搜索" placeholder="按任务名称搜索"
@@ -268,7 +278,7 @@ export default function AdminScheduledTasksPage() {
onClick={() => setSelectedTaskId(task.task_id)} onClick={() => setSelectedTaskId(task.task_id)}
> >
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0 flex-1">
<div className="truncate font-medium"> <div className="truncate font-medium">
{task.name} {task.name}
</div> </div>
@@ -348,10 +358,10 @@ export default function AdminScheduledTasksPage() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="flex min-h-0 flex-col"> <Card className="flex min-h-0 min-w-0 flex-col">
<CardHeader className="gap-2"> <CardHeader className="gap-2">
<CardTitle>执行记录</CardTitle> <CardTitle>执行记录</CardTitle>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground min-w-0 truncate text-sm">
{selectedTask ? selectedTask.name : "请选择左侧任务"} {selectedTask ? selectedTask.name : "请选择左侧任务"}
</p> </p>
</CardHeader> </CardHeader>
@@ -379,11 +389,11 @@ export default function AdminScheduledTasksPage() {
className="rounded-md border p-4 transition-colors hover:bg-accent/40" className="rounded-md border p-4 transition-colors hover:bg-accent/40"
> >
<div className="flex flex-wrap items-start justify-between gap-3"> <div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0 flex-1">
<div className="truncate font-medium"> <div className="truncate font-medium">
{run.result?.title || formatDate(run.scheduled_for)} {run.result?.title || formatDate(run.scheduled_for)}
</div> </div>
<div className="text-muted-foreground mt-1 text-xs"> <div className="text-muted-foreground mt-1 break-words text-xs">
计划:{formatDate(run.scheduled_for)} · 开始: 计划:{formatDate(run.scheduled_for)} · 开始:
{formatDate(run.started_at)} {formatDate(run.started_at)}
</div> </div>
@@ -97,7 +97,7 @@ function RenderPreview({ file }: { file: ScheduledTaskResultFileContent }) {
/> />
); );
} }
return <pre className="whitespace-pre-wrap text-sm">{file.content}</pre>; return <pre className="whitespace-pre-wrap break-words text-sm">{file.content}</pre>;
} }
/** /**
@@ -162,9 +162,9 @@ function ExecutionMessage({ message }: { message: ScheduledTaskRunMessage }) {
const isAi = message.event_type === "llm.ai.response"; const isAi = message.event_type === "llm.ai.response";
return ( return (
<div className="rounded-md border"> <div className="min-w-0 rounded-md border">
<div className="flex items-center gap-2 border-b px-3 py-1.5"> <div className="flex min-w-0 flex-wrap items-center gap-2 border-b px-3 py-1.5">
<span className={cn("rounded px-1.5 py-0.5 text-xs font-medium", role.tone)}> <span className={cn("max-w-full break-all rounded px-1.5 py-0.5 text-xs font-medium", role.tone)}>
{role.label} {role.label}
{toolName ? ` · ${toolName}` : ""} {toolName ? ` · ${toolName}` : ""}
</span> </span>
@@ -428,9 +428,9 @@ export default function ScheduledTaskRunDetailPage() {
<WorkspaceHeader /> <WorkspaceHeader />
<WorkspaceBody> <WorkspaceBody>
<div className="flex size-full min-h-0 flex-col p-3"> <div className="flex size-full min-h-0 flex-col p-3">
<Card className="flex min-h-0 flex-1 flex-col"> <Card className="flex min-h-0 min-w-0 flex-1 flex-col">
{/* 工具栏 */} {/* 工具栏 */}
<div className="flex flex-wrap items-center gap-2 border-b px-3 py-2"> <div className="flex min-w-0 flex-wrap items-center gap-2 border-b px-3 py-2">
<div className="mr-auto flex min-w-0 items-center gap-2"> <div className="mr-auto flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium"> <span className="truncate text-sm font-medium">
{result?.title || (isLoading ? "正在加载..." : "执行结果详情")} {result?.title || (isLoading ? "正在加载..." : "执行结果详情")}
@@ -470,15 +470,15 @@ export default function ScheduledTaskRunDetailPage() {
{/* 执行信息(可折叠) */} {/* 执行信息(可折叠) */}
{showMeta && run && ( {showMeta && run && (
<div className="border-b px-3 py-2"> <div className="border-b px-3 py-2">
<div className="text-muted-foreground grid gap-x-4 gap-y-1 text-xs md:grid-cols-3 xl:grid-cols-5"> <div className="text-muted-foreground grid min-w-0 gap-x-4 gap-y-1 text-xs md:grid-cols-3 xl:grid-cols-5">
<div>ID:{run.id}</div> <div className="min-w-0 break-all">ID:{run.id}</div>
<div>计划:{formatDate(run.scheduled_for)}</div> <div>计划:{formatDate(run.scheduled_for)}</div>
<div>开始:{formatDate(run.started_at)}</div> <div>开始:{formatDate(run.started_at)}</div>
<div>结束:{formatDate(run.finished_at)}</div> <div>结束:{formatDate(run.finished_at)}</div>
<div>耗时:{formatDuration(run)}</div> <div>耗时:{formatDuration(run)}</div>
</div> </div>
{run.error && ( {run.error && (
<div className="text-destructive mt-2 rounded border border-destructive/30 bg-destructive/5 p-2 text-xs"> <div className="text-destructive mt-2 break-words rounded border border-destructive/30 bg-destructive/5 p-2 text-xs">
{run.error} {run.error}
</div> </div>
)} )}
@@ -502,8 +502,8 @@ export default function ScheduledTaskRunDetailPage() {
} }
className="flex min-h-0 flex-1 flex-col" className="flex min-h-0 flex-1 flex-col"
> >
<div className="border-b px-3 pt-1"> <div className="min-w-0 overflow-x-auto border-b px-3 pt-1">
<TabsList className="h-8"> <TabsList className="h-8 w-max">
{result && isHtmlRun && ( {result && isHtmlRun && (
<TabsTrigger value="html" className="text-xs"> <TabsTrigger value="html" className="text-xs">
<Code2 className="mr-1 size-3" /> <Code2 className="mr-1 size-3" />
@@ -573,7 +573,7 @@ export default function ScheduledTaskRunDetailPage() {
rehypePlugins={[]} rehypePlugins={[]}
/> />
) : ( ) : (
<pre className="whitespace-pre-wrap text-sm"> <pre className="whitespace-pre-wrap break-words text-sm">
{result.content || "无正文内容"} {result.content || "无正文内容"}
</pre> </pre>
)} )}
@@ -594,9 +594,9 @@ export default function ScheduledTaskRunDetailPage() {
const editState = edits[file.index] ?? null; const editState = edits[file.index] ?? null;
const isEditing = editState !== null; const isEditing = editState !== null;
return ( return (
<div key={file.index} className="rounded-md border"> <div key={file.index} className="min-w-0 rounded-md border">
<div className="flex flex-wrap items-center justify-between gap-2 border-b p-2"> <div className="flex flex-wrap items-center justify-between gap-2 border-b p-2">
<div className="min-w-0"> <div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{file.name}</div> <div className="truncate text-sm font-medium">{file.name}</div>
<div className="text-muted-foreground text-xs"> <div className="text-muted-foreground text-xs">
{file.mime_type || file.kind || "附件"} {file.mime_type || file.kind || "附件"}
@@ -679,7 +679,7 @@ export default function ScheduledTaskRunDetailPage() {
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="meta" className="m-0 p-3"> <TabsContent value="meta" className="m-0 p-3">
<pre className="bg-muted overflow-auto rounded-md p-3 text-xs"> <pre className="bg-muted overflow-auto whitespace-pre-wrap break-words rounded-md p-3 text-xs">
{resultMetadata} {resultMetadata}
</pre> </pre>
</TabsContent> </TabsContent>
+31 -10
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { import {
AlertCircle, AlertCircle,
BellOff, BellOff,
@@ -133,6 +134,7 @@ const DESCRIPTION_TEXTAREA_ROWS = 4;
const PROMPT_TEXTAREA_ROWS = 10; const PROMPT_TEXTAREA_ROWS = 10;
export default function ScheduledTasksPage() { export default function ScheduledTasksPage() {
const queryClient = useQueryClient();
const { theme } = useTheme(); const { theme } = useTheme();
const darkToastOpts = const darkToastOpts =
theme === "dark" theme === "dark"
@@ -361,15 +363,35 @@ export default function ScheduledTasksPage() {
} }
}; };
const handleTriggerTask = async (task: ScheduledTask) => {
setSelectedTaskId(task.task_id);
setRunsTimeFilter("all");
try {
await actions.trigger.mutateAsync(task.task_id);
await queryClient.invalidateQueries({
queryKey: ["scheduled-tasks", task.task_id, "runs"],
});
await queryClient.invalidateQueries({
queryKey: ["scheduled-tasks", "list"],
});
toast.success("已触发执行", darkToastOpts.success);
} catch (error) {
toast.error(
error instanceof Error ? error.message : "触发失败",
darkToastOpts.error,
);
}
};
return ( return (
<WorkspaceContainer> <WorkspaceContainer>
<WorkspaceHeader /> <WorkspaceHeader />
<WorkspaceBody> <WorkspaceBody>
<div className="grid size-full min-h-0 grid-cols-1 gap-4 p-4 xl:grid-cols-[420px_minmax(0,1fr)]"> <div className="grid size-full min-h-0 grid-cols-1 grid-rows-[minmax(0,1fr)_minmax(0,1fr)] gap-4 p-4 xl:grid-cols-[420px_minmax(0,1fr)] xl:grid-rows-1">
<Card className="flex min-h-0 flex-col"> <Card className="flex min-h-0 min-w-0 flex-col">
<CardHeader className="gap-3"> <CardHeader className="gap-3">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div className="min-w-0">
<CardTitle>定时任务</CardTitle> <CardTitle>定时任务</CardTitle>
<p className="text-muted-foreground mt-1 text-sm"> <p className="text-muted-foreground mt-1 text-sm">
选择任务后,在右侧查看全部执行记录 选择任务后,在右侧查看全部执行记录
@@ -563,8 +585,7 @@ export default function ScheduledTasksPage() {
title="立即执行" title="立即执行"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
actions.trigger.mutate(task.task_id); void handleTriggerTask(task);
toast.success("已触发执行", darkToastOpts.success);
}} }}
> >
<RefreshCw className="size-4" /> <RefreshCw className="size-4" />
@@ -715,9 +736,9 @@ export default function ScheduledTasksPage() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="flex min-h-0 flex-col"> <Card className="flex min-h-0 min-w-0 flex-col">
<CardHeader className="gap-2"> <CardHeader className="gap-2">
<div className="flex items-center justify-between gap-2"> <div className="flex min-w-0 items-center justify-between gap-2">
<CardTitle>执行记录</CardTitle> <CardTitle>执行记录</CardTitle>
<Select value={runsTimeFilter} onValueChange={(v) => setRunsTimeFilter(v as typeof runsTimeFilter)}> <Select value={runsTimeFilter} onValueChange={(v) => setRunsTimeFilter(v as typeof runsTimeFilter)}>
<SelectTrigger className="h-7 w-28 text-xs"> <SelectTrigger className="h-7 w-28 text-xs">
@@ -731,7 +752,7 @@ export default function ScheduledTasksPage() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground min-w-0 truncate text-sm">
{selectedTask ? desensitize(selectedTask.name) : "请选择左侧任务"} {selectedTask ? desensitize(selectedTask.name) : "请选择左侧任务"}
</p> </p>
{selectedTask && (selectedTask.creator_email || selectedTask.creator_user_id) && ( {selectedTask && (selectedTask.creator_email || selectedTask.creator_user_id) && (
@@ -770,11 +791,11 @@ export default function ScheduledTasksPage() {
)} )}
> >
<div className="flex flex-wrap items-start justify-between gap-3"> <div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0 flex-1">
<div className="truncate font-medium"> <div className="truncate font-medium">
{run.result?.title ? desensitize(run.result.title) : formatDate(run.scheduled_for)} {run.result?.title ? desensitize(run.result.title) : formatDate(run.scheduled_for)}
</div> </div>
<div className="text-muted-foreground mt-1 text-xs"> <div className="text-muted-foreground mt-1 break-words text-xs">
计划:{formatDate(run.scheduled_for)} · 开始: 计划:{formatDate(run.scheduled_for)} · 开始:
{formatDate(run.started_at)} {formatDate(run.started_at)}
</div> </div>
@@ -509,6 +509,8 @@ Per-user runtime overrides (whitelisted fields) live at `{base_dir}/users/{user_
Both endpoints are whitelisted in `auth_middleware._PUBLIC_EXACT_PATHS`. The shared `_get_or_create_user_by_username()` helper (in `routers/auth.py`) backs both `/login/username` and `/login/token`. Both endpoints are whitelisted in `auth_middleware._PUBLIC_EXACT_PATHS`. The shared `_get_or_create_user_by_username()` helper (in `routers/auth.py`) backs both `/login/username` and `/login/token`.
- `log_system` - Admin-only external log-system entry (`deerflow/config/log_system_config.py`): `{enabled, url, label}`. Surfaced read-only by `GET /api/system-settings/log-system` (admin only); the frontend renders it as a "日志系统" item in the bottom-right settings menu (`workspace-nav-menu.tsx`) that opens `url` in a new tab. Disabled by default.
**`extensions_config.json`**: **`extensions_config.json`**:
- `mcpServers` - Map of server name → config (enabled, type, command, args, env, url, headers, oauth, description) - `mcpServers` - Map of server name → config (enabled, type, command, args, env, url, headers, oauth, description)
- `skills` - Map of skill name → state (enabled) - `skills` - Map of skill name → state (enabled)
@@ -10,6 +10,7 @@ from pydantic import BaseModel, Field
from app.gateway.auth.password import hash_password_async from app.gateway.auth.password import hash_password_async
from app.gateway.deps import get_optional_user_from_request from app.gateway.deps import get_optional_user_from_request
from deerflow.config import get_app_config
from deerflow.config.system_settings import ( from deerflow.config.system_settings import (
PromptPrefixSettings, PromptPrefixSettings,
SystemSettings, SystemSettings,
@@ -200,3 +201,23 @@ async def update_appearance_settings(
else: else:
saved = current saved = current
return AppearanceResponse(default_theme=saved.appearance.default_theme) return AppearanceResponse(default_theme=saved.appearance.default_theme)
class LogSystemResponse(BaseModel):
"""External log-system entry shown in the admin settings menu.
Sourced from ``config.yaml`` (``log_system`` section), so it is read-only
here — there is no PUT counterpart.
"""
enabled: bool = Field(..., description="Whether the entry is shown in the settings menu")
url: str = Field(..., description="External log-system URL opened in a new tab")
label: str = Field(..., description="Menu label shown for the entry")
@router.get("/log-system", response_model=LogSystemResponse)
async def get_log_system(request: Request) -> LogSystemResponse:
"""Return the admin-configured external log-system link from config.yaml. Admin only."""
await _require_admin(request)
cfg = get_app_config().log_system
return LogSystemResponse(enabled=cfg.enabled, url=cfg.url, label=cfg.label)
@@ -208,6 +208,13 @@ auth_login:
verify_ssl: false verify_ssl: false
ca_cert_path: "" # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验 ca_cert_path: "" # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验
# 日志系统入口(仅管理员可见):右下角「设置」菜单中会多出一个「日志系统」入口,
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
log_system:
enabled: true
url: "https://your-log-system.example.com"
label: "日志系统"
database: database:
backend: sqlite backend: sqlite
sqlite_dir: .deer-flow/data sqlite_dir: .deer-flow/data
@@ -15,6 +15,7 @@ from deerflow.config.database_config import DatabaseConfig
from deerflow.config.extensions_config import ExtensionsConfig from deerflow.config.extensions_config import ExtensionsConfig
from deerflow.config.gateway_config import GatewayConfig from deerflow.config.gateway_config import GatewayConfig
from deerflow.config.guardrails_config import GuardrailsConfig, load_guardrails_config_from_dict from deerflow.config.guardrails_config import GuardrailsConfig, load_guardrails_config_from_dict
from deerflow.config.log_system_config import LogSystemConfig
from deerflow.config.login_config import AuthLoginConfig from deerflow.config.login_config import AuthLoginConfig
from deerflow.config.memory_config import MemoryConfig, load_memory_config_from_dict from deerflow.config.memory_config import MemoryConfig, load_memory_config_from_dict
from deerflow.config.model_config import ModelConfig from deerflow.config.model_config import ModelConfig
@@ -108,6 +109,7 @@ class AppConfig(BaseModel):
models: list[ModelConfig] = Field(default_factory=list, description="Available models") models: list[ModelConfig] = Field(default_factory=list, description="Available models")
vision: VisionConfig = Field(default_factory=VisionConfig, description="Image recognition (识图) configuration") vision: VisionConfig = Field(default_factory=VisionConfig, description="Image recognition (识图) configuration")
auth_login: AuthLoginConfig = Field(default_factory=AuthLoginConfig, description="Custom login modes (username password gate, token exchange)") auth_login: AuthLoginConfig = Field(default_factory=AuthLoginConfig, description="Custom login modes (username password gate, token exchange)")
log_system: LogSystemConfig = Field(default_factory=LogSystemConfig, description="Admin-only external log-system entry shown in the settings menu")
sandbox: SandboxConfig = Field(description="Sandbox configuration") sandbox: SandboxConfig = Field(description="Sandbox configuration")
tools: list[ToolConfig] = Field(default_factory=list, description="Available tools") tools: list[ToolConfig] = Field(default_factory=list, description="Available tools")
tool_groups: list[ToolGroupConfig] = Field(default_factory=list, description="Available tool groups") tool_groups: list[ToolGroupConfig] = Field(default_factory=list, description="Available tool groups")
@@ -0,0 +1,18 @@
"""Configuration for the admin-only external log-system entry.
A small read-only section in ``config.yaml`` that lets a deployment point the
"日志系统" entry (shown in the bottom-right settings menu for admins) at an
external log/observability system. The URL is opened in a new browser tab.
"""
from __future__ import annotations
from pydantic import BaseModel, Field
class LogSystemConfig(BaseModel):
"""External log-system link surfaced in the admin settings menu."""
enabled: bool = Field(default=False, description="Whether the log-system entry is shown in the settings menu")
url: str = Field(default="", description="External log-system URL opened in a new tab when the entry is clicked")
label: str = Field(default="日志系统", description="Menu label shown for the entry")
+6
View File
@@ -163,6 +163,12 @@ auth_login:
# 永远 401。此处 uat 域名按自签处理,先关校验;生产请置 true 或配 ca_cert_path。 # 永远 401。此处 uat 域名按自签处理,先关校验;生产请置 true 或配 ca_cert_path。
verify_ssl: false verify_ssl: false
ca_cert_path: "" # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验 ca_cert_path: "" # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验
# 日志系统入口(仅管理员可见):右下角「设置」菜单中会多出一个「日志系统」入口,
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
log_system:
enabled: true
url: "https://your-log-system.example.com"
label: "日志系统"
database: database:
backend: mysql backend: mysql
mysql_url: $MYSQL_DATABASE_URL mysql_url: $MYSQL_DATABASE_URL