* feat(canvas): 参考素材 - 增加移除按钮并删除对应连线 * feat(canvas): 完善节点面板交互与拖拽反馈 * fix(canvas): 画布交互与小地图 - 统一浮层层级并完善节点识别 * feat(prompt-optimizer): 增加 AI 提示词优化器及宿主接入 * feat(canvas): 画布 - 增加自动整理节点按钮并补充文档 * chore(web): 前端 - 修正提示词优化器格式 * chore(backend): 后端 - 修正 Go 格式检查 * feat(workspace): 完善诊断导出与本地排障支持 * chore(web): 格式化诊断导出文件 * fix(web-test): 兼容 Bun 测试运行时 * fix(web-test): 对齐模型目录协议断言
79 lines
3.8 KiB
TypeScript
79 lines
3.8 KiB
TypeScript
import type { ReactNode } from "react";
|
||
import { useEffect, useRef } from "react";
|
||
import { App } from "antd";
|
||
|
||
import { createModelChannel, useConfigStore } from "@/stores/use-config-store";
|
||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||
import { useLocalDreaminaModelBootstrap } from "@/stores/use-local-dreamina-model-store";
|
||
import { useLocalRuntimeBootstrap } from "@/stores/use-local-runtime-store";
|
||
import { initializeClientDiagnostics, setDiagnosticUserScope } from "@/services/diagnostics/client-diagnostics";
|
||
import { useUserStore } from "@/stores/use-user-store";
|
||
|
||
export function ClientRootInit({ children }: { children: ReactNode }) {
|
||
const config = useConfigStore((state) => state.config);
|
||
const userId = useUserStore((state) => state.user?.id || "");
|
||
const localRuntimeConfigured = config.channels.some((channel) => channel.transport === "local-runtime" && channel.enabled !== false);
|
||
useLocalRuntimeBootstrap(localRuntimeConfigured);
|
||
useLocalDreaminaModelBootstrap();
|
||
const { message } = App.useApp();
|
||
const handledConfigParams = useRef(false);
|
||
const updateConfig = useConfigStore((state) => state.updateConfig);
|
||
|
||
useEffect(() => {
|
||
initializeClientDiagnostics();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
setDiagnosticUserScope(userId);
|
||
}, [userId]);
|
||
|
||
useEffect(() => {
|
||
const interactiveSelector = 'button, [role="button"], a, [class*="card"], [class*="Card"]';
|
||
const blurPointerFocus = (event: PointerEvent) => {
|
||
if (event.pointerType === "mouse" && event.button !== 0) return;
|
||
const target = event.target instanceof Element ? event.target.closest<HTMLElement>(interactiveSelector) : null;
|
||
if (!target || target.hasAttribute("disabled") || target.getAttribute("aria-disabled") === "true") return;
|
||
// 浏览器可能把鼠标点击误判为 :focus-visible;下一帧只清掉这次指针点击产生的焦点。
|
||
window.requestAnimationFrame(() => {
|
||
if (document.activeElement === target) target.blur();
|
||
});
|
||
};
|
||
document.addEventListener("pointerdown", blurPointerFocus, true);
|
||
return () => document.removeEventListener("pointerdown", blurPointerFocus, true);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (handledConfigParams.current) return;
|
||
const searchParams = new URLSearchParams(window.location.search);
|
||
const baseUrl = searchParams.get("baseUrl") || searchParams.get("baseurl");
|
||
const ignoredApiKey = searchParams.has("apiKey") || searchParams.has("apikey");
|
||
if (!baseUrl && !ignoredApiKey) return;
|
||
handledConfigParams.current = true;
|
||
searchParams.delete("baseUrl");
|
||
searchParams.delete("baseurl");
|
||
searchParams.delete("apiKey");
|
||
searchParams.delete("apikey");
|
||
window.history.replaceState(null, "", `${window.location.pathname}${searchParams.size ? `?${searchParams}` : ""}${window.location.hash}`);
|
||
const firstChannel = config.channels[0];
|
||
updateConfig(
|
||
"channels",
|
||
firstChannel
|
||
? config.channels.map((channel, index) =>
|
||
index === 0
|
||
? {
|
||
...channel,
|
||
...(baseUrl ? { baseUrl } : {}),
|
||
}
|
||
: channel,
|
||
)
|
||
: [createModelChannel({ id: "default", name: "默认渠道", baseUrl: baseUrl || undefined })],
|
||
);
|
||
if (baseUrl) updateConfig("baseUrl", baseUrl);
|
||
navigateToSettings({ section: "channels" });
|
||
if (ignoredApiKey) message.warning("出于安全考虑,链接中的 API Key 已忽略,请在配置中手动填写");
|
||
else message.success("已导入本地直连地址");
|
||
}, [config.channels, message, updateConfig]);
|
||
|
||
return <>{children}</>;
|
||
}
|