Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj
This commit is contained in:
commit
440b490b0d
22 files changed
+1051
-190
No files matched your search
@@ -165,10 +165,26 @@ export function SkillPickerSheet({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const validation = await validateUpload.mutateAsync(file);
|
||||
const validation = await validateUpload.mutateAsync({
|
||||
file,
|
||||
targetCategory: "custom",
|
||||
});
|
||||
if (validation.conflict) {
|
||||
const owner = validation.owner_username ?? validation.owner_user_id;
|
||||
if (validation.owned_by_other && owner) {
|
||||
toast.error(
|
||||
`技能「${validation.skill_name}」已由用户「${owner}」上传,不能覆盖。`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (validation.owned_by_me && validation.can_overwrite) {
|
||||
toast.warning(
|
||||
`您已上传过同名技能「${validation.skill_name}」,可到「技能」设置页重复上传并覆盖原版本。`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
toast.warning(
|
||||
`已存在同名技能「${validation.skill_name}」,请到「技能」设置页上传以选择是否覆盖。`,
|
||||
`已存在同名技能「${validation.skill_name}」,当前不可覆盖。`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1201,7 +1201,24 @@ function InstallSkillDialog({
|
||||
const validateMutation = useValidateSkillUpload();
|
||||
const installMutation = useInstallSkillUpload();
|
||||
const setNameZhMutation = useSetSkillNameZh();
|
||||
const generateNameZhMutation = useGenerateSkillNameZh();
|
||||
const generateNameZhMutation = useGenerateSkillNameZh();
|
||||
|
||||
const validateFile = useCallback(
|
||||
async (selected: File, category: "custom" | "public") => {
|
||||
setValidation(null);
|
||||
setValidationError(null);
|
||||
try {
|
||||
const result = await validateMutation.mutateAsync({
|
||||
file: selected,
|
||||
targetCategory: category,
|
||||
});
|
||||
setValidation(result);
|
||||
} catch (err) {
|
||||
setValidationError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
},
|
||||
[validateMutation],
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setFile(null);
|
||||
@@ -1240,19 +1257,12 @@ function InstallSkillDialog({
|
||||
if (!lower.endsWith(".zip") && !lower.endsWith(".skill")) {
|
||||
toast.error(dialog.rejectInvalidExt);
|
||||
return;
|
||||
}
|
||||
setFile(selected);
|
||||
setValidation(null);
|
||||
setValidationError(null);
|
||||
try {
|
||||
const result = await validateMutation.mutateAsync(selected);
|
||||
setValidation(result);
|
||||
} catch (err) {
|
||||
setValidationError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
},
|
||||
[dialog.rejectInvalidExt, validateMutation],
|
||||
);
|
||||
}
|
||||
setFile(selected);
|
||||
await validateFile(selected, targetCategory);
|
||||
},
|
||||
[dialog.rejectInvalidExt, targetCategory, validateFile],
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(e: React.DragEvent<HTMLDivElement>) => {
|
||||
@@ -1329,12 +1339,29 @@ function InstallSkillDialog({
|
||||
}
|
||||
}, [dialog.nameZhSaved, installedName, nameZhDraft, onClose, reset, setNameZhMutation]);
|
||||
|
||||
const isPublicConflict = validation?.conflict && validation.conflict_kind === "public";
|
||||
const isCustomConflict = validation?.conflict && validation.conflict_kind === "custom";
|
||||
// Admin targeting public directory can overwrite an existing public skill.
|
||||
const adminCanOverwritePublic = isAdmin && targetCategory === "public" && !!isPublicConflict;
|
||||
const canInstall = !!validation && !validationError && !isCustomConflict && (adminCanOverwritePublic ? false : !isPublicConflict);
|
||||
const canOverwrite = !!validation && !validationError && (!!isCustomConflict || adminCanOverwritePublic);
|
||||
const isPublicConflict = validation?.conflict && validation.conflict_kind === "public";
|
||||
const isCustomConflict = validation?.conflict && validation.conflict_kind === "custom";
|
||||
// Admin targeting public directory can overwrite an existing public skill.
|
||||
const adminCanOverwritePublic = isAdmin && targetCategory === "public" && !!isPublicConflict;
|
||||
const canInstall = !!validation && !validationError && validation.can_install_as_is;
|
||||
const canOverwrite = !!validation && !validationError && validation.can_overwrite;
|
||||
const ownCustomCanOverwrite = !!isCustomConflict && !!validation?.owned_by_me && canOverwrite;
|
||||
const otherCustomConflict = !!isCustomConflict && !!validation?.owned_by_other;
|
||||
const customConflictMessage = !validation
|
||||
? ""
|
||||
: otherCustomConflict
|
||||
? dialog.conflictOwnedByOther
|
||||
.replace("{name}", validation.skill_name)
|
||||
.replace(
|
||||
"{owner}",
|
||||
validation.owner_username ?? validation.owner_user_id ?? "—",
|
||||
)
|
||||
: validation.owned_by_me
|
||||
? (ownCustomCanOverwrite
|
||||
? dialog.conflictOwnedByMe
|
||||
: dialog.conflictOwnedByMeWrongTarget
|
||||
).replace("{name}", validation.skill_name)
|
||||
: dialog.conflictCustom.replace("{name}", validation.skill_name);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => !o && handleClose()}>
|
||||
@@ -1453,9 +1480,12 @@ function InstallSkillDialog({
|
||||
<input
|
||||
type="radio"
|
||||
name="targetCategory"
|
||||
value={cat}
|
||||
checked={targetCategory === cat}
|
||||
onChange={() => setTargetCategory(cat)}
|
||||
value={cat}
|
||||
checked={targetCategory === cat}
|
||||
onChange={() => {
|
||||
setTargetCategory(cat);
|
||||
if (file) void validateFile(file, cat);
|
||||
}}
|
||||
className="accent-primary"
|
||||
/>
|
||||
<span>{cat === "custom" ? dialog.categoryCustom : dialog.categoryPublic}</span>
|
||||
@@ -1513,15 +1543,18 @@ function InstallSkillDialog({
|
||||
</ul>
|
||||
</Field>
|
||||
)}
|
||||
{isCustomConflict && (
|
||||
<div className="text-amber-600 mt-1 flex items-start gap-2 text-xs">
|
||||
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>
|
||||
{desensitize(
|
||||
dialog.conflictCustom.replace("{name}", validation.skill_name),
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{isCustomConflict && (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-1 flex items-start gap-2 text-xs",
|
||||
ownCustomCanOverwrite ? "text-amber-600" : "text-destructive",
|
||||
)}
|
||||
>
|
||||
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>
|
||||
{desensitize(customConflictMessage)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isPublicConflict && (
|
||||
<div className={cn("mt-1 flex items-start gap-2 text-xs", adminCanOverwritePublic ? "text-amber-600" : "text-destructive")}>
|
||||
|
||||
@@ -591,20 +591,26 @@ export const enUS: Translations = {
|
||||
chooseFile: "Choose file",
|
||||
validating: "Validating archive…",
|
||||
installing: "Installing…",
|
||||
replace: "Replace existing",
|
||||
replace: "Upload again and replace",
|
||||
install: "Install",
|
||||
cancel: "Cancel",
|
||||
nameLabel: "Name",
|
||||
descriptionLabel: "Description",
|
||||
licenseLabel: "License",
|
||||
toolsLabel: "Allowed tools",
|
||||
warningsLabel: "Warnings",
|
||||
conflictCustom:
|
||||
"A custom skill named '{name}' is already installed. Use 'Replace existing' to overwrite (its enabled state will be kept).",
|
||||
warningsLabel: "Warnings",
|
||||
conflictOwnedByMe:
|
||||
"You already uploaded a skill named '{name}'. You may upload it again and replace your existing copy (its enabled state will be kept).",
|
||||
conflictOwnedByMeWrongTarget:
|
||||
"The custom skill '{name}' belongs to you. Switch the target to 'Custom skill' to replace it.",
|
||||
conflictOwnedByOther:
|
||||
"Skill '{name}' was uploaded by user '{owner}' and cannot be replaced. Rename the skill before uploading.",
|
||||
conflictCustom:
|
||||
"A custom skill named '{name}' already exists, but its uploader could not be identified. Rename it to avoid overwriting someone else's skill.",
|
||||
conflictPublic:
|
||||
"A built-in public skill named '{name}' already exists and cannot be overwritten.",
|
||||
conflictPublicAdminHint:
|
||||
"A built-in public skill named '{name}' exists. Switch to 'Built-in' and click 'Replace existing' to overwrite.",
|
||||
conflictPublicAdminHint:
|
||||
"A built-in public skill named '{name}' exists. Click 'Upload again and replace' to overwrite it.",
|
||||
installSuccess: "Skill '{name}' installed.",
|
||||
installSuccessReplaced: "Skill '{name}' replaced.",
|
||||
rejectInvalidExt: "Only .zip or .skill files are accepted.",
|
||||
|
||||
@@ -525,9 +525,12 @@ export interface Translations {
|
||||
nameLabel: string;
|
||||
descriptionLabel: string;
|
||||
licenseLabel: string;
|
||||
toolsLabel: string;
|
||||
warningsLabel: string;
|
||||
conflictCustom: string;
|
||||
toolsLabel: string;
|
||||
warningsLabel: string;
|
||||
conflictOwnedByMe: string;
|
||||
conflictOwnedByMeWrongTarget: string;
|
||||
conflictOwnedByOther: string;
|
||||
conflictCustom: string;
|
||||
conflictPublic: string;
|
||||
conflictPublicAdminHint: string;
|
||||
installSuccess: string;
|
||||
|
||||
@@ -566,18 +566,24 @@ export const zhCN: Translations = {
|
||||
chooseFile: "选择文件",
|
||||
validating: "正在校验文件…",
|
||||
installing: "安装中…",
|
||||
replace: "覆盖已有",
|
||||
replace: "重复上传并覆盖",
|
||||
install: "安装",
|
||||
cancel: "取消",
|
||||
nameLabel: "名称",
|
||||
descriptionLabel: "描述",
|
||||
licenseLabel: "许可证",
|
||||
toolsLabel: "允许的工具",
|
||||
warningsLabel: "警告",
|
||||
conflictCustom:
|
||||
"已存在同名自定义技能 “{name}”。点击「覆盖已有」可替换(原启用状态保留)。",
|
||||
warningsLabel: "警告",
|
||||
conflictOwnedByMe:
|
||||
"您已上传过同名技能 “{name}”,可重复上传并覆盖原版本(原启用状态保留)。",
|
||||
conflictOwnedByMeWrongTarget:
|
||||
"同名技能 “{name}” 是您上传的自定义技能,请切换到「自定义技能」后覆盖。",
|
||||
conflictOwnedByOther:
|
||||
"技能 “{name}” 已由用户 “{owner}” 上传,不能覆盖,请修改技能名称后再上传。",
|
||||
conflictCustom:
|
||||
"已存在同名自定义技能 “{name}”,但无法确认上传用户,为避免误覆盖请修改技能名称。",
|
||||
conflictPublic: `存在同名内置技能 '{name}',无法覆盖。`,
|
||||
conflictPublicAdminHint: `存在同名内置技能 '{name}'。切换到「内置技能」并点击「覆盖已有」可替换。`,
|
||||
conflictPublicAdminHint: `存在同名内置技能 '{name}'。点击「重复上传并覆盖」可替换。`,
|
||||
installSuccess: `技能 '{name}' 已安装。`,
|
||||
installSuccessReplaced: `技能 '{name}' 已替换。`,
|
||||
rejectInvalidExt: `只接受 .zip 或 .skill 文件。`,
|
||||
|
||||
@@ -175,16 +175,24 @@ export interface PreviewSkillInstallResponse {
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface ValidateSkillUploadResponse {
|
||||
ok: boolean;
|
||||
skill_name: string;
|
||||
description: string;
|
||||
license: string | null;
|
||||
allowed_tools: string[];
|
||||
conflict: boolean;
|
||||
conflict_kind: "public" | "custom" | null;
|
||||
warnings: string[];
|
||||
}
|
||||
export interface ValidateSkillUploadResponse {
|
||||
ok: boolean;
|
||||
skill_name: string;
|
||||
description: string;
|
||||
license: string | null;
|
||||
allowed_tools: string[];
|
||||
conflict: boolean;
|
||||
conflict_kind: "public" | "custom" | null;
|
||||
can_install_as_is: boolean;
|
||||
can_overwrite: boolean;
|
||||
owned_by_me: boolean;
|
||||
owned_by_other: boolean;
|
||||
builtin_conflict: boolean;
|
||||
owner_username?: string | null;
|
||||
owner_user_id?: string | null;
|
||||
message: string;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export async function installSkill(
|
||||
request: InstallSkillRequest,
|
||||
@@ -235,13 +243,20 @@ async function parseDetailedError(response: Response, fallback: string): Promise
|
||||
return new Error(message);
|
||||
}
|
||||
|
||||
export async function validateSkillUpload(file: File): Promise<ValidateSkillUploadResponse> {
|
||||
const form = new FormData();
|
||||
form.append("file", file, file.name);
|
||||
const response = await apiFetch(`${getBackendBaseURL()}/api/skills/validate`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
export async function validateSkillUpload(
|
||||
file: File,
|
||||
targetCategory: "public" | "custom" = "custom",
|
||||
): Promise<ValidateSkillUploadResponse> {
|
||||
const form = new FormData();
|
||||
form.append("file", file, file.name);
|
||||
const params = new URLSearchParams({ target_category: targetCategory });
|
||||
const response = await apiFetch(
|
||||
`${getBackendBaseURL()}/api/skills/validate?${params.toString()}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: form,
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw await parseDetailedError(response, "Validation failed");
|
||||
}
|
||||
|
||||
@@ -192,11 +192,17 @@ export function useSetSkillAlwaysOn() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useValidateSkillUpload() {
|
||||
return useMutation({
|
||||
mutationFn: (file: File) => validateSkillUpload(file),
|
||||
});
|
||||
}
|
||||
export function useValidateSkillUpload() {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
file,
|
||||
targetCategory,
|
||||
}: {
|
||||
file: File;
|
||||
targetCategory?: "public" | "custom";
|
||||
}) => validateSkillUpload(file, targetCategory),
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstallSkillUpload() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -320,7 +320,7 @@ export async function stopActiveThreadBeforeClarificationReply(
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
}
|
||||
|
||||
function getStreamErrorMessage(error: unknown): string {
|
||||
function getStreamErrorMessage(error: unknown): string {
|
||||
if (typeof error === "string" && error.trim()) {
|
||||
return error;
|
||||
}
|
||||
@@ -340,8 +340,21 @@ function getStreamErrorMessage(error: unknown): string {
|
||||
return nestedError;
|
||||
}
|
||||
}
|
||||
return "Request failed.";
|
||||
}
|
||||
return "Request failed.";
|
||||
}
|
||||
|
||||
/**
|
||||
* React production error #185 is the minified form of "Maximum update depth
|
||||
* exceeded". It is a client render-loop guard, not a backend/model failure,
|
||||
* so it must never be surfaced as the answer's stream error.
|
||||
*/
|
||||
function isReactMaximumUpdateDepthError(message: string): boolean {
|
||||
return (
|
||||
/maximum update depth exceeded/i.test(message) ||
|
||||
/minified react error\s*#185\b/i.test(message) ||
|
||||
/react\.dev\/errors\/185\b/i.test(message)
|
||||
);
|
||||
}
|
||||
|
||||
const TERMINAL_RUN_STATUSES = new Set([
|
||||
"success",
|
||||
@@ -674,11 +687,17 @@ export function useThreadStream({
|
||||
runMetadataStorageRef.current = getRunMetadataStorage();
|
||||
}
|
||||
|
||||
const thread = useStream<AgentThreadState>({
|
||||
client: getAPIClient(isMock),
|
||||
assistantId: assistantId ?? "lead_agent",
|
||||
threadId: onStreamThreadId,
|
||||
reconnectOnMount: runMetadataStorageRef.current
|
||||
const thread = useStream<AgentThreadState>({
|
||||
client: getAPIClient(isMock),
|
||||
assistantId: assistantId ?? "lead_agent",
|
||||
threadId: onStreamThreadId,
|
||||
// The SDK's current implementation falls back to synchronous external-
|
||||
// store notifications when this option is omitted. A network chunk can
|
||||
// contain dozens of token events, causing nested React renders and the
|
||||
// intermittent production-only React #185 error. Coalesce notifications
|
||||
// to one visual update per frame; stream data itself is not dropped.
|
||||
throttle: 16,
|
||||
reconnectOnMount: runMetadataStorageRef.current
|
||||
? () => runMetadataStorageRef.current!
|
||||
: false,
|
||||
fetchStateHistory: { limit: 1 },
|
||||
@@ -843,10 +862,21 @@ export function useThreadStream({
|
||||
toast(e.message);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
markStreamActivity();
|
||||
const message = getStreamErrorMessage(error);
|
||||
// Replacing a stalled local SSE reader aborts the in-flight fetch. That
|
||||
onError(error) {
|
||||
markStreamActivity();
|
||||
const message = getStreamErrorMessage(error);
|
||||
// A synchronous SDK store notification used to let React's update-depth
|
||||
// guard escape into the stream promise. Do not present that internal UI
|
||||
// exception as a model failure. Reconcile/rejoin on the next task so the
|
||||
// backend run can continue and the current answer is preserved.
|
||||
if (isReactMaximumUpdateDepthError(message)) {
|
||||
window.setTimeout(() => {
|
||||
setStreamHoldOpen(true);
|
||||
void confirmOrRejoinRef.current("error");
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
// Replacing a stalled local SSE reader aborts the in-flight fetch. That
|
||||
// is expected and must not look like a user-facing "network" failure.
|
||||
if (isAbortLikeError(error)) {
|
||||
return;
|
||||
@@ -1675,13 +1705,20 @@ export function useThreadStream({
|
||||
thread.isLoading,
|
||||
) ?? thread.values;
|
||||
|
||||
const mergedMessages =
|
||||
const mergedMessages =
|
||||
Array.isArray(mergedValues?.messages)
|
||||
? (mergedValues.messages as Message[])
|
||||
: thread.messages;
|
||||
|
||||
const mergedThread = {
|
||||
...thread,
|
||||
: thread.messages;
|
||||
|
||||
const visibleThreadError =
|
||||
thread.error &&
|
||||
isReactMaximumUpdateDepthError(getStreamErrorMessage(thread.error))
|
||||
? undefined
|
||||
: thread.error;
|
||||
|
||||
const mergedThread = {
|
||||
...thread,
|
||||
error: visibleThreadError,
|
||||
values: mergedValues,
|
||||
messages:
|
||||
optimisticMessages.length > 0
|
||||
|
||||
Reference in new issue
Block a user