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:
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>
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
+18
@@ -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")
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user