chore: update ji branch work

This commit is contained in:
吉鑫升 committed 2026-06-04 17:46:11 +08:00
1 parent 6c30e649e3
commit cbd07c9d1b
9 files changed
+188 -124

No files matched your search

@@ -222,6 +222,7 @@ export function AgentManageDialog({
key={agent.id}
value={`${agent.name} ${agent.description ?? ""}`}
onSelect={() => void add(agent)}
className="flex min-w-0 items-center overflow-hidden"
>
<BotIcon className="text-muted-foreground mr-2 size-4 shrink-0" />
<div className="flex min-w-0 flex-1 flex-col">
@@ -120,7 +120,7 @@ export function AgentSelector({
value={`${agent.name} ${agent.description ?? ""}`}
onSelect={() => handleSelect(agent)}
className={cn(
"flex items-center gap-2",
"flex min-w-0 items-center gap-2 overflow-hidden",
agent.id === selectedAgentId && "bg-accent",
)}
>
@@ -29,6 +29,7 @@ import {
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useAgents } from "@/core/agents/hooks";
import { getStoredAuth } from "@/core/auth";
import {
useCreateCustomPrompt,
useCustomPrompts,
@@ -42,16 +43,6 @@ import {
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
/** Sentinel select value for "no agent association" (global prompt). */
const NO_AGENT = "__global__";
/**
* Bar above the chat input showing the user's saved prompts as centered,
* clickable chips. When there are too many to fit, they become a horizontal
* carousel (arrows appear). A gear pinned to the far right opens the manage
* dialog. Prompts are bound to the account (server-backed), and this coexists
* with the admin-configured global prompt prefix.
*/
export function CustomPromptBar({
onInsert,
className,
@@ -59,19 +50,13 @@ export function CustomPromptBar({
}: {
onInsert: (content: string) => void;
className?: string;
/**
* The agent currently selected for this chat (or null on the general
* new-chat page). Agent-associated prompts only show when their ``agent_id``
* matches; un-associated (global) prompts always show.
*/
currentAgentId?: string | null;
}) {
const allPrompts = useCustomPrompts();
const prompts = allPrompts.filter(
(p) => !p.agent_id || p.agent_id === currentAgentId,
(prompt) => !prompt.agent_id || prompt.agent_id === currentAgentId,
);
const [manageOpen, setManageOpen] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const [canLeft, setCanLeft] = useState(false);
const [canRight, setCanRight] = useState(false);
@@ -87,7 +72,7 @@ export function CustomPromptBar({
updateArrows();
window.addEventListener("resize", updateArrows);
return () => window.removeEventListener("resize", updateArrows);
}, [updateArrows, prompts]);
}, [prompts, updateArrows]);
const scrollByDir = (dir: 1 | -1) => {
scrollRef.current?.scrollBy({ left: dir * 220, behavior: "smooth" });
@@ -97,7 +82,7 @@ export function CustomPromptBar({
return (
<>
<div className={cn("flex items-center gap-1", className)}>
<div className={cn("flex min-w-0 items-center gap-1", className)}>
{prompts.length > 0 && (
<>
{overflowing && (
@@ -116,7 +101,7 @@ export function CustomPromptBar({
<div
ref={scrollRef}
onScroll={updateArrows}
className="flex-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
className="min-w-0 flex-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
<div className="mx-auto flex w-max items-center gap-1.5 px-0.5 py-0.5">
{prompts.map((prompt) => (
@@ -150,7 +135,7 @@ export function CustomPromptBar({
)}
</>
)}
{prompts.length === 0 && <div className="flex-1" />}
{prompts.length === 0 && <div className="min-w-0 flex-1" />}
<Button
type="button"
variant="ghost"
@@ -183,34 +168,33 @@ function ManagePromptsDialog({
const { agents } = useAgents();
const preferences = useUserPreferences();
const updatePreferences = useUpdateUserPreferences();
const currentUserId = getStoredAuth()?.user_id ?? null;
const [editingId, setEditingId] = useState<string | null>(null);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [agentId, setAgentId] = useState<string>(NO_AGENT);
const [agentId, setAgentId] = useState<string | undefined>(undefined);
const saving = createPrompt.isPending || updatePrompt.isPending;
const agentNameById = (id?: string | null) =>
id ? agents.find((a) => a.id === id)?.name ?? null : null;
// Only offer the agents the user has favorited (收藏到「我的」) for
// association — the full catalog is huge, mostly irrelevant, and overflowed
// the dropdown. The agent a prompt is *already* bound to is kept in the list
// even if it is no longer a favorite, so editing never silently drops it.
const selectableAgents = useMemo(() => {
const favorites = agents.filter((a) => a.is_favorite);
if (agentId !== NO_AGENT && !favorites.some((a) => a.id === agentId)) {
const bound = agents.find((a) => a.id === agentId);
if (bound) return [bound, ...favorites];
const owned = currentUserId
? agents.filter((agent) => agent.user_id === currentUserId)
: [];
if (agentId && !owned.some((agent) => agent.id === agentId)) {
const bound = agents.find((agent) => agent.id === agentId);
if (bound) return [bound, ...owned];
}
return favorites;
}, [agents, agentId]);
return owned;
}, [agents, agentId, currentUserId]);
const agentNameById = (id?: string | null) =>
id ? agents.find((agent) => agent.id === id)?.name ?? null : null;
const resetForm = () => {
setEditingId(null);
setTitle("");
setContent("");
setAgentId(NO_AGENT);
setAgentId(undefined);
};
const handleSubmit = async () => {
@@ -219,19 +203,23 @@ function ManagePromptsDialog({
toast.error("提示词内容不能为空");
return;
}
const boundAgentId = agentId === NO_AGENT ? null : agentId;
if (!agentId) {
toast.error("请选择关联智能体");
return;
}
try {
if (editingId) {
await updatePrompt.mutateAsync({
id: editingId,
input: { title: trimmedTitle, content, agent_id: boundAgentId },
input: { title: trimmedTitle, content, agent_id: agentId },
});
toast.success("提示词已更新");
} else {
await createPrompt.mutateAsync({
title: trimmedTitle,
content,
agent_id: boundAgentId,
agent_id: agentId,
});
toast.success("提示词已添加");
}
@@ -242,12 +230,12 @@ function ManagePromptsDialog({
};
const handleEdit = (id: string) => {
const target = prompts.find((p) => p.id === id);
const target = prompts.find((prompt) => prompt.id === id);
if (!target) return;
setEditingId(id);
setTitle(target.title);
setContent(target.content);
setAgentId(target.agent_id || NO_AGENT);
setAgentId(target.agent_id || undefined);
};
const handleToggleHideAdmin = async (next: boolean) => {
@@ -262,6 +250,7 @@ function ManagePromptsDialog({
try {
await deletePrompt.mutateAsync(id);
if (editingId === id) resetForm();
toast.success("提示词已删除");
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
@@ -279,7 +268,7 @@ function ManagePromptsDialog({
<DialogHeader>
<DialogTitle>管理常用提示词</DialogTitle>
<DialogDescription>
保存常用的提示词,保存后会以小标签形式显示在输入框上方,点击即可填入。提示词与你的账号绑定,可跨设备使用。
保存常用提示词后,会显示在输入框上方。提示词与账号绑定,可跨设备使用。
</DialogDescription>
</DialogHeader>
@@ -287,41 +276,44 @@ function ManagePromptsDialog({
<div className="min-w-0">
<p className="text-sm font-medium">只用我自己的提示词</p>
<p className="text-muted-foreground mt-0.5 text-xs">
开启后,输入框不再显示管理员配置的提示词(提示词前缀)开关,本账号仅使用自己保存的提示词。
开启后,输入框不再显示管理员配置的提示词前缀开关,本账号仅使用自己保存的提示词。
</p>
</div>
<Switch
checked={preferences.hide_admin_prompt_prefix}
disabled={updatePreferences.isPending}
onCheckedChange={(v) => void handleToggleHideAdmin(v)}
onCheckedChange={(value) => void handleToggleHideAdmin(value)}
/>
</div>
<div className="space-y-3 rounded-md border p-3">
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
onChange={(event) => setTitle(event.target.value)}
placeholder="名称(可选,例如:周报总结)"
/>
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
onChange={(event) => setContent(event.target.value)}
placeholder="提示词内容"
className="min-h-28"
/>
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-xs">关联智能体(可选)</p>
<div className="flex min-w-0 flex-col gap-1">
<p className="text-muted-foreground text-xs">关联智能体</p>
<Select value={agentId} onValueChange={setAgentId}>
<SelectTrigger className="w-full min-w-0 max-w-full overflow-hidden [&>span]:min-w-0 [&>span]:flex-1 [&>span]:truncate [&>span]:text-left">
<SelectValue placeholder="不关联(所有对话可见)" />
<SelectValue placeholder="请选择已配置的智能体" />
</SelectTrigger>
<SelectContent
position="popper"
className="max-h-[40vh] w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)]"
className="max-h-[40vh] w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)] overflow-hidden"
>
<SelectItem value={NO_AGENT}>不关联(所有对话可见)</SelectItem>
{selectableAgents.map((agent) => (
<SelectItem key={agent.id} value={agent.id} className="pr-8">
<SelectItem
key={agent.id}
value={agent.id}
className="max-w-full pr-8"
>
<span className="block max-w-full truncate">
{desensitize(agent.name)}
</span>
@@ -331,13 +323,18 @@ function ManagePromptsDialog({
</Select>
<p className="text-muted-foreground text-xs">
{selectableAgents.length === 0
? "仅可关联你收藏到「我的」的常用智能体;当前还没有收藏任何智能体。"
: "关联后,仅在选中该智能体的对话里显示此提示词。这里只列出你收藏的常用智能体。"}
? "当前账号还没有已配置的智能体。"
: "关联后,仅在选中该智能体的对话里显示此提示词。"}
</p>
</div>
<div className="flex justify-end gap-2">
{editingId && (
<Button variant="outline" size="sm" onClick={resetForm} disabled={saving}>
<Button
variant="outline"
size="sm"
onClick={resetForm}
disabled={saving}
>
取消编辑
</Button>
)}
@@ -48,6 +48,13 @@ export function useUserPreferences(): UserPreferences {
return data ?? DEFAULT_PREFS;
}
export function useUserPreferencesQuery() {
return useQuery({
queryKey: QUERY_KEY,
queryFn: getUserPreferences,
});
}
export function useUpdateUserPreferences() {
const queryClient = useQueryClient();
return useMutation({
+8 -2
View File
@@ -5,7 +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 { useUserPreferencesQuery } from "@/core/user-preferences";
import { ArtifactTrigger, useArtifacts } from "@/components/workspace/artifacts";
import {
ChatBox,
@@ -53,7 +53,9 @@ export default function ChatPage() {
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 { data: userPreferences, isLoading: preferencesLoading } =
useUserPreferencesQuery();
const hideAdminPrompt = userPreferences?.hide_admin_prompt_prefix === true;
const [promptPrefixOn, setPromptPrefixOn] = useState(false);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const { agents: selectorAgents, isLoading: selectorLoading } =
@@ -163,6 +165,7 @@ export default function ChatPage() {
isNewThread &&
!hasStartedConversation &&
promptPrefixOn &&
!preferencesLoading &&
!hideAdminPrompt &&
promptPrefixSettings?.enabled === true;
setHasStartedConversation(true);
@@ -195,6 +198,7 @@ export default function ChatPage() {
promptPrefixOn,
promptPrefixSettings,
hideAdminPrompt,
preferencesLoading,
selectedAgentId,
router,
settings.context.writing_mode,
@@ -223,6 +227,7 @@ export default function ChatPage() {
const promptPrefixProp = useMemo(() => {
if (!promptPrefixSettings?.enabled) return undefined;
if (preferencesLoading) 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.
@@ -235,6 +240,7 @@ export default function ChatPage() {
}, [
promptPrefixSettings,
hideAdminPrompt,
preferencesLoading,
isNewThread,
hasStartedConversation,
promptPrefixOn,
@@ -257,6 +257,7 @@ function stripTimestampSuffix(title: string): string {
}
const BODY_TAB_VALUE = "__body__";
const HTML_TAB_VALUE = "__html__";
const RUN_LIST_COLLAPSED_WIDTH = 56;
const RUN_LIST_DEFAULT_WIDTH = 380;
const RUN_LIST_MIN_WIDTH = 280;
@@ -427,12 +428,8 @@ export default function PublicScheduledTaskPage() {
className="relative flex min-h-0 shrink-0 transition-[width] duration-200 ease-out"
style={{ width: visibleRunListWidth }}
>
<Card
className={cn(
"flex min-h-0 w-full flex-col overflow-hidden",
!runListExpanded && "pointer-events-none [&>*]:invisible",
)}
>
{runListExpanded ? (
<Card className="flex min-h-0 w-full flex-col overflow-hidden">
<CardHeader className="gap-1">
<div className="flex items-start justify-between gap-2">
<Tooltip>
@@ -447,9 +444,21 @@ export default function PublicScheduledTaskPage() {
</TooltipContent>
)}
</Tooltip>
<Badge variant={task?.enabled ? "default" : "secondary"} className="shrink-0">
{task?.enabled ? "启用" : "暂停"}
</Badge>
<div className="flex shrink-0 items-center gap-1">
<Badge variant={task?.enabled ? "default" : "secondary"}>
{task?.enabled ? "启用" : "暂停"}
</Badge>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-primary size-7"
onClick={collapseRunList}
title="收起执行记录列表"
aria-label="收起执行记录列表"
>
<ChevronLeft className="size-4" />
</Button>
</div>
</div>
{task?.description && task.description.trim() && (
<p className="text-muted-foreground line-clamp-3 text-xs leading-relaxed break-all whitespace-pre-wrap">
@@ -552,15 +561,20 @@ export default function PublicScheduledTaskPage() {
</ScrollArea>
</CardContent>
</Card>
<button
type="button"
onClick={runListExpanded ? collapseRunList : expandRunList}
className="bg-background text-muted-foreground hover:bg-accent hover:text-primary focus-visible:ring-ring absolute top-3 left-1/2 z-30 flex size-9 -translate-x-1/2 items-center justify-center rounded-md border shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
title={runListExpanded ? "收起执行记录列表" : "展开执行记录列表"}
aria-label={runListExpanded ? "收起执行记录列表" : "展开执行记录列表"}
>
{runListExpanded ? <ChevronLeft className="size-4" /> : <List className="size-4" />}
</button>
) : (
<Card className="flex min-h-0 w-full flex-col items-center overflow-hidden py-3">
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-primary"
onClick={expandRunList}
title="展开执行记录列表"
aria-label="展开执行记录列表"
>
<List className="size-4" />
</Button>
</Card>
)}
<div
role="separator"
aria-orientation="vertical"
@@ -638,12 +652,14 @@ export default function PublicScheduledTaskPage() {
}
type ContentTab = {
/** Stable tab identifier — `__body__` for inline content, `file-<index>` for files. */
/** Stable tab identifier — `__html__` for the HTML preview, `__body__` for
* inline content, `file-<index>` for files. */
value: string;
/** Short label rendered in the TabsTrigger. */
label: string;
/** Distinguishes body vs MD attachment so the editor knows which PUT to call. */
kind: "body" | "file";
/** Distinguishes the HTML preview vs body vs MD attachment so the renderer /
* editor knows what to show and which PUT to call. */
kind: "body" | "file" | "html";
/** File index (when `kind === "file"`). */
fileIndex?: number;
/** Filename for downloads (defaults to the run title for body). */
@@ -684,17 +700,27 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
[run.result?.files],
);
// Tabs: every MD file first (priority per spec), then the inline body.
// Tabs: HTML preview first (when present), then every MD file, then the
// inline body. For an HTML run that also produced a Markdown file, this keeps
// the page preview AND surfaces the .md instead of hiding it. The generated
// HTML document itself is never listed as a downloadable "file" tab — it has
// its own preview.
const tabs: ContentTab[] = useMemo(() => {
const list: ContentTab[] = markdownFiles.map((file, idx) => ({
value: `file-${file.index}`,
// Show the actual filename without the `.md` suffix — the user explicitly
// asked for "no '文件' label, just the name without extension".
label: stripExtension(file.name) || `文件 ${idx + 1}`,
kind: "file",
fileIndex: file.index,
fileName: file.name,
}));
const list: ContentTab[] = [];
if (isHtmlRun) {
list.push({ value: HTML_TAB_VALUE, label: "HTML 预览", kind: "html" });
}
list.push(
...markdownFiles.map((file, idx) => ({
value: `file-${file.index}`,
// Show the actual filename without the `.md` suffix — the user explicitly
// asked for "no '文件' label, just the name without extension".
label: stripExtension(file.name) || `文件 ${idx + 1}`,
kind: "file" as const,
fileIndex: file.index,
fileName: file.name,
})),
);
list.push({
value: BODY_TAB_VALUE,
label: "正文",
@@ -702,7 +728,7 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
fileName: run.result?.title || "scheduled-task-result",
});
return list;
}, [markdownFiles, run.result?.title]);
}, [isHtmlRun, markdownFiles, run.result?.title]);
const [activeTab, setActiveTab] = useState<string>(tabs[0]?.value ?? BODY_TAB_VALUE);
@@ -843,13 +869,13 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
下载原文件
</a>
)}
{!isHtmlRun && (
{activeTabMeta?.kind !== "html" && (
<Button size="sm" variant="outline" onClick={handleDownloadWord}>
<FileText className="size-4" />
下载 Word
</Button>
)}
{!isHtmlRun &&
{activeTabMeta?.kind !== "html" &&
(isEditing ? (
<>
<Button
@@ -895,22 +921,7 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
</CardHeader>
<CardContent className="min-h-0 flex-1 overflow-hidden p-0">
{isHtmlRun ? (
<div className="h-full p-4">
{htmlContent ? (
<iframe
title="HTML 预览"
sandbox="allow-same-origin"
srcDoc={htmlContent}
className="h-full min-h-[60vh] w-full rounded-md border bg-white"
/>
) : (
<div className="text-muted-foreground rounded-md border p-3 text-sm">
HTML 页面尚未生成或正在生成中。
</div>
)}
</div>
) : tabs.length === 0 ? (
{tabs.length === 0 ? (
<div className="text-muted-foreground p-6 text-sm">无可用内容。</div>
) : (
<Tabs
@@ -940,7 +951,20 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
{run.error}
</div>
)}
{fileIsLoading ? (
{activeTabMeta?.kind === "html" ? (
htmlContent ? (
<iframe
title="HTML 预览"
sandbox="allow-same-origin"
srcDoc={htmlContent}
className="min-h-[60vh] w-full flex-1 rounded-md border bg-white"
/>
) : (
<div className="text-muted-foreground rounded-md border p-3 text-sm">
HTML 页面尚未生成或正在生成中。
</div>
)
) : fileIsLoading ? (
<div className="text-muted-foreground flex items-center gap-2 text-sm">
<Loader2 className="size-4 animate-spin" />
正在加载文件内容...
@@ -270,7 +270,16 @@ export default function ScheduledTaskRunDetailPage() {
}
};
const resultFiles = useMemo(() => result?.files ?? [], [result]);
// The generated HTML document is already shown in the dedicated "HTML 预览"
// tab, so it is excluded from the "结果文件" list — that list surfaces the
// Markdown (and any other non-HTML artifact) only.
const resultFiles = useMemo(
() =>
(result?.files ?? []).filter(
(f) => !(f.kind === "html" || (f.name ?? "").toLowerCase().endsWith(".html")),
),
[result],
);
const fileCount = resultFiles.length;
const resultMetadata = useMemo(() => {
if (!result) return "";
@@ -295,18 +304,28 @@ export default function ScheduledTaskRunDetailPage() {
}
setFilesLoading(true);
try {
const loaded = await Promise.all(
// Load each file independently. A single failing file (e.g. an archive
// that 404s) must NOT blank the whole panel — otherwise the tab shows
// "结果文件(2)" while the body says "没有生成文件". Render every file
// that loads and skip the ones that don't.
const settled = await Promise.allSettled(
resultFiles.map((f) => {
const index = result.files.indexOf(f);
return getScheduledTaskRunFileContent(run.id, index);
}),
);
if (!cancelled) {
const loaded = settled
.filter(
(r): r is PromiseFulfilledResult<ScheduledTaskResultFileContent> =>
r.status === "fulfilled",
)
.map((r) => r.value);
setFileContents(loaded);
}
} catch (error) {
if (!cancelled) {
toast.error(error instanceof Error ? error.message : "加载文件内容失败");
const failed = settled.length - loaded.length;
if (failed > 0 && loaded.length === 0) {
toast.error("结果文件已不可用(可能已被清理)");
}
}
} finally {
if (!cancelled) {
@@ -584,7 +603,9 @@ export default function ScheduledTaskRunDetailPage() {
)}
{!filesLoading && fileContents.length === 0 && (
<div className="text-muted-foreground rounded-md border p-3 text-sm">
本次执行没有生成文件。
{fileCount > 0
? `本次执行生成了 ${fileCount} 个文件,但当前无法读取(文件可能已被清理或移动)。可尝试「下载全部」。`
: "本次执行没有生成文件。"}
</div>
)}
<div className="flex flex-col gap-3">
@@ -243,6 +243,17 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
# Get newly uploaded files from the current message's additional_kwargs.files
new_files = self._files_from_kwargs(last_message, uploads_dir) or []
# Only inject when the *current* message actually carries new uploads.
# Turns without a fresh upload must not re-inject previously uploaded
# files: the original upload turn already injected (and persisted) their
# <uploaded_files> block, and the files remain physically available for
# read_file/grep/view_image. Re-injecting on every later message made the
# agent re-reference old images/docs, and — since the frontend parses
# files out of the <uploaded_files> content when additional_kwargs.files
# is empty — re-rendered the old attachment on each new question.
if not new_files:
return None
# Collect historical files from the uploads directory (all except the new ones)
new_filenames = {f["filename"] for f in new_files}
historical_files: list[dict] = []
@@ -270,9 +281,6 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
file["outline"] = outline
file["outline_preview"] = preview
if not new_files and not historical_files:
return None
logger.debug(f"New files: {[f['filename'] for f in new_files]}, historical: {[f['filename'] for f in historical_files]}")
# Create files message and prepend to the last human message content
@@ -1007,11 +1007,11 @@ class ScheduledTaskService:
if negative_prompt:
parts.append(f"限制条件(最高优先级,必须严格遵守):{negative_prompt}")
gather_prompt = "\n".join(parts)
# When the Markdown switch is also on, ask this same gather pass to save a
# .md file (the length gate is enforced in code, not surfaced here) so the
# task produces both deliverables in one agent run.
if also_markdown:
gather_prompt += _MARKDOWN_INSTRUCTION
# When the Markdown switch is also on, this same gather pass must save a
# .md file. We do NOT append _MARKDOWN_INSTRUCTION here — `_start_agent_run`
# already appends it exactly once when `require_markdown` is present in the
# execution context (kept below). Appending it here too made the prompt
# carry the 【输出要求】 block twice when both HTML and Markdown were on.
gather_task = {**task, "prompt": gather_prompt}
gather_ctx = dict(task.get("execution_context") or {})
# Strip the HTML-specific flags so the gather pass is a plain agent run.