This commit is contained in:
吉鑫升 committed 2026-06-04 14:28:22 +08:00
commit 9704eff9f7
42 files changed
+1973 -10517

No files matched your search

+1
View File
@@ -250,6 +250,7 @@ export default function AgentChatPage() {
className="w-full"
isNewThread={isLandingState}
threadId={threadId}
assistantId={agent_id}
autoFocus={isLandingState}
status={
thread.error
+15 -1
View File
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { usePromptPrefixSettings } from "@/core/system-settings/hooks";
import { useUserPreferences } from "@/core/user-preferences";
import { ArtifactTrigger, useArtifacts } from "@/components/workspace/artifacts";
import {
ChatBox,
@@ -50,6 +51,9 @@ export default function ChatPage() {
const [mounted, setMounted] = useState(false);
const { tokenUsageEnabled } = useModels();
const { data: promptPrefixSettings } = usePromptPrefixSettings();
// Personal opt-out: when set, hide the admin prompt-prefix switch entirely and
// never apply the prefix — the user uses only their own prompts.
const { hide_admin_prompt_prefix: hideAdminPrompt } = useUserPreferences();
const [promptPrefixOn, setPromptPrefixOn] = useState(false);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const { agents: selectorAgents, isLoading: selectorLoading } =
@@ -159,6 +163,7 @@ export default function ChatPage() {
isNewThread &&
!hasStartedConversation &&
promptPrefixOn &&
!hideAdminPrompt &&
promptPrefixSettings?.enabled === true;
setHasStartedConversation(true);
window.dispatchEvent(
@@ -189,6 +194,7 @@ export default function ChatPage() {
hasStartedConversation,
promptPrefixOn,
promptPrefixSettings,
hideAdminPrompt,
selectedAgentId,
router,
settings.context.writing_mode,
@@ -217,6 +223,8 @@ export default function ChatPage() {
const promptPrefixProp = useMemo(() => {
if (!promptPrefixSettings?.enabled) return undefined;
// Personal opt-out hides the admin prompt-prefix switch entirely.
if (hideAdminPrompt) return undefined;
// The prefix toggle is offered only on the opening turn of a new thread.
if (!isNewThread || hasStartedConversation) return undefined;
return {
@@ -224,7 +232,13 @@ export default function ChatPage() {
on: promptPrefixOn,
onToggle: () => setPromptPrefixOn((v) => !v),
};
}, [promptPrefixSettings, isNewThread, hasStartedConversation, promptPrefixOn]);
}, [
promptPrefixSettings,
hideAdminPrompt,
isNewThread,
hasStartedConversation,
promptPrefixOn,
]);
const messageListPaddingBottom = showFollowups
? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM +
+31 -4
View File
@@ -19,6 +19,11 @@ import {
} from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import {
Select,
SelectContent,
@@ -361,8 +366,19 @@ export default function NewAgentPage() {
) : null}
</header>
<main className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[minmax(0,1fr)_420px]">
<section className="flex min-h-0 flex-col border-r">
<ResizablePanelGroup
id="new-agent-config-panels"
orientation="horizontal"
defaultLayout={{ chat: 62, config: 38 }}
className="min-h-0 flex-1"
>
<ResizablePanel
id="chat"
defaultSize={62}
minSize={40}
className="min-h-0"
>
<section className="flex size-full min-h-0 flex-col">
<div className="min-h-0 flex-1">
<MessageList
className="size-full"
@@ -428,8 +444,18 @@ export default function NewAgentPage() {
</PromptInput>
</div>
</section>
</ResizablePanel>
<aside className="bg-background min-h-0 overflow-y-auto">
<ResizableHandle withHandle />
<ResizablePanel
id="config"
defaultSize={38}
minSize={24}
maxSize={60}
className="min-h-0"
>
<aside className="bg-background size-full min-h-0 overflow-y-auto">
<div className="space-y-5 p-4">
<div>
<h2 className="text-sm font-semibold">智能体配置</h2>
@@ -606,7 +632,8 @@ export default function NewAgentPage() {
</Button>
</div>
</aside>
</main>
</ResizablePanel>
</ResizablePanelGroup>
</div>
</ArtifactsProvider>
</ThreadContext.Provider>
+105 -109
View File
@@ -114,13 +114,15 @@ const emptyForm = {
timezone: "Asia/Shanghai",
prompt: "",
execution_agent_name: "",
// Two independent output switches (may both be on): require a Markdown file
// and/or render an HTML page. The form no longer splits into "普通任务 / HTML
// 页面" tabs.
require_markdown: false,
// HTML page task fields. ``task_kind`` "agent" keeps the original behavior.
// Data gathering is delegated to the agent's own search skills — no explicit
// search query / data source. ``html_model_name`` empty means "use the first
// available model" — never a hardcoded GLM-5.
task_kind: "agent" as "agent" | "html_page",
html_page_prompt: "",
require_html: false,
// HTML-only extra fields, shown when ``require_html`` is on. The main
// ``prompt`` above doubles as the page requirement, so there is no separate
// page-prompt field. ``html_model_name`` empty means "use the first available
// model" — never a hardcoded GLM-5.
html_negative_prompt: "",
html_reference_favorite_id: "",
html_model_name: "",
@@ -244,11 +246,15 @@ export default function ScheduledTasksPage() {
description: task.description ?? "",
schedule: task.schedule_text,
timezone: task.timezone,
prompt: task.prompt,
// The main prompt doubles as the HTML page requirement; legacy HTML tasks
// stored that under ``html_config.page_prompt``, so prefer it on edit.
prompt: html.page_prompt || task.prompt,
execution_agent_name: task.execution_agent_name ?? "",
require_markdown: !!task.execution_context?.require_markdown,
task_kind: task.execution_context?.task_kind === "html_page" ? "html_page" : "agent",
html_page_prompt: html.page_prompt ?? "",
// Backward compatible: legacy HTML tasks only carry ``task_kind``.
require_html:
!!task.execution_context?.require_html ||
task.execution_context?.task_kind === "html_page",
html_negative_prompt: html.negative_prompt ?? "",
html_reference_favorite_id: html.reference_favorite_id ?? "",
html_model_name: html.model_name ?? "",
@@ -270,20 +276,26 @@ export default function ScheduledTasksPage() {
setScheduleConfig(DEFAULT_SCHEDULE_CONFIG);
};
const isHtmlTask = form.task_kind === "html_page";
const isHtmlTask = form.require_html;
// The model actually shown/used: the user's choice, else the first configured
// model. Never a hardcoded GLM-5 — it follows config order and the selection.
const effectiveHtmlModel = form.html_model_name || availableModels[0]?.name || "";
// Build the execution_context for the current form. HTML tasks persist their
// config under ``html_config``; regular tasks send an explicit ``agent`` kind
// so switching a task back from HTML clears the old config.
// Build the execution_context for the current form. ``require_html`` and
// ``require_markdown`` are independent toggles (both may be on). HTML tasks
// persist their extra config under ``html_config`` and reuse the main prompt
// as the page requirement; ``task_kind`` is kept in sync only for backward
// compatibility with older readers.
const buildExecutionContext = (): ScheduledTaskExecutionContext => {
if (!isHtmlTask) return { task_kind: "agent", html_config: undefined };
if (!isHtmlTask) {
return { require_html: false, task_kind: "agent", html_config: undefined };
}
return {
require_html: true,
task_kind: "html_page",
html_config: {
page_prompt: form.html_page_prompt.trim() || null,
// The main prompt doubles as the page requirement.
page_prompt: form.prompt.trim() || null,
negative_prompt: form.html_negative_prompt.trim() || null,
reference_favorite_id: form.html_reference_favorite_id || null,
// Send the user's selection; fall back to the first configured model,
@@ -297,11 +309,10 @@ export default function ScheduledTasksPage() {
try {
const cronSchedule = buildCron(scheduleConfig);
const executionContext = buildExecutionContext();
// HTML tasks may leave the agent prompt empty — fall back to the page
// prompt so the (non-null) prompt column always has something useful.
const effectivePrompt = isHtmlTask
? form.prompt.trim() || form.html_page_prompt.trim() || "生成 HTML 页面"
: form.prompt;
// The (non-null) prompt column always needs something useful; HTML tasks
// may leave it sparse, so fall back to a generic instruction.
const effectivePrompt =
form.prompt.trim() || (isHtmlTask ? "生成 HTML 页面" : form.prompt);
if (creating) {
const task = await actions.create.mutateAsync({
name: form.name || "定时任务",
@@ -309,8 +320,8 @@ export default function ScheduledTasksPage() {
schedule: cronSchedule,
timezone: form.timezone || "Asia/Shanghai",
prompt: effectivePrompt,
execution_agent_name: isHtmlTask ? null : form.execution_agent_name || null,
require_markdown: isHtmlTask ? false : form.require_markdown,
execution_agent_name: form.execution_agent_name || null,
require_markdown: form.require_markdown,
execution_context: executionContext,
});
setSelectedTaskId(task.task_id);
@@ -324,8 +335,8 @@ export default function ScheduledTasksPage() {
schedule: cronSchedule,
timezone: form.timezone,
prompt: effectivePrompt,
execution_agent_name: isHtmlTask ? null : form.execution_agent_name || null,
require_markdown: isHtmlTask ? false : form.require_markdown,
execution_agent_name: form.execution_agent_name || null,
require_markdown: form.require_markdown,
execution_context: executionContext,
},
});
@@ -502,7 +513,8 @@ export default function ScheduledTasksPage() {
<Globe className="size-3" />已发布
</Badge>
)}
{task.execution_context?.task_kind === "html_page" && (
{(task.execution_context?.require_html ||
task.execution_context?.task_kind === "html_page") && (
<Badge
variant="outline"
className="shrink-0 border-sky-500/30 bg-sky-500/10 text-xs text-sky-600 dark:text-sky-400"
@@ -953,20 +965,6 @@ export default function ScheduledTasksPage() {
}
placeholder="任务描述(可选,将显示在公开页标题下方)"
/>
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-xs">任务类型</p>
<Tabs
value={form.task_kind}
onValueChange={(v) =>
setForm((value) => ({ ...value, task_kind: v as "agent" | "html_page" }))
}
>
<TabsList className="w-full">
<TabsTrigger value="agent" className="flex-1">普通任务</TabsTrigger>
<TabsTrigger value="html_page" className="flex-1">HTML 页面</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-xs">执行频率</p>
<ScheduleBuilder value={scheduleConfig} onChange={setScheduleConfig} />
@@ -979,77 +977,83 @@ export default function ScheduledTasksPage() {
placeholder="时区,例如:Asia/Shanghai"
/>
{!isHtmlTask && (
<>
<Select
value={form.execution_agent_name || "__default__"}
onValueChange={(v) =>
setForm((value) => ({
...value,
execution_agent_name: v === "__default__" ? "" : v,
}))
}
>
<SelectTrigger className="w-full min-w-0 max-w-full overflow-hidden [&>span]:min-w-0 [&>span]:flex-1 [&>span]:truncate">
<SelectValue placeholder="选择执行智能体" />
</SelectTrigger>
<SelectContent
position="popper"
className="w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)]"
>
<SelectItem value="__default__">默认助手(不指定)</SelectItem>
{agents.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
<span className="block truncate">
{desensitize(agent.name)}
{agent.description ? ` — ${desensitize(agent.description)}` : ""}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Textarea
className="min-h-40"
value={form.prompt}
onChange={(event) =>
setForm((value) => ({ ...value, prompt: event.target.value }))
}
placeholder="任务 Prompt"
/>
<div className="flex items-center justify-between rounded-md border p-3">
<div className="min-w-0 pr-3">
<p className="text-sm font-medium">必须生成 Markdown 文件</p>
<p className="text-muted-foreground text-xs">
开启后会提示大模型生成 Markdown(.md) 文件;系统会校验本次是否生成了 .md 文件且文件正文超过 100 字,未达到则记为失败。
</p>
</div>
<Switch
checked={form.require_markdown}
onCheckedChange={(v) =>
setForm((value) => ({ ...value, require_markdown: v }))
}
/>
</div>
</>
)}
<Select
value={form.execution_agent_name || "__default__"}
onValueChange={(v) =>
setForm((value) => ({
...value,
execution_agent_name: v === "__default__" ? "" : v,
}))
}
>
<SelectTrigger className="w-full min-w-0 max-w-full overflow-hidden [&>span]:min-w-0 [&>span]:flex-1 [&>span]:truncate">
<SelectValue placeholder="选择执行智能体" />
</SelectTrigger>
<SelectContent
position="popper"
className="w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)]"
>
<SelectItem value="__default__">默认助手(不指定)</SelectItem>
{agents.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
<span className="block truncate">
{desensitize(agent.name)}
{agent.description ? ` — ${desensitize(agent.description)}` : ""}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Textarea
className="min-h-40"
value={form.prompt}
onChange={(event) =>
setForm((value) => ({ ...value, prompt: event.target.value }))
}
placeholder={
isHtmlTask
? "任务 / 页面生成要求,例如:检索新能源车最新舆情,生成适合领导快速浏览的态势页"
: "任务 Prompt"
}
/>
<div className="flex items-center justify-between rounded-md border p-3">
<div className="min-w-0 pr-3">
<p className="text-sm font-medium">必须生成 Markdown 文件</p>
<p className="text-muted-foreground text-xs">
开启后会提示大模型生成 Markdown(.md) 文件;系统会校验本次是否生成了 .md 文件且文件正文超过 100 字,未达到则记为失败。
</p>
</div>
<Switch
checked={form.require_markdown}
onCheckedChange={(v) =>
setForm((value) => ({ ...value, require_markdown: v }))
}
/>
</div>
<div className="flex items-center justify-between rounded-md border p-3">
<div className="min-w-0 pr-3">
<p className="text-sm font-medium">必须生成 HTML 页面</p>
<p className="text-muted-foreground text-xs">
开启后会先用智能体检索整理真实数据,再渲染为完整 HTML 页面(可在执行记录中预览)。可与“必须生成 Markdown”同时开启。
</p>
</div>
<Switch
checked={form.require_html}
onCheckedChange={(v) =>
setForm((value) => ({ ...value, require_html: v }))
}
/>
</div>
{isHtmlTask && (
<>
<Textarea
className="min-h-24"
value={form.html_page_prompt}
onChange={(event) =>
setForm((value) => ({ ...value, html_page_prompt: event.target.value }))
}
placeholder="页面生成要求,例如:检索新能源车最新舆情,生成适合领导快速浏览的态势页"
/>
<Textarea
className="min-h-16"
value={form.html_negative_prompt}
onChange={(event) =>
setForm((value) => ({ ...value, html_negative_prompt: event.target.value }))
}
placeholder="限制条件(可选),例如:不要使用深色背景,不要出现外部链接脚本"
placeholder="HTML 限制条件(可选),例如:不要使用深色背景,不要出现外部链接脚本"
/>
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-xs">参考收藏页面(可选)</p>
@@ -1127,14 +1131,6 @@ export default function ScheduledTasksPage() {
</SelectContent>
</Select>
</div>
<Textarea
className="min-h-16"
value={form.prompt}
onChange={(event) =>
setForm((value) => ({ ...value, prompt: event.target.value }))
}
placeholder="补充说明(可选):进一步约束智能体如何检索数据、页面包含哪些内容"
/>
<p className="text-muted-foreground text-xs">
执行流程:智能体运用自身检索技能整理真实数据 → 选定模型渲染为完整 HTML → 自动安全清洗与结构校验 → 可在执行记录中预览。
</p>