Compare commits

..
1 Commits
Author SHA1 Message Date
gozeros 1d19260673 sync snapshot from LAN repo (source dhk-v5 @ d1e57306) 2026-11-05 11:03:03 +08:00
29 changed files with 106 additions and 652 deletions

No files matched your search

-3
View File
@@ -112,9 +112,6 @@ window.__CM_RUNTIME_CONFIG__ = {
// A 系统跳转地址:人物、组织、事件等列表项打开链接使用。 // A 系统跳转地址:人物、组织、事件等列表项打开链接使用。
VITE_AIPI_PRESENT_URL: "http://present.a.uat.4.cn/present/domain", VITE_AIPI_PRESENT_URL: "http://present.a.uat.4.cn/present/domain",
// B 系统(XWT检索)头像代理;按部署环境设置为浏览器可访问的地址。
VITE_AIPI_MINIO_PROXY_URL: "http://minio-proxy.b.sit.4.cn/facebook-proxy",
// B 系统 knowes 根地址。 // B 系统 knowes 根地址。
VITE_AIPI_KNOWES_URL: "http://ch1.b.uat.4.cn/knowes", VITE_AIPI_KNOWES_URL: "http://ch1.b.uat.4.cn/knowes",
@@ -599,8 +599,8 @@ const DocumentUploadPopupContent: FC<{
}; };
const yUserId = getYUserId(); const yUserId = getYUserId();
const notebookToken = getNotebookToken(); const userToken = getStoredAuth()?.access_token;
const iframeTokenParam = notebookToken ? `&token=${encodeURIComponent(notebookToken)}` : ""; const iframeTokenParam = userToken ? `&token=${encodeURIComponent(userToken)}` : "";
return ( return (
<Dialog open={isOpen} onOpenChange={onOpenChange}> <Dialog open={isOpen} onOpenChange={onOpenChange}>
@@ -188,12 +188,9 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
const [directionType, setDirectionType] = useState<string>('TW'); const [directionType, setDirectionType] = useState<string>('TW');
const directionValue = useRef<string>('TW'); const directionValue = useRef<string>('TW');
const consumerUrl = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || 'https://ch1.b.uat.4.cn/consumer'; const consumerUrl = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || 'https://ch1.b.uat.4.cn/consumer';
const minioProxyUrl = readRuntimeEnv("VITE_AIPI_MINIO_PROXY_URL") || 'http://minio-proxy.b.sit.4.cn/facebook-proxy'; const backendUrl = typeof window !== 'undefined'
const actorBaseUrl = readRuntimeEnv("VITE_AIPI_B2ACTOR_URL") || 'http://ty.b.uat.4.cn'; ? (readRuntimeEnv("VITE_BACKEND_BASE_URL") || 'http://localhost:8001')
const avatarUrl = (profileImage: string | null) => profileImage : 'http://localhost:8001';
? `${minioProxyUrl}?${new URLSearchParams({ url: profileImage, is_avatar: 'true' })}`
: `${actorBaseUrl}/manage/img/default.png`;
const backendUrl = readRuntimeEnv("VITE_AIPI_BACKENP_URL") || 'http://localhost:8001';
const [selectedSearchSources, setSelectedSearchSources] = useState<UserDataItem[]>([]); const [selectedSearchSources, setSelectedSearchSources] = useState<UserDataItem[]>([]);
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
@@ -291,7 +288,9 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
setIsSearching(false); setIsSearching(false);
const result: UserDataItem[] = data.data.list; const result: UserDataItem[] = data.data.list;
result.forEach((item) => { result.forEach((item) => {
item.profile_image = avatarUrl(item.profile_image); item.profile_image = item.profile_image
? `http://minio-proxy.b.sit.4.cn/facebook-proxy?url=${item.profile_image}&is_avatar=true`
: 'http://ty.b.uat.4.cn/manage/img/default.png';
item.classification = `${item.political_stand}-${item.history_key}-${item.history_title}`; item.classification = `${item.political_stand}-${item.history_key}-${item.history_title}`;
item.title = item.user_name || item.screen_name || item.user_id; item.title = item.user_name || item.screen_name || item.user_id;
item.content = item.agentInfo['账号描述']; item.content = item.agentInfo['账号描述'];
@@ -331,7 +330,9 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
const result: UserDataItem[] = data.data.data; const result: UserDataItem[] = data.data.data;
result.forEach((item) => { result.forEach((item) => {
item.hobbies = item.groupDiscussTags || []; item.hobbies = item.groupDiscussTags || [];
item.profile_image = avatarUrl(item.profile_image); item.profile_image = item.profile_image
? `http://minio-proxy.b.sit.4.cn/facebook-proxy?url=${item.profile_image}&is_avatar=true`
: 'http://ty.b.uat.4.cn/manage/img/default.png';
item.classification = `${item.political_stand}`; item.classification = `${item.political_stand}`;
item.title = item.group_name || item.groupName; item.title = item.group_name || item.groupName;
if (item.hx_info?.['用户画像']) { if (item.hx_info?.['用户画像']) {
@@ -16,7 +16,6 @@ export function ContextWindowSettingsPage() {
const update = useUpdateConversationContextSettings(); const update = useUpdateConversationContextSettings();
const enabled = data?.recent_turn_window_enabled ?? false; const enabled = data?.recent_turn_window_enabled ?? false;
const recentTurns = data?.recent_turns ?? 3; const recentTurns = data?.recent_turns ?? 3;
const useQuestionAsTitle = data?.use_question_as_title ?? false;
const handleChange = async (checked: boolean) => { const handleChange = async (checked: boolean) => {
try { try {
@@ -31,19 +30,6 @@ export function ContextWindowSettingsPage() {
} }
}; };
const handleQuestionTitleChange = async (checked: boolean) => {
try {
await update.mutateAsync({ use_question_as_title: checked });
toast.success(
checked
? "已开启:对话标题直接使用首个问题"
: "已关闭:恢复回答后的标题生成",
);
} catch (error) {
toast.error(error instanceof Error ? error.message : "保存失败");
}
};
return ( return (
<section className="space-y-6"> <section className="space-y-6">
<div className="space-y-1"> <div className="space-y-1">
@@ -76,25 +62,6 @@ export function ContextWindowSettingsPage() {
)} )}
</div> </div>
<div className="flex items-center justify-between gap-6 rounded-lg border p-4">
<div className="space-y-1">
<div className="font-medium">标题直接使用首个问题</div>
<p className="text-muted-foreground text-sm">
开启后,首个问题会立即成为最终标题;回答结束后不再额外调用模型生成标题,也不轮询标题状态,避免影响下一轮提问。关闭后恢复原来的回答后标题生成方式。
</p>
</div>
{isLoading ? (
<Loader2Icon className="text-muted-foreground size-5 animate-spin" />
) : (
<Switch
checked={useQuestionAsTitle}
disabled={isError || update.isPending}
onCheckedChange={handleQuestionTitleChange}
aria-label="标题直接使用首个问题"
/>
)}
</div>
{isError && ( {isError && (
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<p className="text-destructive text-sm"> <p className="text-destructive text-sm">
@@ -1,6 +1,6 @@
"use client"; "use client";
import { Clock3Icon, FileOutputIcon, ListOrderedIcon, Loader2Icon, UsersIcon } from "lucide-react"; import { Clock3Icon, ListOrderedIcon, Loader2Icon, UsersIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -9,7 +9,6 @@ import { Switch } from "@/components/ui/switch";
import { import {
useRoundtableRoomLimitSettings, useRoundtableRoomLimitSettings,
useUpdateRoundtableRoomLimitSettings, useUpdateRoundtableRoomLimitSettings,
useWriteFileCompatibilitySettings,
} from "@/core/system-settings/hooks"; } from "@/core/system-settings/hooks";
import { loadRoundtableRoomSummary } from "@/roundtable-planning/api/rooms"; import { loadRoundtableRoomSummary } from "@/roundtable-planning/api/rooms";
@@ -17,10 +16,7 @@ export function RoundtableRoomSettingsPage() {
const { data, isLoading, isError, isFetching, refetch } = const { data, isLoading, isError, isFetching, refetch } =
useRoundtableRoomLimitSettings(); useRoundtableRoomLimitSettings();
const update = useUpdateRoundtableRoomLimitSettings(); const update = useUpdateRoundtableRoomLimitSettings();
const globalFileStream = useWriteFileCompatibilitySettings();
const enabled = data?.enabled ?? true; const enabled = data?.enabled ?? true;
const fileStreamEnabled = data?.write_file_compatibility_enabled ?? false;
const globalFileStreamEnabled = globalFileStream.data?.enabled ?? false;
const [maxRooms, setMaxRooms] = useState(String(data?.max_rooms ?? 3)); const [maxRooms, setMaxRooms] = useState(String(data?.max_rooms ?? 3));
const [maxHoldMinutes, setMaxHoldMinutes] = useState( const [maxHoldMinutes, setMaxHoldMinutes] = useState(
String(data?.max_hold_minutes ?? 30), String(data?.max_hold_minutes ?? 30),
@@ -66,7 +62,6 @@ export function RoundtableRoomSettingsPage() {
max_rooms?: number; max_rooms?: number;
max_hold_minutes?: number; max_hold_minutes?: number;
max_cycles?: number; max_cycles?: number;
write_file_compatibility_enabled?: boolean;
}) => { }) => {
try { try {
await update.mutateAsync(next); await update.mutateAsync(next);
@@ -220,37 +215,6 @@ export function RoundtableRoomSettingsPage() {
</Button> </Button>
</div> </div>
<div className="flex items-center justify-between gap-6 rounded-lg border p-4">
<div className="space-y-1">
<div className="flex items-center gap-2 font-medium">
<FileOutputIcon className="size-4" />
会商文件流式兼容
</div>
<p className="text-muted-foreground text-sm">
会商生成文件并打开沙箱时使用兼容写入。全局「文件流式兼容」开着时,会商自动使用;全局关着时,打开本开关后只有会商使用,普通对话保持原来的写入方式。
</p>
<p className="text-muted-foreground text-xs">
{globalFileStreamEnabled
? "当前会商会使用兼容写入,因为全局开关已开启。"
: fileStreamEnabled
? "当前只有会商使用兼容写入。"
: "当前会商使用各模型原来的写入方式。"}
</p>
</div>
{isLoading ? (
<Loader2Icon className="text-muted-foreground size-5 animate-spin" />
) : (
<Switch
checked={fileStreamEnabled}
disabled={isError || update.isPending}
onCheckedChange={(checked) =>
void save({ write_file_compatibility_enabled: checked })
}
aria-label="会商启用文件流式兼容"
/>
)}
</div>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
当前全站占用:{activeCount == null ? "暂时无法读取" : `${activeCount} / ${data?.max_rooms ?? maxRooms}`} 当前全站占用:{activeCount == null ? "暂时无法读取" : `${activeCount} / ${data?.max_rooms ?? maxRooms}`}
</p> </p>
@@ -1,14 +1,6 @@
import { apiFetch } from "@/core/api"; import { apiFetch } from "@/core/api";
import { getBackendBaseURL } from "@/core/config"; import { getBackendBaseURL } from "@/core/config";
export type ModelWriteFileTransport =
| "native"
| "buffered_gateway"
| "artifact_renderer"
| "text_bridge";
export type ModelWriteFileTransportToggle = "native" | "buffered_gateway";
export interface ManagedModel { export interface ManagedModel {
name: string; name: string;
display_name: string | null; display_name: string | null;
@@ -31,7 +23,6 @@ export interface ManagedModel {
thinking: Record<string, unknown> | null; thinking: Record<string, unknown> | null;
use_responses_api: boolean | null; use_responses_api: boolean | null;
output_version: string | null; output_version: string | null;
write_file_transport: ModelWriteFileTransport;
extra_settings: Record<string, unknown>; extra_settings: Record<string, unknown>;
api_key_configured: boolean; api_key_configured: boolean;
api_key_source: string | null; api_key_source: string | null;
@@ -129,22 +120,6 @@ export async function updateManagedModelReasoningEffortCapability(
return response.json() as Promise<ManagedModel>; return response.json() as Promise<ManagedModel>;
} }
export async function updateManagedModelWriteFileTransport(
name: string,
writeFileTransport: ModelWriteFileTransportToggle,
): Promise<ManagedModel> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/admin/models/write-file-transport/${encodeURIComponent(name)}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ write_file_transport: writeFileTransport }),
},
);
if (!response.ok) return readError(response, "保存文件流式兼容设置失败");
return response.json() as Promise<ManagedModel>;
}
export async function deleteManagedModel(name: string): Promise<void> { export async function deleteManagedModel(name: string): Promise<void> {
const response = await apiFetch(`${getBackendBaseURL()}/api/admin/models/${encodeURIComponent(name)}`, { const response = await apiFetch(`${getBackendBaseURL()}/api/admin/models/${encodeURIComponent(name)}`, {
method: "DELETE", method: "DELETE",
@@ -8,9 +8,7 @@ import {
testManagedModel, testManagedModel,
updateManagedModel, updateManagedModel,
updateManagedModelReasoningEffortCapability, updateManagedModelReasoningEffortCapability,
updateManagedModelWriteFileTransport,
type ModelSavePayload, type ModelSavePayload,
type ModelWriteFileTransportToggle,
} from "./api"; } from "./api";
export const MANAGED_MODELS_QUERY_KEY = ["admin", "model-config"] as const; export const MANAGED_MODELS_QUERY_KEY = ["admin", "model-config"] as const;
@@ -68,25 +66,6 @@ export function useUpdateManagedModelReasoningEffortCapability() {
}); });
} }
export function useUpdateManagedModelWriteFileTransport() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
name,
writeFileTransport,
}: {
name: string;
writeFileTransport: ModelWriteFileTransportToggle;
}) => updateManagedModelWriteFileTransport(name, writeFileTransport),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: MANAGED_MODELS_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ["models"] }),
]);
},
});
}
export function useDeleteManagedModel() { export function useDeleteManagedModel() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
+1 -5
View File
@@ -728,12 +728,10 @@ export interface MemoryRuntimeSettings {
export interface ConversationContextSettings { export interface ConversationContextSettings {
recent_turn_window_enabled: boolean; recent_turn_window_enabled: boolean;
recent_turns: number; recent_turns: number;
use_question_as_title: boolean;
} }
export interface ConversationContextUpdate { export interface ConversationContextUpdate {
recent_turn_window_enabled?: boolean; recent_turn_window_enabled: boolean;
use_question_as_title?: boolean;
} }
export interface RoundtableRoomLimitSettings { export interface RoundtableRoomLimitSettings {
@@ -741,7 +739,6 @@ export interface RoundtableRoomLimitSettings {
max_rooms: number; max_rooms: number;
max_hold_minutes: number; max_hold_minutes: number;
max_cycles: number; max_cycles: number;
write_file_compatibility_enabled: boolean;
} }
export interface RoundtableRoomLimitUpdate { export interface RoundtableRoomLimitUpdate {
@@ -749,7 +746,6 @@ export interface RoundtableRoomLimitUpdate {
max_rooms?: number; max_rooms?: number;
max_hold_minutes?: number; max_hold_minutes?: number;
max_cycles?: number; max_cycles?: number;
write_file_compatibility_enabled?: boolean;
} }
export interface OrdinaryKnowledgeRetrievalSettings { export interface OrdinaryKnowledgeRetrievalSettings {
+43 -43
View File
@@ -1340,11 +1340,11 @@ export function useThreadStream({
runId = inflight?.run_id ?? null; runId = inflight?.run_id ?? null;
if (runId) { if (runId) {
activeRunIdRef.current = runId; activeRunIdRef.current = runId;
} else { } else {
const lastTerminal = runs.find((item) => const lastTerminal = runs.find((item) =>
TERMINAL_RUN_STATUSES.has(item.status), TERMINAL_RUN_STATUSES.has(item.status),
); );
if (lastTerminal) terminalStatus = lastTerminal.status; if (lastTerminal) terminalStatus = lastTerminal.status;
} }
} catch { } catch {
if (!isCurrentRecovery()) return "unknown"; if (!isCurrentRecovery()) return "unknown";
@@ -1357,45 +1357,45 @@ export function useThreadStream({
} }
} }
if (terminalStatus) { if (terminalStatus) {
try { try {
return (await applyTerminalState(terminalStatus)) return (await applyTerminalState(terminalStatus))
? "terminal" ? "terminal"
: "unknown"; : "unknown";
} catch { } catch {
if (isCurrentRecovery()) { if (isCurrentRecovery()) {
lastStreamActivityAtRef.current = Date.now(); lastStreamActivityAtRef.current = Date.now();
setStreamHoldOpen(true); setStreamHoldOpen(true);
} }
return "unknown"; return "unknown";
} }
} }
if (!runId) { if (!runId) {
// The backend confirms there is no in-flight run, but an SDK reader // The backend confirms there is no in-flight run, but an SDK reader
// can remain locally open after a proxy/SSE disconnect. Detach that // can remain locally open after a proxy/SSE disconnect. Detach that
// reader only after removing its reconnect marker so stop() cannot // reader only after removing its reconnect marker so stop() cannot
// accidentally cancel a server run. // accidentally cancel a server run.
try { try {
const readerStopped = await stopRecoveredRunReader({ const readerStopped = await stopRecoveredRunReader({
isCurrent: isCurrentRecovery, isCurrent: isCurrentRecovery,
clearReconnect: () => clearActiveRunId(tid), clearReconnect: () => clearActiveRunId(tid),
stop: () => threadStreamRef.current?.stop(), stop: () => threadStreamRef.current?.stop(),
}); });
if (!readerStopped) return "unknown"; if (!readerStopped) return "unknown";
activeRunIdRef.current = null; activeRunIdRef.current = null;
setStreamHoldOpen(false); setStreamHoldOpen(false);
streamReconnectAttemptsRef.current = 0; streamReconnectAttemptsRef.current = 0;
backgroundHistory.confirmTerminal(); backgroundHistory.confirmTerminal();
void backgroundHistory.refresh(false); void backgroundHistory.refresh(false);
return "terminal"; return "terminal";
} catch { } catch {
if (isCurrentRecovery()) { if (isCurrentRecovery()) {
lastStreamActivityAtRef.current = Date.now(); lastStreamActivityAtRef.current = Date.now();
setStreamHoldOpen(true); setStreamHoldOpen(true);
} }
return "unknown"; return "unknown";
} }
} }
// A second UI failure can pause recovery while /runs is in flight. // A second UI failure can pause recovery while /runs is in flight.
@@ -22,7 +22,6 @@ import {
CircleAlertIcon, CircleAlertIcon,
CpuIcon, CpuIcon,
EyeIcon, EyeIcon,
FileOutputIcon,
GripVerticalIcon, GripVerticalIcon,
KeyRoundIcon, KeyRoundIcon,
LoaderCircleIcon, LoaderCircleIcon,
@@ -55,9 +54,7 @@ import {
useSaveManagedModel, useSaveManagedModel,
useTestManagedModel, useTestManagedModel,
useUpdateManagedModelReasoningEffortCapability, useUpdateManagedModelReasoningEffortCapability,
useUpdateManagedModelWriteFileTransport,
} from "@/core/model-management"; } from "@/core/model-management";
import { useWriteFileCompatibilitySettings } from "@/core/system-settings/hooks";
import { import {
WorkspaceBody, WorkspaceBody,
WorkspaceContainer, WorkspaceContainer,
@@ -200,44 +197,25 @@ function SortableModelCard({
dragEnabled, dragEnabled,
reasoningCapabilityDisabled, reasoningCapabilityDisabled,
reasoningCapabilityPending, reasoningCapabilityPending,
globalWriteFileCompatEnabled,
writeFileCompatPending,
onConfigure, onConfigure,
onDelete, onDelete,
onReasoningCapabilityChange, onReasoningCapabilityChange,
onReasoningEffortChange, onReasoningEffortChange,
onWriteFileCompatChange,
}: { }: {
model: ManagedModel; model: ManagedModel;
isDefault: boolean; isDefault: boolean;
dragEnabled: boolean; dragEnabled: boolean;
reasoningCapabilityDisabled: boolean; reasoningCapabilityDisabled: boolean;
reasoningCapabilityPending: boolean; reasoningCapabilityPending: boolean;
globalWriteFileCompatEnabled: boolean;
writeFileCompatPending: boolean;
onConfigure: () => void; onConfigure: () => void;
onDelete: () => void; onDelete: () => void;
onReasoningCapabilityChange: (enabled: boolean) => void; onReasoningCapabilityChange: (enabled: boolean) => void;
onReasoningEffortChange: (effort: "low" | "medium" | "xhigh") => void; onReasoningEffortChange: (effort: "low" | "medium" | "xhigh") => void;
onWriteFileCompatChange: (enabled: boolean) => void;
}) { }) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: model.name, id: model.name,
disabled: !dragEnabled, disabled: !dragEnabled,
}); });
const writeFileCompatEligible =
(model.use === "langchain_openai:ChatOpenAI" ||
model.use === "deerflow.models.patched_openai:PatchedChatOpenAI") &&
!model.use_responses_api;
const perModelWriteFileCompat = model.write_file_transport === "buffered_gateway";
const writeFileCompatActive = perModelWriteFileCompat || globalWriteFileCompatEnabled;
const writeFileCompatHint = globalWriteFileCompatEnabled
? "全局文件流式兼容已开启,所有兼容模型均已强制启用;关闭全局开关后恢复此处的单独设置。"
: !writeFileCompatEligible
? "仅支持 OpenAI 兼容接口(Chat Completions)模型。"
: perModelWriteFileCompat
? "该模型已启用:文件正文改由普通文本流生成,适配缓存长工具参数的网关。"
: "针对会缓存长工具参数的内网模型网关;开启后文件正文改由普通文本流生成,对问答与文件卡片完全透明。";
return ( return (
<article <article
@@ -310,9 +288,6 @@ function SortableModelCard({
{model.supports_vision && ( {model.supports_vision && (
<ModelCapabilityBadge icon={EyeIcon} label="视觉" className="bg-amber-500/10 text-amber-700 dark:text-amber-300" /> <ModelCapabilityBadge icon={EyeIcon} label="视觉" className="bg-amber-500/10 text-amber-700 dark:text-amber-300" />
)} )}
{writeFileCompatActive && writeFileCompatEligible && (
<ModelCapabilityBadge icon={FileOutputIcon} label="文件流式兼容" className="bg-sky-500/10 text-sky-700 dark:text-sky-300" />
)}
{model.supports_reasoning_effort && ( {model.supports_reasoning_effort && (
<ModelCapabilityBadge label={`全局默认:${model.default_reasoning_effort === "low" ? "低" : model.default_reasoning_effort === "medium" ? "中" : "Ultra"}`} className="bg-muted text-muted-foreground" /> <ModelCapabilityBadge label={`全局默认:${model.default_reasoning_effort === "low" ? "低" : model.default_reasoning_effort === "medium" ? "中" : "Ultra"}`} className="bg-muted text-muted-foreground" />
)} )}
@@ -357,22 +332,6 @@ function SortableModelCard({
/> />
</div> </div>
)} )}
<div className="mt-2 flex items-center justify-between gap-4 rounded-lg border border-border px-3 py-2.5">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 text-sm font-medium">
<span>文件流式兼容</span>
<Tag theme="default" variant="light" size="small">config.yaml</Tag>
{globalWriteFileCompatEnabled && <Tag theme="primary" variant="light" size="small">全局已开启</Tag>}
</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">{writeFileCompatHint}</p>
</div>
<Switch
value={writeFileCompatActive}
loading={writeFileCompatPending}
disabled={globalWriteFileCompatEnabled || !writeFileCompatEligible}
onChange={(value) => onWriteFileCompatChange(Boolean(value))}
/>
</div>
</article> </article>
); );
} }
@@ -487,9 +446,6 @@ export default function ModelManagementPage() {
const reorder = useReorderManagedModels(); const reorder = useReorderManagedModels();
const updateReasoningCapability = const updateReasoningCapability =
useUpdateManagedModelReasoningEffortCapability(); useUpdateManagedModelReasoningEffortCapability();
const updateWriteFileTransport = useUpdateManagedModelWriteFileTransport();
const { data: writeFileCompatSettings } = useWriteFileCompatibilitySettings();
const globalWriteFileCompatEnabled = writeFileCompatSettings?.enabled ?? false;
const sourceModels = data?.models ?? []; const sourceModels = data?.models ?? [];
const orderedModels = useMemo(() => { const orderedModels = useMemo(() => {
if (!pendingOrder || pendingOrder.length !== sourceModels.length) return sourceModels; if (!pendingOrder || pendingOrder.length !== sourceModels.length) return sourceModels;
@@ -555,26 +511,6 @@ export default function ModelManagementPage() {
showGlobalMessage("error", error instanceof Error ? error.message : "保存全局思考强度失败"); showGlobalMessage("error", error instanceof Error ? error.message : "保存全局思考强度失败");
} }
}; };
const handleWriteFileCompatChange = async (
model: ManagedModel,
enabled: boolean,
) => {
try {
await updateWriteFileTransport.mutateAsync({
name: model.name,
writeFileTransport: enabled ? "buffered_gateway" : "native",
});
showGlobalMessage(
"success",
`模型“${model.display_name || model.name}”的文件流式兼容已${enabled ? "开启" : "关闭"},从下一次发起的问答开始生效。`,
);
} catch (error) {
showGlobalMessage(
"error",
error instanceof Error ? error.message : "保存文件流式兼容设置失败",
);
}
};
const dragEnabled = filter === "all" && !search.trim() && !reorder.isPending; const dragEnabled = filter === "all" && !search.trim() && !reorder.isPending;
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
@@ -701,11 +637,6 @@ export default function ModelManagementPage() {
updateReasoningCapability.isPending && updateReasoningCapability.isPending &&
updateReasoningCapability.variables?.name === model.name updateReasoningCapability.variables?.name === model.name
} }
globalWriteFileCompatEnabled={globalWriteFileCompatEnabled}
writeFileCompatPending={
updateWriteFileTransport.isPending &&
updateWriteFileTransport.variables?.name === model.name
}
onConfigure={() => setEditor(model)} onConfigure={() => setEditor(model)}
onDelete={() => setDeleteTarget(model)} onDelete={() => setDeleteTarget(model)}
onReasoningCapabilityChange={(enabled) => onReasoningCapabilityChange={(enabled) =>
@@ -714,9 +645,6 @@ export default function ModelManagementPage() {
onReasoningEffortChange={(effort) => onReasoningEffortChange={(effort) =>
void handleReasoningEffortChange(model, effort) void handleReasoningEffortChange(model, effort)
} }
onWriteFileCompatChange={(enabled) =>
void handleWriteFileCompatChange(model, enabled)
}
/> />
))} ))}
</div> </div>
+1 -1
View File
@@ -1489,7 +1489,7 @@ scheduler runs once per deployment. Per run:
- **可检测的模型失败**:`LLMErrorHandlingMiddleware` 在同一模型重试 3 次后**不抛异常**,而是返回兜底 `AIMessage` —— 现在给它打 `additional_kwargs[LLM_ERROR_MARKER="deerflow_llm_error"]=reason`(model_dump 保留,经 checkpointer 序列化仍在)。`is_llm_error_message(msg)` 据标记(回退按已知兜底文案正文)识别,返回 reason。 - **可检测的模型失败**:`LLMErrorHandlingMiddleware` 在同一模型重试 3 次后**不抛异常**,而是返回兜底 `AIMessage` —— 现在给它打 `additional_kwargs[LLM_ERROR_MARKER="deerflow_llm_error"]=reason`(model_dump 保留,经 checkpointer 序列化仍在)。`is_llm_error_message(msg)` 据标记(回退按已知兜底文案正文)识别,返回 reason。
- **候选模型链**:保留真正模型故障后的自动换模,但“仅有思考 / 暂时无正文 / 空交付”不再判作模型故障,绝不据此切换用户选择的模型。`app/gateway/roundtable_model_fallback.py::fallback_model_chain(current)` 按 `config.yaml models[]` 顺序给出 `[当前模型, …其余]`(去重保序)。`reason_text(reason)` 翻译成简短中文。总开关 env `ROUNDTABLE_MODEL_FALLBACK`(默认开,`0/false/off` 关);`ROUNDTABLE_MODEL_FALLBACK_MAX`(0=试完全部)。 - **候选模型链**:保留真正模型故障后的自动换模,但“仅有思考 / 暂时无正文 / 空交付”不再判作模型故障,绝不据此切换用户选择的模型。`app/gateway/roundtable_model_fallback.py::fallback_model_chain(current)` 按 `config.yaml models[]` 顺序给出 `[当前模型, …其余]`(去重保序)。`reason_text(reason)` 翻译成简短中文。总开关 env `ROUNDTABLE_MODEL_FALLBACK`(默认开,`0/false/off` 关);`ROUNDTABLE_MODEL_FALLBACK_MAX`(0=试完全部)。
- **触发换模型的情形**:① 单轮 run 抛异常(start_run 失败 / idle 卡死 → `timeout`,其余 `exception`);② 兜底错误文案(`is_llm_error_message`)。交付无效、仅有思考、空串以及普通总控轮既无正文又无派活**均不触发换模**;唯一例外是无工具的最终收口请求已经完整结束仍无正文,此时结果客观不可交付,才继续自动换下一个模型。席位仍只认剥掉 `<think>` 后的可见正文(`deerflow.agents.roundtable_orchestrator.delivery`:`visible_seat_text` / `classify_seat_delivery`):前台 `_special_run` 对无效交付 yield `status:"invalid_delivery"` 且不广播;`GET /last-ai-message` 回 `{delivered, content, length, invalid_reason}`(思考-only 标 `thinking_only`,总控席位清单标 ⚠️)。后台引擎只把可见正文写入 `deliveries`,无效席位继续走既有未交付处理,不切换模型。Step3 单例(report/summary/dashboard/ingest/结构化)的产物是落盘文件,同样只对真模型错误换模型。 - **触发换模型的情形**:① 单轮 run 抛异常(start_run 失败 / idle 卡死 → `timeout`,其余 `exception`);② 兜底错误文案(`is_llm_error_message`)。交付无效、仅有思考、空串以及普通总控轮既无正文又无派活**均不触发换模**;唯一例外是无工具的最终收口请求已经完整结束仍无正文,此时结果客观不可交付,才继续自动换下一个模型。席位仍只认剥掉 `<think>` 后的可见正文(`deerflow.agents.roundtable_orchestrator.delivery`:`visible_seat_text` / `classify_seat_delivery`):前台 `_special_run` 对无效交付 yield `status:"invalid_delivery"` 且不广播;`GET /last-ai-message` 回 `{delivered, content, length, invalid_reason}`(思考-only 标 `thinking_only`,总控席位清单标 ⚠️)。后台引擎只把可见正文写入 `deliveries`,无效席位继续走既有未交付处理,不切换模型。Step3 单例(report/summary/dashboard/ingest/结构化)的产物是落盘文件,同样只对真模型错误换模型。
- **Step2 总控工具与可配置收口轮数**:普通总控使用真正白名单,只允许 `agent_orchestration` / `ask_clarification`;`write_file`、`present_files`、技能目录与文件/命令工具均不可见。`system_settings.json -> roundtable_room_limit.max_cycles`(默认 8,范围 2..100;管理员在设置弹窗「会商并发与收口」中修改)控制 recommend 模式到第几轮切换为显式 `finalize_only`:空工具白名单、注入已有有效席位全文、要求直接输出最终总结(失败席位如实标缺口),不能表现为“已停止”。一次会商启动时锁定当时值,运行中修改从下一次研讨生效;前台实时会商与后台作业读取同一设置。同一页的「会商文件流式兼容」(`write_file_compatibility_enabled`,默认关) 只作用于会商总控、席位、第三步和后台作业:全局文件流式兼容开着时会商一并使用;全局关着而本开关开着时,只有这些会商 run 改走 `buffered_gateway`。普通对话不读这个开关。chain / dag 的最终总结仍在既定席位完成后走同一纯正文路径;LangGraph 单轮 recursion limit 仍为 1000。 - **Step2 总控工具与可配置收口轮数**:普通总控使用真正白名单,只允许 `agent_orchestration` / `ask_clarification`;`write_file`、`present_files`、技能目录与文件/命令工具均不可见。`system_settings.json -> roundtable_room_limit.max_cycles`(默认 8,范围 2..100;管理员在设置弹窗「会商并发与收口」中修改)控制 recommend 模式到第几轮切换为显式 `finalize_only`:空工具白名单、注入已有有效席位全文、要求直接输出最终总结(失败席位如实标缺口),不能表现为“已停止”。一次会商启动时锁定当时值,运行中修改从下一次研讨生效;前台实时会商与后台作业读取同一设置。chain / dag 的最终总结仍在既定席位完成后走同一纯正文路径;LangGraph 单轮 recursion limit 仍为 1000。
- **后台**:`InProcessRoundtableGateway._run_turn_with_fallback(...)` 包住 `_run_agent_turn`,逐候选模型试;换模型落 `model_switch` 诊断、全失败落 `model_fallback_exhausted`。`run_leader/run_seat/run_report/run_summary/run_dashboard_data` 全走它(report/summary/dashboard 的产物自愈重试保留,并记住已切到的可用模型用于后续续写轮)。各 Turn/Result 带回 `switches: list[ModelSwitch]`,引擎 `_emit_switches` → `JobProgressWriter.model_switch(...)` 往研讨时间线插一条 `role="system"` 系统提示气泡。 - **后台**:`InProcessRoundtableGateway._run_turn_with_fallback(...)` 包住 `_run_agent_turn`,逐候选模型试;换模型落 `model_switch` 诊断、全失败落 `model_fallback_exhausted`。`run_leader/run_seat/run_report/run_summary/run_dashboard_data` 全走它(report/summary/dashboard 的产物自愈重试保留,并记住已切到的可用模型用于后续续写轮)。各 Turn/Result 带回 `switches: list[ModelSwitch]`,引擎 `_emit_switches` → `JobProgressWriter.model_switch(...)` 往研讨时间线插一条 `role="system"` 系统提示气泡。
- **前台**:`_special_run`/`_leader_run` stream 收尾后仅检测明确的模型错误标记;命中才 yield `{status:"model_switch", role, agent_name, failed_model, next_model, reason}` 帧并用下一个模型重新流式,思考-only/空正文不进入该分支;终态帧带 `model_used`。结构化抽取 `flow_extract.py` 的 round1 `model.astream` 也包了同款换模型循环(emit `event:"model_switch"` 帧)。 - **前台**:`_special_run`/`_leader_run` stream 收尾后仅检测明确的模型错误标记;命中才 yield `{status:"model_switch", role, agent_name, failed_model, next_model, reason}` 帧并用下一个模型重新流式,思考-only/空正文不进入该分支;终态帧带 `model_used`。结构化抽取 `flow_extract.py` 的 round1 `model.astream` 也包了同款换模型循环(emit `event:"model_switch"` 帧)。
- **Step2 单席位隔离**:`engine._run_recommend/_run_chain/_run_dag` 的每个席位执行包 try/except —— 一个席位彻底失败(落 `seat_failed` 诊断 + 置空交付)不拖垮整批/整链/整个作业。 - **Step2 单席位隔离**:`engine._run_recommend/_run_chain/_run_dag` 的每个席位执行包 try/except —— 一个席位彻底失败(落 `seat_failed` 诊断 + 置空交付)不拖垮整批/整链/整个作业。
@@ -36,7 +36,6 @@ from app.gateway.routers._roundtable_seed import (
SUMMARY_AGENT_ID, SUMMARY_AGENT_ID,
ensure_roundtable_functional_agents, ensure_roundtable_functional_agents,
) )
from app.gateway.routers.multi_agent import _roundtable_file_stream_context
from app.gateway.services import RUN_SCOPE_BACKGROUND_CHILD from app.gateway.services import RUN_SCOPE_BACKGROUND_CHILD
from deerflow.agents.roundtable_orchestrator.delivery import classify_seat_delivery, visible_seat_text from deerflow.agents.roundtable_orchestrator.delivery import classify_seat_delivery, visible_seat_text
from deerflow.agents.roundtable_orchestrator.report_json import extract_report_json from deerflow.agents.roundtable_orchestrator.report_json import extract_report_json
@@ -366,7 +365,6 @@ async def _run_agent_turn(
# 被上轮会话 memory.json 带偏(臆造席位 / 直接干活)。详见该处注释。 # 被上轮会话 memory.json 带偏(臆造席位 / 直接干活)。详见该处注释。
"memory_injection_enabled": False, "memory_injection_enabled": False,
"allowed_tools": allowed_tools, "allowed_tools": allowed_tools,
**_roundtable_file_stream_context(),
}, },
excluded_tools=excluded_tools, excluded_tools=excluded_tools,
skill_stop_names=skill_stop_names, skill_stop_names=skill_stop_names,
@@ -224,12 +224,6 @@ class ReasoningEffortCapabilityRequest(BaseModel):
default_reasoning_effort: Literal["low", "medium", "xhigh"] | None = None default_reasoning_effort: Literal["low", "medium", "xhigh"] | None = None
class WriteFileTransportToggleRequest(BaseModel):
"""Per-model 文件流式兼容 switch: off keeps native, on selects buffered_gateway."""
write_file_transport: Literal["native", "buffered_gateway"]
class ModelOrderRequest(BaseModel): class ModelOrderRequest(BaseModel):
"""The complete ordered set of model stable names.""" """The complete ordered set of model stable names."""
@@ -319,39 +313,6 @@ def _validate_reasoning_capability(model: dict[str, Any], enabled: bool) -> None
) )
# ``write_file_transport=buffered_gateway`` is implemented only by the patched
# OpenAI-compatible chat-completions classes (see deerflow.models.factory);
# every other driver is rejected there at construction time, so mirror the
# same constraint here with a clearer message.
_BUFFERED_GATEWAY_COMPATIBLE_USES = frozenset(
{
"langchain_openai:ChatOpenAI",
"deerflow.models.patched_openai:PatchedChatOpenAI",
}
)
def _validate_write_file_transport(model: dict[str, Any], transport: str) -> None:
if transport != WriteFileTransport.BUFFERED_GATEWAY.value:
return
if str(model.get("use") or "") not in _BUFFERED_GATEWAY_COMPATIBLE_USES:
raise HTTPException(
status_code=422,
detail=(
"write_file compatibility requires an OpenAI-compatible "
"langchain_openai:ChatOpenAI model"
),
)
if bool(model.get("use_responses_api")):
raise HTTPException(
status_code=422,
detail=(
"write_file compatibility requires the chat-completions API, "
"not use_responses_api"
),
)
async def _restore_capability( async def _restore_capability(
store: ModelCapabilityStore, store: ModelCapabilityStore,
model_name: str, model_name: str,
@@ -711,48 +672,6 @@ async def update_reasoning_effort_capability(
) )
@router.put(
"/write-file-transport/{model_name:path}",
response_model=ModelAdminItem,
)
async def update_write_file_transport(
model_name: str,
body: WriteFileTransportToggleRequest,
request: Request,
) -> ModelAdminItem:
"""Toggle one model's buffered-gateway write_file compatibility in config.yaml.
The global system-settings switch keeps overriding every compatible model
while it is enabled; this per-model value becomes effective as soon as the
global switch is turned off.
"""
await _require_admin(request)
async with _MUTATION_LOCK:
path = _resolve_config_path()
raw_before, _, models = await asyncio.to_thread(_read_config_data, path)
index = next((i for i, item in enumerate(models) if str(item.get("name")) == model_name), None)
if index is None:
raise HTTPException(status_code=404, detail=f"Model '{model_name}' was not found")
updated = copy.deepcopy(models[index])
_validate_write_file_transport(updated, body.write_file_transport)
updated["write_file_transport"] = body.write_file_transport
try:
ModelConfig.model_validate(updated)
except Exception as exc:
raise HTTPException(status_code=422, detail=f"Invalid model configuration: {exc}") from exc
next_models = [*models]
next_models[index] = updated
await _save_models(request, raw_before, next_models)
capabilities = await _capabilities_by_name(request)
capability = capabilities.get(model_name, {})
return _to_admin_item(
updated,
supports_reasoning_effort=bool(capability.get("supports_reasoning_effort", False)),
default_reasoning_effort=str(capability.get("default_reasoning_effort") or "xhigh"),
)
@router.put("/{model_name:path}", response_model=ModelAdminItem) @router.put("/{model_name:path}", response_model=ModelAdminItem)
async def update_model_configuration(model_name: str, body: ModelUpsertRequest, request: Request) -> ModelAdminItem: async def update_model_configuration(model_name: str, body: ModelUpsertRequest, request: Request) -> ModelAdminItem:
admin = await _require_admin(request) admin = await _require_admin(request)
@@ -742,7 +742,6 @@ def _run_payload(
cancel_on_disconnect: bool = False, cancel_on_disconnect: bool = False,
extra_context: dict[str, Any] | None = None, extra_context: dict[str, Any] | None = None,
files: list[dict[str, Any]] | None = None, files: list[dict[str, Any]] | None = None,
roundtable_file_stream: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""构造 ``POST /api/threads/{thread_id}/runs/stream`` 的请求体。 """构造 ``POST /api/threads/{thread_id}/runs/stream`` 的请求体。
@@ -820,20 +819,9 @@ def _run_payload(
# 供 read_peer_delivery 工具按需读取)。只进 context dict,不进 prompt → 不调工具就不耗 token。 # 供 read_peer_delivery 工具按需读取)。只进 context dict,不进 prompt → 不调工具就不耗 token。
if extra_context: if extra_context:
payload["context"].update(extra_context) payload["context"].update(extra_context)
if roundtable_file_stream:
payload["context"].update(_roundtable_file_stream_context())
return payload return payload
def _roundtable_file_stream_context() -> dict[str, bool]:
"""会商专用文件流式兼容。关闭时不写 context,本轮仍只看全局开关。"""
from deerflow.config.system_settings import get_roundtable_room_limit_settings
if get_roundtable_room_limit_settings().write_file_compatibility_enabled:
return {"roundtable_write_file_compatibility": True}
return {}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# POST /api/multi-agent/init — Step 2 席位 + 总控初始化(SSE) # POST /api/multi-agent/init — Step 2 席位 + 总控初始化(SSE)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1709,7 +1697,6 @@ async def _leader_run(
cancel_on_disconnect=True, cancel_on_disconnect=True,
# 用户随干预上传的参考文件 → 总控可用 read_file 等工具读取后再派活。 # 用户随干预上传的参考文件 → 总控可用 read_file 等工具读取后再派活。
files=files, files=files,
roundtable_file_stream=True,
) )
body = _mk_body(current_model) body = _mk_body(current_model)
@@ -1993,7 +1980,6 @@ async def _special_run(
), ),
# 用户随干预上传的参考文件 → 席位可用 read_file 等工具读取。 # 用户随干预上传的参考文件 → 席位可用 read_file 等工具读取。
files=files, files=files,
roundtable_file_stream=True,
) )
t_payload_built = time.perf_counter() t_payload_built = time.perf_counter()
@@ -456,15 +456,10 @@ class ConversationContextResponse(BaseModel):
default=3, default=3,
description="Fixed number of real user turns retained by the sliding window", description="Fixed number of real user turns retained by the sliding window",
) )
use_question_as_title: bool = Field(
default=False,
description="Whether the first user question is the final title, without post-answer title generation",
)
class ConversationContextUpdateRequest(BaseModel): class ConversationContextUpdateRequest(BaseModel):
recent_turn_window_enabled: bool | None = None recent_turn_window_enabled: bool
use_question_as_title: bool | None = None
class RoundtableRoomLimitResponse(BaseModel): class RoundtableRoomLimitResponse(BaseModel):
@@ -472,7 +467,6 @@ class RoundtableRoomLimitResponse(BaseModel):
max_rooms: int = 3 max_rooms: int = 3
max_hold_minutes: int = 30 max_hold_minutes: int = 30
max_cycles: int = 8 max_cycles: int = 8
write_file_compatibility_enabled: bool = False
class RoundtableRoomLimitUpdateRequest(BaseModel): class RoundtableRoomLimitUpdateRequest(BaseModel):
@@ -480,7 +474,6 @@ class RoundtableRoomLimitUpdateRequest(BaseModel):
max_rooms: int | None = Field(default=None, ge=1, le=20) max_rooms: int | None = Field(default=None, ge=1, le=20)
max_hold_minutes: int | None = Field(default=None, ge=1, le=1440) max_hold_minutes: int | None = Field(default=None, ge=1, le=1440)
max_cycles: int | None = Field(default=None, ge=2, le=100) max_cycles: int | None = Field(default=None, ge=2, le=100)
write_file_compatibility_enabled: bool | None = None
class OrdinaryKnowledgeRetrievalResponse(BaseModel): class OrdinaryKnowledgeRetrievalResponse(BaseModel):
@@ -563,7 +556,6 @@ def _conversation_context_response(settings: ConversationContextSettings) -> Con
return ConversationContextResponse( return ConversationContextResponse(
recent_turn_window_enabled=settings.recent_turn_window_enabled, recent_turn_window_enabled=settings.recent_turn_window_enabled,
recent_turns=3, recent_turns=3,
use_question_as_title=settings.use_question_as_title,
) )
@@ -581,15 +573,13 @@ async def update_conversation_context_setting(
"""Enable or disable the fixed recent-three-turn window. Admin only.""" """Enable or disable the fixed recent-three-turn window. Admin only."""
await _require_admin(request) await _require_admin(request)
current = load_system_settings() current = load_system_settings()
updated_fields = body.model_dump(exclude_unset=True, exclude_none=True) updated = current.conversation_context.model_copy(
if not updated_fields: update={"recent_turn_window_enabled": body.recent_turn_window_enabled}
raise HTTPException(status_code=422, detail="至少需要提供一个上下文设置项") )
updated = current.conversation_context.model_copy(update=updated_fields)
saved = save_system_settings(current.model_copy(update={"conversation_context": updated})) saved = save_system_settings(current.model_copy(update={"conversation_context": updated}))
logger.info( logger.info(
"Conversation runtime settings changed: recent_turn_window=%s use_question_as_title=%s", "Conversation recent-turn window changed: enabled=%s retained_turns=3",
saved.conversation_context.recent_turn_window_enabled, saved.conversation_context.recent_turn_window_enabled,
saved.conversation_context.use_question_as_title,
) )
return _conversation_context_response(saved.conversation_context) return _conversation_context_response(saved.conversation_context)
@@ -600,7 +590,6 @@ def _roundtable_room_limit_response(settings: RoundtableRoomLimitSettings) -> Ro
max_rooms=settings.max_rooms, max_rooms=settings.max_rooms,
max_hold_minutes=settings.max_hold_minutes, max_hold_minutes=settings.max_hold_minutes,
max_cycles=settings.max_cycles, max_cycles=settings.max_cycles,
write_file_compatibility_enabled=settings.write_file_compatibility_enabled,
) )
@@ -627,17 +616,14 @@ async def update_roundtable_room_limit_setting(
updates["max_hold_minutes"] = body.max_hold_minutes updates["max_hold_minutes"] = body.max_hold_minutes
if body.max_cycles is not None: if body.max_cycles is not None:
updates["max_cycles"] = body.max_cycles updates["max_cycles"] = body.max_cycles
if body.write_file_compatibility_enabled is not None:
updates["write_file_compatibility_enabled"] = body.write_file_compatibility_enabled
updated = current.roundtable_room_limit.model_copy(update=updates) updated = current.roundtable_room_limit.model_copy(update=updates)
saved = save_system_settings(current.model_copy(update={"roundtable_room_limit": updated})) saved = save_system_settings(current.model_copy(update={"roundtable_room_limit": updated}))
logger.info( logger.info(
"Roundtable runtime limits changed: enabled=%s max_rooms=%s max_hold_minutes=%s max_cycles=%s write_file_compatibility=%s", "Roundtable runtime limits changed: enabled=%s max_rooms=%s max_hold_minutes=%s max_cycles=%s",
saved.roundtable_room_limit.enabled, saved.roundtable_room_limit.enabled,
saved.roundtable_room_limit.max_rooms, saved.roundtable_room_limit.max_rooms,
saved.roundtable_room_limit.max_hold_minutes, saved.roundtable_room_limit.max_hold_minutes,
saved.roundtable_room_limit.max_cycles, saved.roundtable_room_limit.max_cycles,
saved.roundtable_room_limit.write_file_compatibility_enabled,
) )
return _roundtable_room_limit_response(saved.roundtable_room_limit) return _roundtable_room_limit_response(saved.roundtable_room_limit)
@@ -577,8 +577,6 @@ _CONTEXT_CONFIGURABLE_KEYS: frozenset[str] = frozenset(
"roundtable_peer_deliveries", "roundtable_peer_deliveries",
# 岗位会商席位 / 方案总结:每轮 outputs 下 write_file 上限(行动规划不传此键)。 # 岗位会商席位 / 方案总结:每轮 outputs 下 write_file 上限(行动规划不传此键)。
"position_artifact_cap", "position_artifact_cap",
# 会商专用文件流式兼容。只由会商 run 写入;普通对话不带此键,仍只看全局开关。
"roundtable_write_file_compatibility",
# 上下文压缩开关:由聊天框下方的按钮控制(默认关闭),透传到 run config 后 # 上下文压缩开关:由聊天框下方的按钮控制(默认关闭),透传到 run config 后
# 在 _build_middlewares 里决定是否挂载 SummarizationMiddleware。 # 在 _build_middlewares 里决定是否挂载 SummarizationMiddleware。
"summarization_enabled", "summarization_enabled",
+3 -5
View File
@@ -87,7 +87,6 @@ models:
temperature: 1.0 temperature: 1.0
supports_vision: true supports_vision: true
supports_thinking: true supports_thinking: true
write_file_transport: buffered_gateway
- name: MiniMax-M2.7-fash - name: MiniMax-M2.7-fash
display_name: MiniMax-M2.7-fash display_name: MiniMax-M2.7-fash
use: langchain_openai:ChatOpenAI use: langchain_openai:ChatOpenAI
@@ -108,6 +107,9 @@ models:
base_url: https://ai.hzfzlz.com/v1 base_url: https://ai.hzfzlz.com/v1
request_timeout: 600.0 request_timeout: 600.0
max_retries: 2 max_retries: 2
# 不带 max_tokens 时该中转站按自身小默认值截断输出(实测 ~64 token),
# 检索词 JSON 会被截断导致解析失败走兜底。
# write_file 写整页 HTML 时常超过 4096,截断后只剩 path,沙箱无法流式预览。
max_tokens: 32768 max_tokens: 32768
stream_usage: true stream_usage: true
supports_thinking: false supports_thinking: false
@@ -123,7 +125,6 @@ models:
stream_usage: true stream_usage: true
supports_thinking: false supports_thinking: false
supports_vision: false supports_vision: false
write_file_transport: native
- name: zai-org/GLM-5-FP8 - name: zai-org/GLM-5-FP8
display_name: zai-org/GLM-5-FP8 display_name: zai-org/GLM-5-FP8
use: langchain_openai:ChatOpenAI use: langchain_openai:ChatOpenAI
@@ -194,9 +195,6 @@ models:
temperature: 1.0 temperature: 1.0
supports_vision: false supports_vision: false
supports_thinking: true supports_thinking: true
# 不带 max_tokens 时该中转站按自身小默认值截断输出(实测 ~64 token),
# 检索词 JSON 会被截断导致解析失败走兜底。
# write_file 写整页 HTML 时常超过 4096,截断后只剩 path,沙箱无法流式预览。
# - name: deepseek-v4-flash # - name: deepseek-v4-flash
# display_name: DeepSeek V4 Flash # display_name: DeepSeek V4 Flash
# use: langchain_openai:ChatOpenAI # use: langchain_openai:ChatOpenAI
@@ -826,8 +826,6 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
# create_chat_model(force_disable_thinking=...) so enable_thinking=false is # create_chat_model(force_disable_thinking=...) so enable_thinking=false is
# actually injected. Default off → no behavior change for normal chats. # actually injected. Default off → no behavior change for normal chats.
thinking_force_disabled = bool(cfg.get("thinking_force_disabled", False)) thinking_force_disabled = bool(cfg.get("thinking_force_disabled", False))
# Roundtable runs opt in explicitly. Missing/false keeps the global switch.
write_file_compatibility = True if cfg.get("roundtable_write_file_compatibility") else None
reasoning_effort = cfg.get("reasoning_effort", None) reasoning_effort = cfg.get("reasoning_effort", None)
requested_model_name: str | None = cfg.get("model_name") or cfg.get("model") requested_model_name: str | None = cfg.get("model_name") or cfg.get("model")
is_plan_mode = cfg.get("is_plan_mode", False) is_plan_mode = cfg.get("is_plan_mode", False)
@@ -1027,7 +1025,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
setup_tools = [setup_writing] if writing_setup_quick else [setup_writing, ask_clarification_tool] setup_tools = [setup_writing] if writing_setup_quick else [setup_writing, ask_clarification_tool]
return create_agent( return create_agent(
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, write_file_compatibility=write_file_compatibility, app_config=resolved_app_config), model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
tools=setup_tools, tools=setup_tools,
middleware=_build_setup_middlewares( middleware=_build_setup_middlewares(
config, config,
@@ -1064,7 +1062,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
) )
) )
return create_agent( return create_agent(
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, write_file_compatibility=write_file_compatibility, app_config=resolved_app_config), model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
tools=setup_tools, tools=setup_tools,
middleware=_build_setup_middlewares( middleware=_build_setup_middlewares(
config, config,
@@ -1089,7 +1087,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
if is_bootstrap: if is_bootstrap:
# Special bootstrap agent with minimal prompt for initial custom agent creation flow # Special bootstrap agent with minimal prompt for initial custom agent creation flow
return create_agent( return create_agent(
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, write_file_compatibility=write_file_compatibility, app_config=resolved_app_config), model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
tools=get_available_tools( tools=get_available_tools(
include_mcp=not classic_knowledge_space_strict, include_mcp=not classic_knowledge_space_strict,
model_name=model_name, model_name=model_name,
@@ -1099,7 +1097,6 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
allowed_tools=requested_allowed_tools, allowed_tools=requested_allowed_tools,
allow_agent_orchestration=agent_id in _COORDINATOR_AGENT_IDS, allow_agent_orchestration=agent_id in _COORDINATOR_AGENT_IDS,
app_config=resolved_app_config, app_config=resolved_app_config,
write_file_compatibility=write_file_compatibility,
) )
+ [setup_agent], + [setup_agent],
middleware=_build_middlewares(config, model_name=model_name, app_config=resolved_app_config), middleware=_build_middlewares(config, model_name=model_name, app_config=resolved_app_config),
@@ -1133,7 +1130,6 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
allowed_tools=requested_allowed_tools, allowed_tools=requested_allowed_tools,
allow_agent_orchestration=agent_id in _COORDINATOR_AGENT_IDS, allow_agent_orchestration=agent_id in _COORDINATOR_AGENT_IDS,
app_config=resolved_app_config, app_config=resolved_app_config,
write_file_compatibility=write_file_compatibility,
) )
) )
if agent_id == FORCED_RESEARCH_AGENT_ID: if agent_id == FORCED_RESEARCH_AGENT_ID:
@@ -1247,7 +1243,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
) )
return create_agent( return create_agent(
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, force_disable_thinking=thinking_force_disabled, write_file_compatibility=write_file_compatibility, app_config=resolved_app_config), model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
tools=default_tools, tools=default_tools,
middleware=_build_middlewares(config, model_name=model_name, agent_name=agent_id, app_config=resolved_app_config), middleware=_build_middlewares(config, model_name=model_name, agent_name=agent_id, app_config=resolved_app_config),
system_prompt=system_prompt, system_prompt=system_prompt,
@@ -12,7 +12,6 @@ from langchain.agents import AgentState
from langchain.agents.middleware import AgentMiddleware from langchain.agents.middleware import AgentMiddleware
from langgraph.runtime import Runtime from langgraph.runtime import Runtime
from deerflow.config.system_settings import get_conversation_context_settings
from deerflow.config.title_config import get_title_config from deerflow.config.title_config import get_title_config
from deerflow.models import create_chat_model from deerflow.models import create_chat_model
from deerflow.utils.time import now_iso from deerflow.utils.time import now_iso
@@ -210,15 +209,14 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]):
return self._fallback_title(user_msg) return self._fallback_title(user_msg)
def _should_set_provisional_title(self, state: TitleMiddlewareState) -> bool: def _should_set_provisional_title(self, state: TitleMiddlewareState) -> bool:
"""Check if we should set an instant, question-derived title. """Check if we should set an instant, question-derived placeholder title.
Runs in ``before_model`` so the thread shows a sensible title the moment Runs in ``before_model`` so the thread shows a sensible title the moment
the user sends their first message — eliminating the "untitled" window. the user sends their first message — eliminating the "untitled" window
When the administrator selects question-derived titles, this also works while the (possibly long) first answer streams. No LLM call, no latency.
when LLM title generation is disabled because no title model is needed.
""" """
config = self._get_title_config() config = self._get_title_config()
if not config.enabled and not get_conversation_context_settings().use_question_as_title: if not config.enabled:
return False return False
# Already has a title (provisional or final) — nothing to do. # Already has a title (provisional or final) — nothing to do.
@@ -335,13 +333,8 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]):
if not user_msg.strip(): if not user_msg.strip():
return None return None
title = self._validated_fallback_title(state, user_msg) title = self._validated_fallback_title(state, user_msg)
use_question_as_title = get_conversation_context_settings().use_question_as_title logger.debug("TitleMiddleware: set provisional title %r before first model call", title)
logger.debug( return {"title": title, "title_provisional": True}
"TitleMiddleware: set %s title %r before first model call",
"final question-derived" if use_question_as_title else "provisional question-derived",
title,
)
return {"title": title, "title_provisional": not use_question_as_title}
def _generate_title_result(self, state: TitleMiddlewareState) -> dict | None: def _generate_title_result(self, state: TitleMiddlewareState) -> dict | None:
"""Generate a local fallback title without blocking on an LLM call.""" """Generate a local fallback title without blocking on an LLM call."""
@@ -576,13 +576,6 @@ class ConversationContextSettings(BaseModel):
default=False, default=False,
description="Whether model requests use the fixed recent-three-turn sliding window.", description="Whether model requests use the fixed recent-three-turn sliding window.",
) )
use_question_as_title: bool = Field(
default=False,
description=(
"Whether the first user question is used as the final conversation title, "
"skipping post-answer LLM title generation."
),
)
model_config = ConfigDict(extra="ignore") model_config = ConfigDict(extra="ignore")
@@ -636,12 +629,22 @@ class RoundtableRoomLimitSettings(BaseModel):
"The value is captured when an orchestration starts." "The value is captured when an orchestration starts."
), ),
) )
write_file_compatibility_enabled: bool = Field( model_config = ConfigDict(extra="ignore")
class ConversationContextSettings(BaseModel):
"""Deployment-wide short-context policy for chat model requests.
The recent-turn window is deliberately opt-in. When enabled, the lead
agent keeps the persisted conversation intact but sends only the latest
three real user turns (plus their assistant/tool continuations) to the
model. This avoids an extra summarization-model call while preserving the
complete transcript for the UI and checkpoint recovery.
"""
recent_turn_window_enabled: bool = Field(
default=False, default=False,
description=( description="Whether model requests use the fixed recent-three-turn sliding window.",
"When true, roundtable leader/seat/step-3 runs use buffered-gateway "
"write_file streaming. Ordinary chat keeps the global switch."
),
) )
model_config = ConfigDict(extra="ignore") model_config = ConfigDict(extra="ignore")
@@ -711,6 +714,7 @@ class SystemSettings(BaseModel):
write_file_compatibility: WriteFileCompatibilitySettings = Field( write_file_compatibility: WriteFileCompatibilitySettings = Field(
default_factory=WriteFileCompatibilitySettings default_factory=WriteFileCompatibilitySettings
) )
conversation_context: ConversationContextSettings = Field(default_factory=ConversationContextSettings)
roundtable_room_limit: RoundtableRoomLimitSettings = Field(default_factory=RoundtableRoomLimitSettings) roundtable_room_limit: RoundtableRoomLimitSettings = Field(default_factory=RoundtableRoomLimitSettings)
local_wiki_encoder: LocalWikiEncoderSettings = Field(default_factory=LocalWikiEncoderSettings) local_wiki_encoder: LocalWikiEncoderSettings = Field(default_factory=LocalWikiEncoderSettings)
strategy_projects: StrategyProjectSettings = Field(default_factory=StrategyProjectSettings) strategy_projects: StrategyProjectSettings = Field(default_factory=StrategyProjectSettings)
@@ -847,6 +851,15 @@ def get_roundtable_room_limit_settings() -> RoundtableRoomLimitSettings:
return RoundtableRoomLimitSettings() return RoundtableRoomLimitSettings()
def get_conversation_context_settings() -> ConversationContextSettings:
"""Return the admin-controlled recent-turn policy, failing closed."""
try:
return load_system_settings().conversation_context
except Exception:
logger.debug("Failed to read conversation context setting; keeping the recent-turn window disabled", exc_info=True)
return ConversationContextSettings()
def save_system_settings(settings: SystemSettings) -> SystemSettings: def save_system_settings(settings: SystemSettings) -> SystemSettings:
"""Persist the settings document atomically, returning the saved value.""" """Persist the settings document atomically, returning the saved value."""
global _settings_cache_key, _settings_cache_value global _settings_cache_key, _settings_cache_value
@@ -136,7 +136,7 @@ def _enable_stream_usage_by_default(model_use_path: str, model_settings_from_con
model_settings_from_config["stream_usage"] = True model_settings_from_config["stream_usage"] = True
def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *, app_config: AppConfig | None = None, force_disable_thinking: bool = False, write_file_compatibility: bool | None = None, **kwargs) -> BaseChatModel: def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *, app_config: AppConfig | None = None, force_disable_thinking: bool = False, **kwargs) -> BaseChatModel:
"""Create a chat model instance from the config. """Create a chat model instance from the config.
Args: Args:
@@ -187,17 +187,10 @@ def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *
}, },
) )
configured_write_file_transport = model_config.write_file_transport configured_write_file_transport = model_config.write_file_transport
# ``None`` follows the global switch, so a roundtable run with its own runtime_write_file_compatibility = get_write_file_compatibility_settings()
# switch off still uses compatibility when the global switch is on.
# ``True`` is the roundtable opt-in used when the global switch is off.
compatibility_enabled = (
get_write_file_compatibility_settings().enabled
if write_file_compatibility is None
else write_file_compatibility
)
effective_write_file_transport = ( effective_write_file_transport = (
WriteFileTransport.BUFFERED_GATEWAY WriteFileTransport.BUFFERED_GATEWAY
if compatibility_enabled if runtime_write_file_compatibility.enabled
else configured_write_file_transport else configured_write_file_transport
) )
if model_config.tool_call_transport.value != "native": if model_config.tool_call_transport.value != "native":
@@ -289,8 +289,6 @@ class SubagentExecutor:
thread_data: ThreadDataState | None = None, thread_data: ThreadDataState | None = None,
thread_id: str | None = None, thread_id: str | None = None,
trace_id: str | None = None, trace_id: str | None = None,
*,
write_file_compatibility: bool | None = None,
): ):
"""Initialize the executor. """Initialize the executor.
@@ -319,7 +317,6 @@ class SubagentExecutor:
self.sandbox_state = sandbox_state self.sandbox_state = sandbox_state
self.thread_data = thread_data self.thread_data = thread_data
self.thread_id = thread_id self.thread_id = thread_id
self.write_file_compatibility = True if write_file_compatibility else None
# Generate trace_id if not provided (for top-level calls) # Generate trace_id if not provided (for top-level calls)
self.trace_id = trace_id or str(uuid.uuid4())[:8] self.trace_id = trace_id or str(uuid.uuid4())[:8]
@@ -337,12 +334,7 @@ class SubagentExecutor:
app_config = self.app_config or get_app_config() app_config = self.app_config or get_app_config()
if self.model_name is None: if self.model_name is None:
self.model_name = resolve_subagent_model_name(self.config, self.parent_model, app_config=app_config) self.model_name = resolve_subagent_model_name(self.config, self.parent_model, app_config=app_config)
model = create_chat_model( model = create_chat_model(name=self.model_name, thinking_enabled=False, app_config=app_config)
name=self.model_name,
thinking_enabled=False,
app_config=app_config,
write_file_compatibility=self.write_file_compatibility,
)
from deerflow.agents.middlewares.tool_error_handling_middleware import build_subagent_runtime_middlewares from deerflow.agents.middlewares.tool_error_handling_middleware import build_subagent_runtime_middlewares
@@ -157,8 +157,6 @@ async def task_tool(
effective_model = resolve_subagent_model_name(config, parent_model, app_config=resolved_app_config) effective_model = resolve_subagent_model_name(config, parent_model, app_config=resolved_app_config)
# Subagents should not have subagent tools enabled (prevent recursive nesting) # Subagents should not have subagent tools enabled (prevent recursive nesting)
parent_context = runtime.context if runtime is not None and isinstance(getattr(runtime, "context", None), dict) else {}
roundtable_file_stream = bool(parent_context.get("roundtable_write_file_compatibility"))
available_tools_kwargs = { available_tools_kwargs = {
"model_name": effective_model, "model_name": effective_model,
"groups": parent_tool_groups, "groups": parent_tool_groups,
@@ -166,8 +164,6 @@ async def task_tool(
} }
if resolved_app_config is not None: if resolved_app_config is not None:
available_tools_kwargs["app_config"] = resolved_app_config available_tools_kwargs["app_config"] = resolved_app_config
if roundtable_file_stream:
available_tools_kwargs["write_file_compatibility"] = True
tools = get_available_tools(**available_tools_kwargs) tools = get_available_tools(**available_tools_kwargs)
# Create executor # Create executor
@@ -182,8 +178,6 @@ async def task_tool(
} }
if resolved_app_config is not None: if resolved_app_config is not None:
executor_kwargs["app_config"] = resolved_app_config executor_kwargs["app_config"] = resolved_app_config
if roundtable_file_stream:
executor_kwargs["write_file_compatibility"] = True
executor = SubagentExecutor(**executor_kwargs) executor = SubagentExecutor(**executor_kwargs)
# Start background execution (always async to prevent blocking) # Start background execution (always async to prevent blocking)
@@ -71,7 +71,6 @@ def get_available_tools(
allowed_tools: list[str] | None = None, allowed_tools: list[str] | None = None,
allow_agent_orchestration: bool = False, allow_agent_orchestration: bool = False,
app_config: AppConfig | None = None, app_config: AppConfig | None = None,
write_file_compatibility: bool | None = None,
) -> list[BaseTool]: ) -> list[BaseTool]:
"""Get all available tools from config. """Get all available tools from config.
@@ -106,8 +105,6 @@ def get_available_tools(
# requests entirely inside PatchedChatOpenAI. # requests entirely inside PatchedChatOpenAI.
global_write_file_compatibility_enabled = ( global_write_file_compatibility_enabled = (
get_write_file_compatibility_settings().enabled get_write_file_compatibility_settings().enabled
if write_file_compatibility is None
else write_file_compatibility
) )
write_renderer_enabled = ( write_renderer_enabled = (
not global_write_file_compatibility_enabled not global_write_file_compatibility_enabled
@@ -4543,37 +4543,6 @@ def test_factory_global_switch_overrides_every_compatible_model(
assert second.write_file_transport == "buffered_gateway" assert second.write_file_transport == "buffered_gateway"
def test_roundtable_override_enables_buffered_gateway_while_global_is_off(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
model_factory,
"get_write_file_compatibility_settings",
lambda: SimpleNamespace(enabled=False),
)
config = AppConfig.model_construct(
models=[
ModelConfig(
name="native-model",
use="langchain_openai:ChatOpenAI",
model="native-model",
api_key="test-key",
base_url=None,
)
]
)
ordinary = model_factory.create_chat_model("native-model", app_config=config)
roundtable = model_factory.create_chat_model(
"native-model",
app_config=config,
write_file_compatibility=True,
)
assert ordinary.write_file_transport == "native"
assert roundtable.write_file_transport == "buffered_gateway"
def test_factory_rejects_bridge_for_non_patched_provider(monkeypatch: pytest.MonkeyPatch) -> None: def test_factory_rejects_bridge_for_non_patched_provider(monkeypatch: pytest.MonkeyPatch) -> None:
config = AppConfig.model_construct( config = AppConfig.model_construct(
models=[ models=[
@@ -215,121 +215,6 @@ def test_reasoning_capability_route_precedes_generic_model_update_and_accepts_sl
] ]
def test_write_file_transport_route_precedes_generic_model_update() -> None:
scope = {
"type": "http",
"method": "PUT",
"path": "/api/admin/models/write-file-transport/zai-org/GLM-5-FP8",
}
matching_paths = [route.path for route in router.router.routes if route.matches(scope)[0] is Match.FULL]
assert matching_paths[:2] == [
"/api/admin/models/write-file-transport/{model_name:path}",
"/api/admin/models/{model_name:path}",
]
def _admin_request():
return SimpleNamespace(
state=SimpleNamespace(user=SimpleNamespace(system_role="admin")),
app=SimpleNamespace(state=SimpleNamespace()),
)
@pytest.mark.asyncio
async def test_write_file_transport_toggle_persists_native_and_buffered_gateway(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.yaml"
config_path.write_text(
"""models:
- name: flash
use: langchain_openai:ChatOpenAI
model: flash
""",
encoding="utf-8",
)
saved: dict[str, object] = {}
async def save_models(_request, raw_before: str, models: list[dict[str, object]]) -> list[dict[str, object]]:
saved["raw_before"] = raw_before
saved["models"] = models
return models
monkeypatch.setattr(router, "_resolve_config_path", lambda: config_path)
monkeypatch.setattr(router, "_save_models", save_models)
enabled = await router.update_write_file_transport(
"flash",
router.WriteFileTransportToggleRequest(write_file_transport="buffered_gateway"),
_admin_request(),
)
assert enabled.write_file_transport == "buffered_gateway"
assert saved["models"] == [
{
"name": "flash",
"use": "langchain_openai:ChatOpenAI",
"model": "flash",
"write_file_transport": "buffered_gateway",
}
]
disabled = await router.update_write_file_transport(
"flash",
router.WriteFileTransportToggleRequest(write_file_transport="native"),
_admin_request(),
)
assert disabled.write_file_transport == "native"
assert saved["models"][0]["write_file_transport"] == "native"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("use", "use_responses_api"),
[
("langchain_anthropic:ChatAnthropic", None),
("langchain_openai:ChatOpenAI", True),
],
)
async def test_write_file_transport_rejects_incompatible_models(
use: str,
use_responses_api: bool | None,
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.yaml"
original = f"""models:
- name: incompatible
use: {use}
model: incompatible
"""
if use_responses_api:
original = original.replace(
" model: incompatible\n",
" model: incompatible\n use_responses_api: true\n",
)
config_path.write_text(original, encoding="utf-8")
monkeypatch.setattr(router, "_resolve_config_path", lambda: config_path)
async def fail_save_models(_request, _raw_before, _models):
raise AssertionError("incompatible models must not be persisted")
monkeypatch.setattr(router, "_save_models", fail_save_models)
with pytest.raises(HTTPException) as exc_info:
await router.update_write_file_transport(
"incompatible",
router.WriteFileTransportToggleRequest(write_file_transport="buffered_gateway"),
_admin_request(),
)
assert exc_info.value.status_code == 422
assert config_path.read_text(encoding="utf-8") == original
def _reasoning_request(store, *, config: AppConfig | None = None): def _reasoning_request(store, *, config: AppConfig | None = None):
return SimpleNamespace( return SimpleNamespace(
state=SimpleNamespace( state=SimpleNamespace(
@@ -745,7 +745,6 @@ async def test_admin_can_change_roundtable_room_limit_without_restart(
assert ss.get_roundtable_room_limit_settings().max_rooms == 3 assert ss.get_roundtable_room_limit_settings().max_rooms == 3
assert ss.get_roundtable_room_limit_settings().max_hold_minutes == 30 assert ss.get_roundtable_room_limit_settings().max_hold_minutes == 30
assert ss.get_roundtable_room_limit_settings().max_cycles == 8 assert ss.get_roundtable_room_limit_settings().max_cycles == 8
assert ss.get_roundtable_room_limit_settings().write_file_compatibility_enabled is False
saved = await ss_router.update_roundtable_room_limit_setting( saved = await ss_router.update_roundtable_room_limit_setting(
ss_router.RoundtableRoomLimitUpdateRequest( ss_router.RoundtableRoomLimitUpdateRequest(
@@ -763,36 +762,6 @@ async def test_admin_can_change_roundtable_room_limit_without_restart(
assert ss.load_system_settings().roundtable_room_limit.max_rooms == 5 assert ss.load_system_settings().roundtable_room_limit.max_rooms == 5
assert ss.load_system_settings().roundtable_room_limit.max_hold_minutes == 45 assert ss.load_system_settings().roundtable_room_limit.max_hold_minutes == 45
assert ss.load_system_settings().roundtable_room_limit.max_cycles == 12 assert ss.load_system_settings().roundtable_room_limit.max_cycles == 12
assert saved.write_file_compatibility_enabled is False
@pytest.mark.asyncio
async def test_roundtable_file_stream_is_only_attached_to_roundtable_runs(
tmp_runtime_home: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_admin(monkeypatch)
base = dict(
agent_name="roundtable-seat",
model_name="test-model",
thread_id="thread-1",
new_message="test",
excluded_tools=[],
skill_stop_names=[],
)
before = _run_payload(**base, roundtable_file_stream=True)
assert "roundtable_write_file_compatibility" not in before["context"]
saved = await ss_router.update_roundtable_room_limit_setting(
ss_router.RoundtableRoomLimitUpdateRequest(write_file_compatibility_enabled=True),
SimpleNamespace(),
)
assert saved.write_file_compatibility_enabled is True
opted_in = _run_payload(**base, roundtable_file_stream=True)
assert opted_in["context"]["roundtable_write_file_compatibility"] is True
ordinary = _run_payload(**base)
assert "roundtable_write_file_compatibility" not in ordinary["context"]
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -114,46 +114,6 @@ def _title_config() -> TitleConfig:
) )
def test_admin_question_title_switch_finalizes_question_before_first_model_call(monkeypatch) -> None:
monkeypatch.setattr(
title_middleware_module,
"get_conversation_context_settings",
lambda: SimpleNamespace(use_question_as_title=True),
)
middleware = TitleMiddleware(
title_config=_title_config().model_copy(update={"enabled": False})
)
state = {
"messages": [SimpleNamespace(type="human", content="请帮我分析这份合同")],
}
result = middleware.before_model(state, runtime=SimpleNamespace())
assert result == {
"title": "请帮我分析这份合同",
"title_provisional": False,
}
def test_admin_question_title_switch_off_keeps_async_title_generation(monkeypatch) -> None:
monkeypatch.setattr(
title_middleware_module,
"get_conversation_context_settings",
lambda: SimpleNamespace(use_question_as_title=False),
)
middleware = TitleMiddleware(title_config=_title_config())
state = {
"messages": [SimpleNamespace(type="human", content="请帮我分析这份合同")],
}
result = middleware.before_model(state, runtime=SimpleNamespace())
assert result == {
"title": "请帮我分析这份合同",
"title_provisional": True,
}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_finalize_thread_title_updates_checkpoint_and_display_name(monkeypatch) -> None: async def test_finalize_thread_title_updates_checkpoint_and_display_name(monkeypatch) -> None:
async def _fake_generate(self, state): async def _fake_generate(self, state):
+1
View File
@@ -493,3 +493,4 @@ task_deeplink:
taskDirection: "正向引导 + 风险预警" taskDirection: "正向引导 + 风险预警"
taskName: "任务01:暑期文旅舆情综合研判" taskName: "任务01:暑期文旅舆情综合研判"
taskContent: "围绕暑期文旅消费高峰,对重点城市、重点景区的公众情绪与舆情态势做综合研判,识别情绪拐点与潜在风险,提出正向引导与处置建议。" taskContent: "围绕暑期文旅消费高峰,对重点城市、重点景区的公众情绪与舆情态势做综合研判,识别情绪拐点与潜在风险,提出正向引导与处置建议。"
taskType: 1