From 925675ab54423f0c9d93fb3fec98420a4cc6e23c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Thu, 4 Jun 2026 09:18:32 +0800 Subject: [PATCH] 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) --- .../workspace/workspace-nav-menu.tsx | 35 ++++++++++++++++ frontend-web/src/core/system-settings/api.ts | 16 ++++++++ .../src/pages/AdminScheduledTasksPage.tsx | 28 +++++++++---- .../src/pages/ScheduledTaskRunDetailPage.tsx | 30 +++++++------- frontend-web/src/pages/ScheduledTasksPage.tsx | 41 ++++++++++++++----- offline-backend-20260512/backend/CLAUDE.md | 2 + .../app/gateway/routers/system_settings.py | 21 ++++++++++ offline-backend-20260512/backend/config.yaml | 7 ++++ .../harness/deerflow/config/app_config.py | 2 + .../deerflow/config/log_system_config.py | 18 ++++++++ offline-backend-20260512/config.yaml | 6 +++ 11 files changed, 172 insertions(+), 34 deletions(-) create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/config/log_system_config.py diff --git a/frontend-web/src/components/workspace/workspace-nav-menu.tsx b/frontend-web/src/components/workspace/workspace-nav-menu.tsx index d87b34d9..61597fcf 100644 --- a/frontend-web/src/components/workspace/workspace-nav-menu.tsx +++ b/frontend-web/src/components/workspace/workspace-nav-menu.tsx @@ -10,6 +10,7 @@ import { HistoryIcon, MessageSquareCodeIcon, MessageSquarePlusIcon, + ScrollTextIcon, Settings2Icon, SettingsIcon, ShieldCheckIcon, @@ -36,6 +37,10 @@ import { import { getStoredAuth } from "@/core/auth"; import { useI18n } from "@/core/i18n/hooks"; import { useUnreadCount } from "@/core/notifications"; +import { + loadLogSystemSettings, + type LogSystemSettings, +} from "@/core/system-settings/api"; import { NotificationDialog } from "./notification-dialog"; import { useNavigate } from "react-router-dom"; @@ -71,6 +76,7 @@ export function WorkspaceNavMenu() { >("appearance"); const [notificationOpen, setNotificationOpen] = useState(false); const [mounted, setMounted] = useState(false); + const [logSystem, setLogSystem] = useState(null); const { open: isSidebarOpen } = useSidebar(); const { t } = useI18n(); const { data: unreadCount = 0 } = useUnreadCount(); @@ -79,6 +85,21 @@ export function WorkspaceNavMenu() { 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 ( <> )} + {isAdmin && logSystem?.enabled && logSystem.url && ( + + window.open( + logSystem.url, + "_blank", + "noopener,noreferrer", + ) + } + > + + {logSystem.label || "日志系统"} + + )} diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index 265a2276..579de5c4 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -147,6 +147,22 @@ export async function updateCitationDisplaySettings( return (await response.json()) as CitationDisplaySettings; } +export interface LogSystemSettings { + enabled: boolean; + url: string; + label: string; +} + +export async function loadLogSystemSettings(): Promise { + 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( body: PrivateSquareUpdate, ): Promise { diff --git a/frontend-web/src/pages/AdminScheduledTasksPage.tsx b/frontend-web/src/pages/AdminScheduledTasksPage.tsx index 289be4bb..2677b3fa 100644 --- a/frontend-web/src/pages/AdminScheduledTasksPage.tsx +++ b/frontend-web/src/pages/AdminScheduledTasksPage.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; import { Download, Edit3, @@ -71,6 +72,7 @@ const emptyForm = { }; export default function AdminScheduledTasksPage() { + const queryClient = useQueryClient(); const { theme } = useTheme(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); @@ -178,8 +180,15 @@ export default function AdminScheduledTasksPage() { }; const triggerTask = async (task: ScheduledTask) => { + setSelectedTaskId(task.task_id); try { 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("已触发执行"); } catch (error) { toast.error(error instanceof Error ? error.message : "触发失败"); @@ -208,23 +217,24 @@ export default function AdminScheduledTasksPage() { -
- +
+ -
+
定时任务管理

管理员视图:查看、编辑、删除全部用户的定时任务

{ event.preventDefault(); setSearch(searchInput.trim()); }} > setSearchInput(event.target.value)} placeholder="按任务名称搜索" @@ -268,7 +278,7 @@ export default function AdminScheduledTasksPage() { onClick={() => setSelectedTaskId(task.task_id)} >
-
+
{task.name}
@@ -348,10 +358,10 @@ export default function AdminScheduledTasksPage() { - + 执行记录 -

+

{selectedTask ? selectedTask.name : "请选择左侧任务"}

@@ -379,11 +389,11 @@ export default function AdminScheduledTasksPage() { className="rounded-md border p-4 transition-colors hover:bg-accent/40" >
-
+
{run.result?.title || formatDate(run.scheduled_for)}
-
+
计划:{formatDate(run.scheduled_for)} · 开始: {formatDate(run.started_at)}
diff --git a/frontend-web/src/pages/ScheduledTaskRunDetailPage.tsx b/frontend-web/src/pages/ScheduledTaskRunDetailPage.tsx index 0b7df6dd..fe01f2e5 100644 --- a/frontend-web/src/pages/ScheduledTaskRunDetailPage.tsx +++ b/frontend-web/src/pages/ScheduledTaskRunDetailPage.tsx @@ -97,7 +97,7 @@ function RenderPreview({ file }: { file: ScheduledTaskResultFileContent }) { /> ); } - return
{file.content}
; + return
{file.content}
; } /** @@ -162,9 +162,9 @@ function ExecutionMessage({ message }: { message: ScheduledTaskRunMessage }) { const isAi = message.event_type === "llm.ai.response"; return ( -
-
- +
+
+ {role.label} {toolName ? ` · ${toolName}` : ""} @@ -428,9 +428,9 @@ export default function ScheduledTaskRunDetailPage() {
- + {/* 工具栏 */} -
+
{result?.title || (isLoading ? "正在加载..." : "执行结果详情")} @@ -470,15 +470,15 @@ export default function ScheduledTaskRunDetailPage() { {/* 执行信息(可折叠) */} {showMeta && run && (
-
-
ID:{run.id}
+
+
ID:{run.id}
计划:{formatDate(run.scheduled_for)}
开始:{formatDate(run.started_at)}
结束:{formatDate(run.finished_at)}
耗时:{formatDuration(run)}
{run.error && ( -
+
{run.error}
)} @@ -502,8 +502,8 @@ export default function ScheduledTaskRunDetailPage() { } className="flex min-h-0 flex-1 flex-col" > -
- +
+ {result && isHtmlRun && ( @@ -573,7 +573,7 @@ export default function ScheduledTaskRunDetailPage() { rehypePlugins={[]} /> ) : ( -
+                    
                       {result.content || "无正文内容"}
                     
)} @@ -594,9 +594,9 @@ export default function ScheduledTaskRunDetailPage() { const editState = edits[file.index] ?? null; const isEditing = editState !== null; return ( -
+
-
+
{file.name}
{file.mime_type || file.kind || "附件"} @@ -679,7 +679,7 @@ export default function ScheduledTaskRunDetailPage() {
-
+                  
                     {resultMetadata}
                   
diff --git a/frontend-web/src/pages/ScheduledTasksPage.tsx b/frontend-web/src/pages/ScheduledTasksPage.tsx index d9d5dcea..973453de 100644 --- a/frontend-web/src/pages/ScheduledTasksPage.tsx +++ b/frontend-web/src/pages/ScheduledTasksPage.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; import { AlertCircle, BellOff, @@ -133,6 +134,7 @@ const DESCRIPTION_TEXTAREA_ROWS = 4; const PROMPT_TEXTAREA_ROWS = 10; export default function ScheduledTasksPage() { + const queryClient = useQueryClient(); const { theme } = useTheme(); const darkToastOpts = 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 ( -
- +
+
-
+
定时任务

选择任务后,在右侧查看全部执行记录 @@ -563,8 +585,7 @@ export default function ScheduledTasksPage() { title="立即执行" onClick={(event) => { event.stopPropagation(); - actions.trigger.mutate(task.task_id); - toast.success("已触发执行", darkToastOpts.success); + void handleTriggerTask(task); }} > @@ -715,9 +736,9 @@ export default function ScheduledTasksPage() { - + -

+
执行记录
-

+

{selectedTask ? desensitize(selectedTask.name) : "请选择左侧任务"}

{selectedTask && (selectedTask.creator_email || selectedTask.creator_user_id) && ( @@ -770,11 +791,11 @@ export default function ScheduledTasksPage() { )} >
-
+
{run.result?.title ? desensitize(run.result.title) : formatDate(run.scheduled_for)}
-
+
计划:{formatDate(run.scheduled_for)} · 开始: {formatDate(run.started_at)}
diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index e4b56a9b..d9cbcde3 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -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`. +- `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`**: - `mcpServers` - Map of server name → config (enabled, type, command, args, env, url, headers, oauth, description) - `skills` - Map of skill name → state (enabled) diff --git a/offline-backend-20260512/backend/app/gateway/routers/system_settings.py b/offline-backend-20260512/backend/app/gateway/routers/system_settings.py index b2d8f90c..c0047549 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/system_settings.py +++ b/offline-backend-20260512/backend/app/gateway/routers/system_settings.py @@ -10,6 +10,7 @@ from pydantic import BaseModel, Field from app.gateway.auth.password import hash_password_async from app.gateway.deps import get_optional_user_from_request +from deerflow.config import get_app_config from deerflow.config.system_settings import ( PromptPrefixSettings, SystemSettings, @@ -200,3 +201,23 @@ async def update_appearance_settings( else: saved = current 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) diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index 345a2f46..59cd75a8 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -208,6 +208,13 @@ auth_login: verify_ssl: false ca_cert_path: "" # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验 +# 日志系统入口(仅管理员可见):右下角「设置」菜单中会多出一个「日志系统」入口, +# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。 +log_system: + enabled: true + url: "https://your-log-system.example.com" + label: "日志系统" + database: backend: sqlite sqlite_dir: .deer-flow/data diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/config/app_config.py b/offline-backend-20260512/backend/packages/harness/deerflow/config/app_config.py index b47f5301..f1494bb6 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/config/app_config.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/config/app_config.py @@ -15,6 +15,7 @@ from deerflow.config.database_config import DatabaseConfig from deerflow.config.extensions_config import ExtensionsConfig from deerflow.config.gateway_config import GatewayConfig 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.memory_config import MemoryConfig, load_memory_config_from_dict from deerflow.config.model_config import ModelConfig @@ -108,6 +109,7 @@ class AppConfig(BaseModel): models: list[ModelConfig] = Field(default_factory=list, description="Available models") 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)") + 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") tools: list[ToolConfig] = Field(default_factory=list, description="Available tools") tool_groups: list[ToolGroupConfig] = Field(default_factory=list, description="Available tool groups") diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/config/log_system_config.py b/offline-backend-20260512/backend/packages/harness/deerflow/config/log_system_config.py new file mode 100644 index 00000000..87afe2ab --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/config/log_system_config.py @@ -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") diff --git a/offline-backend-20260512/config.yaml b/offline-backend-20260512/config.yaml index adebc8ae..88ad9743 100644 --- a/offline-backend-20260512/config.yaml +++ b/offline-backend-20260512/config.yaml @@ -163,6 +163,12 @@ auth_login: # 永远 401。此处 uat 域名按自签处理,先关校验;生产请置 true 或配 ca_cert_path。 verify_ssl: false ca_cert_path: "" # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验 +# 日志系统入口(仅管理员可见):右下角「设置」菜单中会多出一个「日志系统」入口, +# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。 +log_system: + enabled: true + url: "https://your-log-system.example.com" + label: "日志系统" database: backend: mysql mysql_url: $MYSQL_DATABASE_URL