mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-11 09:55:59 +00:00
Merge remote-tracking branch 'origin/main' into codex/im-channel-connections
# Conflicts: # backend/app/channels/discord.py # backend/app/channels/manager.py # backend/app/channels/slack.py # backend/app/channels/telegram.py
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import type { User } from "./types";
|
||||
|
||||
export const AUTH_DISABLED_USER: User = {
|
||||
id: "e2e-user",
|
||||
email: "e2e@test.local",
|
||||
system_role: "admin",
|
||||
needs_setup: false,
|
||||
};
|
||||
|
||||
const PRODUCTION_ENV_VALUES = new Set(["prod", "production"]);
|
||||
|
||||
function isExplicitProductionEnvironment() {
|
||||
return ["DEER_FLOW_ENV", "ENVIRONMENT"].some((name) =>
|
||||
PRODUCTION_ENV_VALUES.has((process.env[name] ?? "").trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
export function isAuthDisabledMode() {
|
||||
return (
|
||||
process.env.DEER_FLOW_AUTH_DISABLED === "1" &&
|
||||
!isExplicitProductionEnvironment()
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { cookies } from "next/headers";
|
||||
|
||||
import { isStaticWebsiteOnly } from "../static-mode";
|
||||
|
||||
import { AUTH_DISABLED_USER, isAuthDisabledMode } from "./auth-disabled-user";
|
||||
import { getGatewayConfig } from "./gateway-config";
|
||||
import { STATIC_WEBSITE_USER } from "./static-user";
|
||||
import { type AuthResult, userSchema } from "./types";
|
||||
@@ -20,15 +21,10 @@ export async function getServerSideUser(): Promise<AuthResult> {
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.DEER_FLOW_AUTH_DISABLED === "1") {
|
||||
if (isAuthDisabledMode()) {
|
||||
return {
|
||||
tag: "authenticated",
|
||||
user: {
|
||||
id: "e2e-user",
|
||||
email: "e2e@test.local",
|
||||
system_role: "admin",
|
||||
needs_setup: false,
|
||||
},
|
||||
user: AUTH_DISABLED_USER,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+246
-19
@@ -1,3 +1,47 @@
|
||||
type ClipboardItemLike = {
|
||||
types?: readonly string[];
|
||||
getType?: (type: string) => Promise<Blob>;
|
||||
items?: Record<string, Blob | string>;
|
||||
};
|
||||
|
||||
function copyTextWithExecCommand(text: string): boolean {
|
||||
const document = globalThis.document;
|
||||
if (
|
||||
typeof document?.createElement !== "function" ||
|
||||
typeof document.body?.appendChild !== "function" ||
|
||||
typeof document.execCommand !== "function"
|
||||
) {
|
||||
throw new Error("Clipboard DOM fallback not available");
|
||||
}
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.top = "-9999px";
|
||||
textarea.style.left = "-9999px";
|
||||
|
||||
let copied = false;
|
||||
let appended = false;
|
||||
try {
|
||||
document.body.appendChild(textarea);
|
||||
appended = true;
|
||||
textarea.select();
|
||||
copied = document.execCommand("copy");
|
||||
} finally {
|
||||
if (appended) {
|
||||
const parentNode = textarea.parentNode;
|
||||
if (typeof textarea.remove === "function") {
|
||||
textarea.remove();
|
||||
} else if (typeof parentNode?.removeChild === "function") {
|
||||
parentNode.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
export async function writeTextToClipboard(text: string): Promise<boolean> {
|
||||
try {
|
||||
const clipboard = globalThis.navigator?.clipboard;
|
||||
@@ -6,26 +50,209 @@ export async function writeTextToClipboard(text: string): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
const document = globalThis.document;
|
||||
if (!document?.body?.appendChild || !document.execCommand) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.top = "-9999px";
|
||||
textarea.style.left = "-9999px";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
return document.execCommand("copy");
|
||||
} finally {
|
||||
textarea.remove();
|
||||
}
|
||||
return copyTextWithExecCommand(text);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackWriteText(text: string): Promise<void> {
|
||||
try {
|
||||
if (!copyTextWithExecCommand(text)) {
|
||||
return Promise.reject(new Error("Clipboard copy command failed"));
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function hasUsableClipboardItem(): boolean {
|
||||
return typeof globalThis.ClipboardItem === "function";
|
||||
}
|
||||
|
||||
async function readPlainTextFromClipboardItem(
|
||||
item: ClipboardItemLike,
|
||||
): Promise<string> {
|
||||
const plainText = item.items?.["text/plain"];
|
||||
if (typeof plainText === "string") {
|
||||
return plainText;
|
||||
}
|
||||
if (plainText instanceof Blob) {
|
||||
return await plainText.text();
|
||||
}
|
||||
|
||||
if (item.types && !item.types.includes("text/plain")) {
|
||||
throw new Error("Clipboard item is missing text/plain data");
|
||||
}
|
||||
|
||||
if (typeof item.getType !== "function") {
|
||||
throw new Error("Clipboard item cannot read text/plain data");
|
||||
}
|
||||
|
||||
const blob = await item.getType("text/plain");
|
||||
if (blob instanceof Blob) {
|
||||
return await blob.text();
|
||||
}
|
||||
|
||||
throw new Error("Clipboard item text/plain data is not a Blob");
|
||||
}
|
||||
|
||||
function canDefineNavigatorClipboard(
|
||||
navigator: Navigator,
|
||||
descriptor: PropertyDescriptor | undefined,
|
||||
): boolean {
|
||||
if (descriptor) {
|
||||
return descriptor.configurable === true;
|
||||
}
|
||||
return Object.isExtensible(navigator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs browser clipboard fallbacks for Streamdown copy controls by patching
|
||||
* missing navigator.clipboard methods and ClipboardItem when the host permits it.
|
||||
*/
|
||||
export function installClipboardFallback(): void {
|
||||
const navigator = globalThis.navigator;
|
||||
if (!navigator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawClipboard = navigator.clipboard;
|
||||
const clipboard =
|
||||
typeof rawClipboard === "object" && rawClipboard !== null
|
||||
? (rawClipboard as Partial<Clipboard>)
|
||||
: undefined;
|
||||
const clipboardDescriptor = Object.getOwnPropertyDescriptor(
|
||||
navigator,
|
||||
"clipboard",
|
||||
);
|
||||
const hasWriteText = typeof clipboard?.writeText === "function";
|
||||
const hasWrite = typeof clipboard?.write === "function";
|
||||
const hasClipboardItem = hasUsableClipboardItem();
|
||||
|
||||
if (hasWriteText && hasWrite && hasClipboardItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const writeText = hasWriteText
|
||||
? clipboard.writeText!.bind(clipboard)
|
||||
: fallbackWriteText;
|
||||
const write = hasWrite
|
||||
? clipboard.write!.bind(clipboard)
|
||||
: (items: ClipboardItemLike[]) => {
|
||||
const firstItem = items[0];
|
||||
if (!firstItem) {
|
||||
return Promise.reject(new Error("Clipboard item not available"));
|
||||
}
|
||||
|
||||
return readPlainTextFromClipboardItem(firstItem).then(writeText);
|
||||
};
|
||||
|
||||
const fallbackClipboard = clipboard ?? {};
|
||||
|
||||
try {
|
||||
const missingMethods: PropertyDescriptorMap = {};
|
||||
if (!hasWrite) {
|
||||
missingMethods.write = {
|
||||
configurable: true,
|
||||
value: write,
|
||||
writable: true,
|
||||
};
|
||||
}
|
||||
if (!hasWriteText) {
|
||||
missingMethods.writeText = {
|
||||
configurable: true,
|
||||
value: writeText,
|
||||
writable: true,
|
||||
};
|
||||
}
|
||||
|
||||
Object.defineProperties(fallbackClipboard, missingMethods);
|
||||
|
||||
if (
|
||||
!clipboard &&
|
||||
canDefineNavigatorClipboard(navigator, clipboardDescriptor)
|
||||
) {
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: fallbackClipboard,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
if (!canDefineNavigatorClipboard(navigator, clipboardDescriptor)) {
|
||||
// The ClipboardItem fallback below is independent from navigator.clipboard.
|
||||
if (hasClipboardItem) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const replacement = Object.create(clipboard ?? null);
|
||||
for (const methodName of ["read", "readText"] as const) {
|
||||
const method = clipboard?.[methodName];
|
||||
if (typeof method === "function") {
|
||||
Object.defineProperty(replacement, methodName, {
|
||||
configurable: true,
|
||||
value: method.bind(clipboard),
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
Object.defineProperties(replacement, {
|
||||
write: {
|
||||
configurable: true,
|
||||
value: write,
|
||||
writable: true,
|
||||
},
|
||||
writeText: {
|
||||
configurable: true,
|
||||
value: writeText,
|
||||
writable: true,
|
||||
},
|
||||
});
|
||||
try {
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: replacement,
|
||||
});
|
||||
} catch {
|
||||
// The ClipboardItem fallback below is independent from navigator.clipboard.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasClipboardItem) {
|
||||
class ClipboardItemFallback {
|
||||
items: Record<string, Blob | string>;
|
||||
types: string[];
|
||||
|
||||
constructor(items: Record<string, Blob | string>) {
|
||||
this.items = items;
|
||||
this.types = Object.keys(items);
|
||||
}
|
||||
|
||||
getType(type: string): Promise<Blob> {
|
||||
const value = this.items[type];
|
||||
if (value instanceof Blob) {
|
||||
return Promise.resolve(value);
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return Promise.resolve(new Blob([value], { type }));
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error(`Clipboard item is missing ${type} data`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Object.defineProperty(globalThis, "ClipboardItem", {
|
||||
configurable: true,
|
||||
value: ClipboardItemFallback,
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,10 +469,14 @@ export function findToolCallResult(toolCallId: string, messages: Message[]) {
|
||||
}
|
||||
|
||||
export function isHiddenFromUIMessage(message: Message) {
|
||||
const content = extractTextFromMessage(message);
|
||||
return (
|
||||
message.additional_kwargs?.hide_from_ui === true ||
|
||||
(typeof message.name === "string" &&
|
||||
HIDDEN_CONTROL_MESSAGE_NAMES.has(message.name))
|
||||
HIDDEN_CONTROL_MESSAGE_NAMES.has(message.name)) ||
|
||||
(message.type === "human" &&
|
||||
content.includes("<slash_skill_activation>") &&
|
||||
stripUploadedFilesTag(content).length === 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -488,12 +492,13 @@ export interface FileInMessage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip <uploaded_files> tag from message content.
|
||||
* Returns the content with the tag removed.
|
||||
* Strip backend-injected human context tags from message content.
|
||||
* Kept under its historical name because callers use it for uploaded-file
|
||||
* display cleanup.
|
||||
*/
|
||||
export function stripUploadedFilesTag(content: string): string {
|
||||
return content
|
||||
.replace(/<uploaded_files>[\s\S]*?<\/uploaded_files>/g, "")
|
||||
.replace(/<(uploaded_files|slash_skill_activation)>[\s\S]*?<\/\1>/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
@@ -504,6 +509,7 @@ export function stripUploadedFilesTag(content: string): string {
|
||||
* These markers are *not* user copy — they come from:
|
||||
*
|
||||
* - ``UploadsMiddleware`` → ``<uploaded_files>``
|
||||
* - ``SkillActivationMiddleware`` → ``<slash_skill_activation>``
|
||||
* - ``DynamicContextMiddleware`` → ``<system-reminder>`` (carrying
|
||||
* ``<memory>`` / ``<current_date>`` inside)
|
||||
* - ``TodoListMiddleware`` / ``LoopDetectionMiddleware`` style reminders
|
||||
@@ -517,6 +523,7 @@ export function stripUploadedFilesTag(content: string): string {
|
||||
*/
|
||||
export const INTERNAL_MARKER_TAGS = [
|
||||
"uploaded_files",
|
||||
"slash_skill_activation",
|
||||
"system-reminder",
|
||||
"memory",
|
||||
"current_date",
|
||||
|
||||
@@ -364,7 +364,7 @@ export function useThreadStream({
|
||||
loadMore: loadMoreHistory,
|
||||
loading: isHistoryLoading,
|
||||
appendMessages,
|
||||
} = useThreadHistory(onStreamThreadId ?? "");
|
||||
} = useThreadHistory(onStreamThreadId ?? "", { enabled: !isMock });
|
||||
|
||||
// Keep listeners ref updated with latest callbacks
|
||||
useEffect(() => {
|
||||
@@ -854,8 +854,15 @@ export function useThreadStream({
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function useThreadHistory(threadId: string) {
|
||||
const runs = useThreadRuns(threadId);
|
||||
type ThreadHistoryOptions = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export function useThreadHistory(
|
||||
threadId: string,
|
||||
{ enabled = true }: ThreadHistoryOptions = {},
|
||||
) {
|
||||
const runs = useThreadRuns(threadId, { enabled });
|
||||
const threadIdRef = useRef(threadId);
|
||||
const runsRef = useRef(runs.data ?? []);
|
||||
const indexRef = useRef(-1);
|
||||
@@ -864,10 +871,15 @@ export function useThreadHistory(threadId: string) {
|
||||
const loadingRunIdRef = useRef<string | null>(null);
|
||||
const loadedRunIdsRef = useRef<Set<string>>(new Set());
|
||||
const runBeforeSeqRef = useRef<Map<string, number>>(new Map());
|
||||
const loadGenerationRef = useRef(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
const loadMessages = useCallback(async () => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const loadGeneration = loadGenerationRef.current;
|
||||
if (loadingRef.current) {
|
||||
const pendingRunIndex = findLatestUnloadedRunIndex(
|
||||
runsRef.current,
|
||||
@@ -921,12 +933,15 @@ export function useThreadHistory(threadId: string) {
|
||||
}).then((res) => {
|
||||
return res.json();
|
||||
});
|
||||
if (
|
||||
loadGenerationRef.current !== loadGeneration ||
|
||||
threadIdRef.current !== requestThreadId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const _messages = result.data
|
||||
.filter((m) => !m.metadata.caller?.startsWith("middleware:"))
|
||||
.map((m) => m.content);
|
||||
if (threadIdRef.current !== requestThreadId) {
|
||||
return;
|
||||
}
|
||||
setMessages((prev) =>
|
||||
dedupeMessagesByIdentity([..._messages, ...prev]),
|
||||
);
|
||||
@@ -961,16 +976,19 @@ export function useThreadHistory(threadId: string) {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
loadingRef.current = false;
|
||||
loadingRunIdRef.current = null;
|
||||
setLoading(false);
|
||||
if (loadGenerationRef.current === loadGeneration) {
|
||||
loadingRef.current = false;
|
||||
loadingRunIdRef.current = null;
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [enabled]);
|
||||
useEffect(() => {
|
||||
const threadChanged = threadIdRef.current !== threadId;
|
||||
threadIdRef.current = threadId;
|
||||
|
||||
if (threadChanged) {
|
||||
if (!enabled || threadChanged) {
|
||||
loadGenerationRef.current += 1;
|
||||
runsRef.current = [];
|
||||
indexRef.current = -1;
|
||||
pendingLoadRef.current = false;
|
||||
@@ -982,6 +1000,10 @@ export function useThreadHistory(threadId: string) {
|
||||
setMessages([]);
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (runs.data && runs.data.length > 0) {
|
||||
runsRef.current = runs.data ?? [];
|
||||
indexRef.current = findLatestUnloadedRunIndex(
|
||||
@@ -992,14 +1014,15 @@ export function useThreadHistory(threadId: string) {
|
||||
loadMessages().catch(() => {
|
||||
toast.error("Failed to load thread history.");
|
||||
});
|
||||
}, [threadId, runs.data, loadMessages]);
|
||||
}, [enabled, threadId, runs.data, loadMessages]);
|
||||
|
||||
const appendMessages = useCallback((_messages: Message[]) => {
|
||||
setMessages((prev) => {
|
||||
return dedupeMessagesByIdentity([...prev, ..._messages]);
|
||||
});
|
||||
}, []);
|
||||
const hasMore = indexRef.current >= 0 || !runs.data;
|
||||
const hasMore =
|
||||
enabled && Boolean(threadId) && (indexRef.current >= 0 || !runs.data);
|
||||
return {
|
||||
runs: runs.data,
|
||||
messages,
|
||||
@@ -1077,7 +1100,10 @@ export function useThreads(
|
||||
});
|
||||
}
|
||||
|
||||
export function useThreadRuns(threadId?: string) {
|
||||
export function useThreadRuns(
|
||||
threadId?: string,
|
||||
{ enabled = true }: { enabled?: boolean } = {},
|
||||
) {
|
||||
const apiClient = getAPIClient();
|
||||
return useQuery<Run[]>({
|
||||
queryKey: ["thread", threadId],
|
||||
@@ -1088,6 +1114,7 @@ export function useThreadRuns(threadId?: string) {
|
||||
const response = await apiClient.runs.list(threadId);
|
||||
return response;
|
||||
},
|
||||
enabled: enabled && Boolean(threadId),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user