mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: resolve Windows compilation errors and warnings
- Add Manager trait import for windows_startup_cmd.rs - Add explicit type annotations for PathBuf in windows_startup_cmd.rs - Fix unused import warning in screenshot_capture_service.rs - Prefix unused variables with underscore in runner.rs and aster_agent_cmd.rs - Update package.json with Node.js engine requirement
This commit is contained in:
@@ -536,6 +536,9 @@ function AppContent() {
|
||||
onNavigate={handleNavigate}
|
||||
projectId={(pageParams as AgentPageParams).projectId}
|
||||
contentId={(pageParams as AgentPageParams).contentId}
|
||||
initialUserPrompt={
|
||||
(pageParams as AgentPageParams).initialUserPrompt
|
||||
}
|
||||
theme={(pageParams as AgentPageParams).theme}
|
||||
lockTheme={(pageParams as AgentPageParams).lockTheme}
|
||||
fromResources={(pageParams as AgentPageParams).fromResources}
|
||||
|
||||
@@ -4937,6 +4937,42 @@ export function AgentChatPage({
|
||||
onInitialUserPromptConsumed,
|
||||
]);
|
||||
|
||||
// 通用聊天场景:若带有 initialUserPrompt,则自动新建并发送首条消息
|
||||
useEffect(() => {
|
||||
const pendingInitialPrompt = (initialUserPrompt || "").trim();
|
||||
if (
|
||||
!pendingInitialPrompt ||
|
||||
contentId ||
|
||||
messages.length > 0 ||
|
||||
isSending
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (consumedInitialPromptRef.current === pendingInitialPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
consumedInitialPromptRef.current = pendingInitialPrompt;
|
||||
void (async () => {
|
||||
await handleSend(
|
||||
[],
|
||||
chatToolPreferences.webSearch,
|
||||
chatToolPreferences.thinking,
|
||||
pendingInitialPrompt,
|
||||
);
|
||||
onInitialUserPromptConsumed?.();
|
||||
})();
|
||||
}, [
|
||||
chatToolPreferences,
|
||||
contentId,
|
||||
handleSend,
|
||||
initialUserPrompt,
|
||||
isSending,
|
||||
messages.length,
|
||||
onInitialUserPromptConsumed,
|
||||
]);
|
||||
|
||||
// 当 contentId 变化时重置引导状态
|
||||
useEffect(() => {
|
||||
hasTriggeredGuide.current = false;
|
||||
|
||||
@@ -52,6 +52,15 @@ interface FetchModelsResult {
|
||||
error: string | null;
|
||||
request_url?: string | null;
|
||||
diagnostic_hint?: string | null;
|
||||
error_kind?:
|
||||
| "not_found"
|
||||
| "unauthorized"
|
||||
| "forbidden"
|
||||
| "network"
|
||||
| "invalid_response"
|
||||
| "other"
|
||||
| null;
|
||||
should_prompt_error?: boolean;
|
||||
}
|
||||
|
||||
interface TestState {
|
||||
@@ -465,6 +474,23 @@ export function ApiServerPage({ hideHeader = false }: ApiServerPageProps) {
|
||||
"fetch_provider_models_auto",
|
||||
{ providerId: provider },
|
||||
);
|
||||
if (result?.error && result.should_prompt_error) {
|
||||
const diagnostics = [
|
||||
result.error,
|
||||
result.request_url ? `请求地址:${result.request_url}` : null,
|
||||
result.diagnostic_hint ?? null,
|
||||
]
|
||||
.filter((line): line is string =>
|
||||
Boolean(line && line.trim()),
|
||||
)
|
||||
.join(" | ");
|
||||
setMessage({
|
||||
type: "error",
|
||||
text:
|
||||
diagnostics ||
|
||||
"模型列表接口配置异常,请检查 Provider Base URL",
|
||||
});
|
||||
}
|
||||
if (result && result.models && result.models.length > 0) {
|
||||
registryModels = result.models;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { LayoutMode } from "../../types";
|
||||
import { LayoutTransition } from "./LayoutTransition";
|
||||
import {
|
||||
cleanupMountedRoots,
|
||||
mountHarness,
|
||||
setupReactActEnvironment,
|
||||
type MountedRoot,
|
||||
} from "../../../workspace/hooks/testUtils";
|
||||
|
||||
setupReactActEnvironment();
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function LayoutHarness({ mode }: { mode: LayoutMode }) {
|
||||
return (
|
||||
<div style={{ width: "1200px", height: "720px" }}>
|
||||
<LayoutTransition
|
||||
mode={mode}
|
||||
chatContent={<div data-testid="layout-chat-content">chat</div>}
|
||||
canvasContent={<div data-testid="layout-canvas-content">canvas</div>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("LayoutTransition", () => {
|
||||
const mountedRoots: MountedRoot[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
cleanupMountedRoots(mountedRoots);
|
||||
});
|
||||
|
||||
it("chat-canvas 模式应为画布与对话保留分栏间距", () => {
|
||||
const { container } = mountHarness(
|
||||
LayoutHarness,
|
||||
{ mode: "chat-canvas" },
|
||||
mountedRoots,
|
||||
);
|
||||
|
||||
const root = container.querySelector<HTMLElement>(
|
||||
'[data-testid="layout-transition-root"]',
|
||||
);
|
||||
expect(root).not.toBeNull();
|
||||
|
||||
const styles = Array.from(document.head.querySelectorAll("style"))
|
||||
.map((node) => node.textContent || "")
|
||||
.join("\n");
|
||||
|
||||
const hasGapRule = Array.from(root?.classList || []).some((className) =>
|
||||
new RegExp(`\\.${escapeRegExp(className)}\\{[^}]*gap:12px;`).test(styles),
|
||||
);
|
||||
|
||||
expect(hasGapRule).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
const ChatPanel = styled.div<{
|
||||
@@ -53,6 +54,7 @@ const CanvasPanel = styled.div<{
|
||||
position: relative;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform ${({ $duration }) => $duration}ms ease-out,
|
||||
@@ -90,7 +92,7 @@ export const LayoutTransition: React.FC<LayoutTransitionProps> = memo(
|
||||
const canvasStyles = getTransitionStyles("canvas");
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container data-testid="layout-transition-root">
|
||||
<CanvasPanel
|
||||
$visible={isCanvasVisible}
|
||||
$transform={canvasStyles.transform as string}
|
||||
|
||||
@@ -121,9 +121,9 @@ vi.mock("./useImageGen", async () => {
|
||||
|
||||
return {
|
||||
useImageGen: () => {
|
||||
const [selectedImageId, setSelectedImageId] = React.useState<string | null>(
|
||||
images[0].id,
|
||||
);
|
||||
const [selectedImageId, setSelectedImageId] = React.useState<
|
||||
string | null
|
||||
>(images[0].id);
|
||||
const selectedImage =
|
||||
images.find((image) => image.id === selectedImageId) ?? images[0];
|
||||
|
||||
@@ -184,11 +184,17 @@ import ImageGenPage from "./ImageGenPage";
|
||||
|
||||
const mountedRoots: MountedRoot[] = [];
|
||||
|
||||
function renderPage(onNavigate?: (page: Page, params?: PageParams) => void): HTMLDivElement {
|
||||
return renderIntoDom(<ImageGenPage onNavigate={onNavigate} />, mountedRoots).container;
|
||||
function renderPage(
|
||||
onNavigate?: (page: Page, params?: PageParams) => void,
|
||||
): HTMLDivElement {
|
||||
return renderIntoDom(<ImageGenPage onNavigate={onNavigate} />, mountedRoots)
|
||||
.container;
|
||||
}
|
||||
|
||||
function findButtonByText(container: HTMLElement, text: string): HTMLButtonElement {
|
||||
function findButtonByText(
|
||||
container: HTMLElement,
|
||||
text: string,
|
||||
): HTMLButtonElement {
|
||||
const target = Array.from(container.querySelectorAll("button")).find((node) =>
|
||||
node.textContent?.includes(text),
|
||||
);
|
||||
@@ -213,6 +219,10 @@ function getPromptChip(container: HTMLElement): HTMLButtonElement {
|
||||
return chip as HTMLButtonElement;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setReactActEnvironment();
|
||||
|
||||
@@ -271,7 +281,9 @@ describe("ImageGenPage", () => {
|
||||
expect(secondHistoryItem).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
secondHistoryItem?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
secondHistoryItem?.dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
});
|
||||
|
||||
const chipAfter = getPromptChip(container);
|
||||
@@ -291,7 +303,36 @@ describe("ImageGenPage", () => {
|
||||
});
|
||||
|
||||
expect(mockBackfillImagesToResource).toHaveBeenCalledTimes(1);
|
||||
expect(mockBackfillImagesToResource).toHaveBeenCalledWith("project-default");
|
||||
expect(mockBackfillImagesToResource).toHaveBeenCalledWith(
|
||||
"project-default",
|
||||
);
|
||||
expect(mockToast.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("AI 生图布局应锁定父级高度避免小屏裁切输入区", () => {
|
||||
const mounted = renderIntoDom(
|
||||
<div style={{ height: "640px", width: "960px" }}>
|
||||
<ImageGenPage />
|
||||
</div>,
|
||||
mountedRoots,
|
||||
);
|
||||
|
||||
const layout = mounted.container.querySelector<HTMLElement>(
|
||||
'[data-testid="ai-image-gen-layout"]',
|
||||
);
|
||||
expect(layout).not.toBeNull();
|
||||
|
||||
const styles = Array.from(document.head.querySelectorAll("style"))
|
||||
.map((node) => node.textContent || "")
|
||||
.join("\n");
|
||||
|
||||
const hasExpectedRule = Array.from(layout?.classList || []).some(
|
||||
(className) =>
|
||||
new RegExp(
|
||||
`\\.${escapeRegExp(className)}\\{[^}]*height:100%;[^}]*overflow:hidden;`,
|
||||
).test(styles),
|
||||
);
|
||||
|
||||
expect(hasExpectedRule).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,9 +223,11 @@ function getStatusText(status: GeneratedImage["status"]): string {
|
||||
// ==================== Styled Components ====================
|
||||
|
||||
const Container = styled.div`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
`;
|
||||
@@ -454,6 +456,7 @@ const Workspace = styled.main`
|
||||
|
||||
const Canvas = styled.div`
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 10px;
|
||||
background: hsl(var(--background));
|
||||
@@ -1061,7 +1064,7 @@ export function AiImageGenTab({ projectId, onNavigate }: AiImageGenTabProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container data-testid="ai-image-gen-layout">
|
||||
<ControlPanel>
|
||||
{availableProviders.length > 1 && (
|
||||
<Section>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type OpenClawNodeCheckResult,
|
||||
type OpenClawSyncModelEntry,
|
||||
} from "@/lib/api/openclaw";
|
||||
import { getOrCreateDefaultProject } from "@/lib/api/project";
|
||||
|
||||
import { OpenClawConfigurePage } from "./OpenClawConfigurePage";
|
||||
import { OpenClawDashboardPage } from "./OpenClawDashboardPage";
|
||||
@@ -133,6 +134,76 @@ function toSyncModels(
|
||||
}));
|
||||
}
|
||||
|
||||
function openClawOperationLabel(kind: OpenClawOperationKind | null): string {
|
||||
switch (kind) {
|
||||
case "install":
|
||||
return "安装";
|
||||
case "uninstall":
|
||||
return "卸载";
|
||||
case "restart":
|
||||
return "重启";
|
||||
default:
|
||||
return "处理";
|
||||
}
|
||||
}
|
||||
|
||||
function buildOpenClawRepairPrompt(
|
||||
kind: OpenClawOperationKind | null,
|
||||
message: string | null,
|
||||
logs: OpenClawInstallProgressEvent[],
|
||||
systemInfo: {
|
||||
os: string;
|
||||
userAgent: string;
|
||||
installPath: string;
|
||||
nodeStatus: string;
|
||||
gitStatus: string;
|
||||
gatewayStatus: string;
|
||||
gatewayPort: number;
|
||||
healthStatus: string;
|
||||
dashboardUrl: string;
|
||||
},
|
||||
): string {
|
||||
const operationLabel = openClawOperationLabel(kind);
|
||||
const visibleLogs = logs.slice(-40);
|
||||
const summarizedError =
|
||||
visibleLogs
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((log) => log.level === "error" || log.level === "warn")?.message ||
|
||||
message ||
|
||||
"安装/运行过程中出现异常";
|
||||
const logText =
|
||||
visibleLogs.length > 0
|
||||
? visibleLogs
|
||||
.map((log) => `[${log.level.toUpperCase()}] ${log.message}`)
|
||||
.join("\n")
|
||||
: "暂无日志输出";
|
||||
|
||||
return [
|
||||
`我正在${operationLabel} openclaw,但在过程中遇到了这个问题:${summarizedError}。`,
|
||||
"",
|
||||
"请帮我:",
|
||||
"1. 判断最可能的根因",
|
||||
"2. 给出最小可执行的修复步骤",
|
||||
"3. 如果需要修改环境变量、Node/npm、PATH、全局包冲突,请明确指出",
|
||||
"4. 如果可以在当前 ProxyCast / Tauri 项目中修复,也请给出具体修改建议",
|
||||
"",
|
||||
"当前系统信息:",
|
||||
`- 操作系统: ${systemInfo.os}`,
|
||||
`- User Agent: ${systemInfo.userAgent}`,
|
||||
`- OpenClaw 安装路径: ${systemInfo.installPath}`,
|
||||
`- Node.js 状态: ${systemInfo.nodeStatus}`,
|
||||
`- Git 状态: ${systemInfo.gitStatus}`,
|
||||
`- Gateway 状态: ${systemInfo.gatewayStatus}`,
|
||||
`- Gateway 端口: ${systemInfo.gatewayPort}`,
|
||||
`- 健康检查: ${systemInfo.healthStatus}`,
|
||||
`- Dashboard 地址: ${systemInfo.dashboardUrl}`,
|
||||
"",
|
||||
"以下是完整日志:",
|
||||
logText,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderBlockedPage(
|
||||
title: string,
|
||||
description: string,
|
||||
@@ -393,6 +464,35 @@ export function OpenClawPage({ pageParams, onNavigate }: OpenClawPageProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!operationState.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const syncProgressLogs = async () => {
|
||||
try {
|
||||
const logs = await openclawApi.getProgressLogs();
|
||||
if (!cancelled && logs.length > 0) {
|
||||
setInstallLogs(logs);
|
||||
}
|
||||
} catch {
|
||||
// 忽略轮询失败,保留事件流或已有日志
|
||||
}
|
||||
};
|
||||
|
||||
void syncProgressLogs();
|
||||
const timer = window.setInterval(() => {
|
||||
void syncProgressLogs();
|
||||
}, 400);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [operationState.running]);
|
||||
|
||||
const refreshGatewayRuntime = useCallback(async () => {
|
||||
const status = await openclawApi.getStatus();
|
||||
setGatewayStatus(status.status);
|
||||
@@ -598,17 +698,27 @@ export function OpenClawPage({ pageParams, onNavigate }: OpenClawPageProps) {
|
||||
);
|
||||
|
||||
const handleInstall = useCallback(async () => {
|
||||
const preview = await openclawApi
|
||||
.getCommandPreview("install")
|
||||
.catch(() => null);
|
||||
await runProgressOperation(
|
||||
"install",
|
||||
() => openclawApi.install(),
|
||||
"runtime",
|
||||
"install",
|
||||
[
|
||||
{
|
||||
level: "info",
|
||||
message: "已发送安装请求,正在等待后端返回安装命令...",
|
||||
},
|
||||
],
|
||||
preview
|
||||
? [
|
||||
{ level: "info", message: preview.title },
|
||||
...preview.command
|
||||
.split("\n")
|
||||
.map((line) => ({ level: "info" as const, message: line })),
|
||||
]
|
||||
: [
|
||||
{
|
||||
level: "info",
|
||||
message: "已发送安装请求,正在等待后端返回安装命令...",
|
||||
},
|
||||
],
|
||||
);
|
||||
}, [runProgressOperation]);
|
||||
|
||||
@@ -618,18 +728,28 @@ export function OpenClawPage({ pageParams, onNavigate }: OpenClawPageProps) {
|
||||
}
|
||||
|
||||
await closeDashboardWindowSilently();
|
||||
const preview = await openclawApi
|
||||
.getCommandPreview("uninstall")
|
||||
.catch(() => null);
|
||||
|
||||
await runProgressOperation(
|
||||
"uninstall",
|
||||
() => openclawApi.uninstall(),
|
||||
"install",
|
||||
installed ? "configure" : "install",
|
||||
[
|
||||
{
|
||||
level: "info",
|
||||
message: "已发送卸载请求,正在等待后端返回卸载命令...",
|
||||
},
|
||||
],
|
||||
preview
|
||||
? [
|
||||
{ level: "info", message: preview.title },
|
||||
...preview.command
|
||||
.split("\n")
|
||||
.map((line) => ({ level: "info" as const, message: line })),
|
||||
]
|
||||
: [
|
||||
{
|
||||
level: "info",
|
||||
message: "已发送卸载请求,正在等待后端返回卸载命令...",
|
||||
},
|
||||
],
|
||||
() => {
|
||||
clearLastSynced();
|
||||
setSelectedModelId("");
|
||||
@@ -645,20 +765,30 @@ export function OpenClawPage({ pageParams, onNavigate }: OpenClawPageProps) {
|
||||
|
||||
const handleRestart = useCallback(async () => {
|
||||
await closeDashboardWindowSilently();
|
||||
const preview = await openclawApi
|
||||
.getCommandPreview("restart", gatewayPort)
|
||||
.catch(() => null);
|
||||
|
||||
await runProgressOperation(
|
||||
"restart",
|
||||
() => openclawApi.restartGateway(),
|
||||
"runtime",
|
||||
"runtime",
|
||||
[
|
||||
{
|
||||
level: "info",
|
||||
message: "已发送重启请求,正在停止并重新拉起 Gateway...",
|
||||
},
|
||||
],
|
||||
preview
|
||||
? [
|
||||
{ level: "info", message: preview.title },
|
||||
...preview.command
|
||||
.split("\n")
|
||||
.map((line) => ({ level: "info" as const, message: line })),
|
||||
]
|
||||
: [
|
||||
{
|
||||
level: "info",
|
||||
message: "已发送重启请求,正在停止并重新拉起 Gateway...",
|
||||
},
|
||||
],
|
||||
);
|
||||
}, [closeDashboardWindowSilently, runProgressOperation]);
|
||||
}, [closeDashboardWindowSilently, gatewayPort, runProgressOperation]);
|
||||
|
||||
const handleSync = useCallback(async () => {
|
||||
await syncProviderConfig();
|
||||
@@ -779,6 +909,168 @@ export function OpenClawPage({ pageParams, onNavigate }: OpenClawPageProps) {
|
||||
navigateSubpage(operationState.returnSubpage);
|
||||
}, [navigateSubpage, operationState.returnSubpage]);
|
||||
|
||||
const openClawRepairPrompt = useMemo(
|
||||
() =>
|
||||
buildOpenClawRepairPrompt(
|
||||
operationState.kind,
|
||||
operationState.message,
|
||||
installLogs,
|
||||
{
|
||||
os:
|
||||
typeof navigator !== "undefined"
|
||||
? `${navigator.platform || "unknown"} / ${navigator.language || "unknown"}`
|
||||
: "unknown",
|
||||
userAgent:
|
||||
typeof navigator !== "undefined"
|
||||
? navigator.userAgent || "unknown"
|
||||
: "unknown",
|
||||
installPath: installedStatus?.path || "未检测到安装路径",
|
||||
nodeStatus: formatNodeStatus(nodeStatus),
|
||||
gitStatus: formatBinaryStatus(gitStatus, "可用", "未检测到 Git"),
|
||||
gatewayStatus,
|
||||
gatewayPort,
|
||||
healthStatus: healthInfo
|
||||
? `${healthInfo.status}${healthInfo.version ? ` · ${healthInfo.version}` : ""}`
|
||||
: "尚未执行健康检查",
|
||||
dashboardUrl: dashboardUrl || "尚未生成 Dashboard 地址",
|
||||
},
|
||||
),
|
||||
[
|
||||
dashboardUrl,
|
||||
gatewayPort,
|
||||
gatewayStatus,
|
||||
gitStatus,
|
||||
healthInfo,
|
||||
installLogs,
|
||||
installedStatus?.path,
|
||||
nodeStatus,
|
||||
operationState.kind,
|
||||
operationState.message,
|
||||
],
|
||||
);
|
||||
|
||||
const openClawRawLogsText = useMemo(
|
||||
() =>
|
||||
installLogs.length > 0
|
||||
? installLogs
|
||||
.map((log) => `[${log.level.toUpperCase()}] ${log.message}`)
|
||||
.join("\n")
|
||||
: "",
|
||||
[installLogs],
|
||||
);
|
||||
|
||||
const openClawDiagnosticBundleJson = useMemo(
|
||||
() =>
|
||||
JSON.stringify(
|
||||
{
|
||||
generatedAt: new Date().toISOString(),
|
||||
source: "openclaw-progress",
|
||||
operation: operationState.kind,
|
||||
running: operationState.running,
|
||||
message: operationState.message,
|
||||
system: {
|
||||
os:
|
||||
typeof navigator !== "undefined"
|
||||
? `${navigator.platform || "unknown"} / ${navigator.language || "unknown"}`
|
||||
: "unknown",
|
||||
userAgent:
|
||||
typeof navigator !== "undefined"
|
||||
? navigator.userAgent || "unknown"
|
||||
: "unknown",
|
||||
installPath: installedStatus?.path || "未检测到安装路径",
|
||||
nodeStatus: formatNodeStatus(nodeStatus),
|
||||
gitStatus: formatBinaryStatus(gitStatus, "可用", "未检测到 Git"),
|
||||
gatewayStatus,
|
||||
gatewayPort,
|
||||
healthStatus: healthInfo
|
||||
? `${healthInfo.status}${healthInfo.version ? ` · ${healthInfo.version}` : ""}`
|
||||
: "尚未执行健康检查",
|
||||
dashboardUrl: dashboardUrl || "尚未生成 Dashboard 地址",
|
||||
},
|
||||
logs: installLogs,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
[
|
||||
dashboardUrl,
|
||||
gatewayPort,
|
||||
gatewayStatus,
|
||||
gitStatus,
|
||||
healthInfo,
|
||||
installLogs,
|
||||
installedStatus?.path,
|
||||
nodeStatus,
|
||||
operationState.kind,
|
||||
operationState.message,
|
||||
operationState.running,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCopyOpenClawRepairPrompt = useCallback(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(openClawRepairPrompt);
|
||||
toast.success("OpenClaw 修复提示词已复制。");
|
||||
} catch {
|
||||
toast.error("复制修复提示词失败。");
|
||||
}
|
||||
}, [openClawRepairPrompt]);
|
||||
|
||||
const handleCopyOpenClawLogs = useCallback(async () => {
|
||||
if (!openClawRawLogsText.trim()) {
|
||||
toast.error("当前没有可复制的日志。");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(openClawRawLogsText);
|
||||
toast.success("OpenClaw 纯日志已复制。");
|
||||
} catch {
|
||||
toast.error("复制纯日志失败。");
|
||||
}
|
||||
}, [openClawRawLogsText]);
|
||||
|
||||
const handleCopyOpenClawDiagnosticBundle = useCallback(async () => {
|
||||
if (!openClawRawLogsText.trim()) {
|
||||
toast.error("当前没有可复制的诊断内容。");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(openClawDiagnosticBundleJson);
|
||||
toast.success("OpenClaw JSON 诊断包已复制。");
|
||||
} catch {
|
||||
toast.error("复制 JSON 诊断包失败。");
|
||||
}
|
||||
}, [openClawDiagnosticBundleJson, openClawRawLogsText]);
|
||||
|
||||
const handleAskAgentFixOpenClaw = useCallback(async () => {
|
||||
const prompt = openClawRepairPrompt.trim();
|
||||
if (!prompt) {
|
||||
toast.error("当前没有可用于诊断的日志内容。");
|
||||
return;
|
||||
}
|
||||
|
||||
const project = await getOrCreateDefaultProject().catch((error) => {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "创建默认项目失败。",
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
onNavigate?.("agent", {
|
||||
projectId: project.id,
|
||||
initialUserPrompt: prompt,
|
||||
newChatAt: Date.now(),
|
||||
theme: "general",
|
||||
lockTheme: false,
|
||||
});
|
||||
}, [onNavigate, openClawRepairPrompt]);
|
||||
|
||||
if (!statusResolved && !operationState.running) {
|
||||
return (
|
||||
<div className="flex min-h-full flex-col items-center justify-center px-6 py-10">
|
||||
@@ -838,7 +1130,12 @@ export function OpenClawPage({ pageParams, onNavigate }: OpenClawPageProps) {
|
||||
}
|
||||
message={operationState.message}
|
||||
logs={installLogs}
|
||||
repairPrompt={openClawRepairPrompt}
|
||||
onClose={handleCloseProgress}
|
||||
onCopyLogs={() => void handleCopyOpenClawLogs()}
|
||||
onCopyDiagnosticBundle={() => void handleCopyOpenClawDiagnosticBundle()}
|
||||
onCopyRepairPrompt={() => void handleCopyOpenClawRepairPrompt()}
|
||||
onAskAgentFix={handleAskAgentFixOpenClaw}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Bot, Copy, Loader2 } from "lucide-react";
|
||||
import type { OpenClawInstallProgressEvent } from "@/lib/api/openclaw";
|
||||
import type { OpenClawOperationKind } from "./types";
|
||||
import { OpenClawMark } from "./OpenClawMark";
|
||||
@@ -8,7 +8,12 @@ interface OpenClawProgressPageProps {
|
||||
running: boolean;
|
||||
message: string | null;
|
||||
logs: OpenClawInstallProgressEvent[];
|
||||
repairPrompt: string;
|
||||
onClose: () => void;
|
||||
onCopyLogs: () => void;
|
||||
onCopyDiagnosticBundle: () => void;
|
||||
onCopyRepairPrompt: () => void;
|
||||
onAskAgentFix: () => void;
|
||||
}
|
||||
|
||||
const titleMap: Record<OpenClawOperationKind, string> = {
|
||||
@@ -28,7 +33,12 @@ export function OpenClawProgressPage({
|
||||
running,
|
||||
message,
|
||||
logs,
|
||||
repairPrompt,
|
||||
onClose,
|
||||
onCopyLogs,
|
||||
onCopyDiagnosticBundle,
|
||||
onCopyRepairPrompt,
|
||||
onAskAgentFix,
|
||||
}: OpenClawProgressPageProps) {
|
||||
return (
|
||||
<div className="flex min-h-full flex-col items-center justify-center px-6 py-10">
|
||||
@@ -51,15 +61,53 @@ export function OpenClawProgressPage({
|
||||
{running ? "命令执行中,请稍候。" : message || "操作已结束。"}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={running}
|
||||
className="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-60"
|
||||
>
|
||||
{running ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
{running ? "处理中" : "关闭"}
|
||||
</button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopyLogs}
|
||||
disabled={logs.length === 0}
|
||||
className="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-60"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
复制纯日志
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopyDiagnosticBundle}
|
||||
disabled={logs.length === 0}
|
||||
className="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-60"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
复制 JSON 诊断包
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopyRepairPrompt}
|
||||
disabled={!repairPrompt.trim()}
|
||||
className="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-60"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
复制修复提示词
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAskAgentFix}
|
||||
disabled={!repairPrompt.trim()}
|
||||
className="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-60"
|
||||
>
|
||||
<Bot className="h-4 w-4" />
|
||||
交给 AI 修复
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={running}
|
||||
className="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-60"
|
||||
>
|
||||
{running ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
{running ? "处理中" : "关闭"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 max-h-[360px] min-h-[260px] overflow-auto rounded-xl bg-slate-950 p-4 text-sm text-slate-100">
|
||||
|
||||
@@ -60,6 +60,15 @@ interface FetchModelsResult {
|
||||
error: string | null;
|
||||
request_url?: string | null;
|
||||
diagnostic_hint?: string | null;
|
||||
error_kind?:
|
||||
| "not_found"
|
||||
| "unauthorized"
|
||||
| "forbidden"
|
||||
| "network"
|
||||
| "invalid_response"
|
||||
| "other"
|
||||
| null;
|
||||
should_prompt_error?: boolean;
|
||||
}
|
||||
|
||||
function buildApiDiagnosticLines(result: {
|
||||
@@ -252,6 +261,7 @@ export const ProviderModelList: React.FC<ProviderModelListProps> = ({
|
||||
const [apiDiagnosticHint, setApiDiagnosticHint] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [apiShouldPromptError, setApiShouldPromptError] = useState(false);
|
||||
|
||||
// 从 API 获取模型列表(自动获取 API Key)
|
||||
const handleRefreshFromApi = useCallback(async () => {
|
||||
@@ -259,6 +269,7 @@ export const ProviderModelList: React.FC<ProviderModelListProps> = ({
|
||||
setApiError(null);
|
||||
setApiRequestUrl(null);
|
||||
setApiDiagnosticHint(null);
|
||||
setApiShouldPromptError(false);
|
||||
|
||||
try {
|
||||
const result = await invoke<FetchModelsResult>(
|
||||
@@ -273,6 +284,7 @@ export const ProviderModelList: React.FC<ProviderModelListProps> = ({
|
||||
setApiSource(result.source);
|
||||
setApiRequestUrl(result.request_url ?? null);
|
||||
setApiDiagnosticHint(result.diagnostic_hint ?? null);
|
||||
setApiShouldPromptError(Boolean(result.should_prompt_error));
|
||||
if (result.error) {
|
||||
setApiError(result.error);
|
||||
}
|
||||
@@ -383,6 +395,11 @@ export const ProviderModelList: React.FC<ProviderModelListProps> = ({
|
||||
</div>
|
||||
{apiError && (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50/80 px-3 py-2 text-left text-xs text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/20 dark:text-amber-300">
|
||||
{apiShouldPromptError ? (
|
||||
<div className="mb-1 font-semibold text-red-600 dark:text-red-400">
|
||||
检测到 Provider 配置错误,请优先修正 Base URL 或鉴权配置
|
||||
</div>
|
||||
) : null}
|
||||
{apiDiagnosticLines.map((line) => (
|
||||
<div key={line} className="break-all leading-5">
|
||||
{line}
|
||||
@@ -470,6 +487,11 @@ export const ProviderModelList: React.FC<ProviderModelListProps> = ({
|
||||
{/* API 错误提示 */}
|
||||
{apiError && (
|
||||
<div className="mb-2 rounded-md border border-amber-200 bg-amber-50/80 px-3 py-2 text-xs text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/20 dark:text-amber-300">
|
||||
{apiShouldPromptError ? (
|
||||
<div className="mb-1 font-semibold text-red-600 dark:text-red-400">
|
||||
检测到 Provider 配置错误,请优先修正 Base URL 或鉴权配置
|
||||
</div>
|
||||
) : null}
|
||||
{apiDiagnosticLines.map((line) => (
|
||||
<div key={line} className="break-all leading-5">
|
||||
{line}
|
||||
|
||||
@@ -44,6 +44,15 @@ interface FetchModelsResult {
|
||||
error: string | null;
|
||||
request_url?: string | null;
|
||||
diagnostic_hint?: string | null;
|
||||
error_kind?:
|
||||
| "not_found"
|
||||
| "unauthorized"
|
||||
| "forbidden"
|
||||
| "network"
|
||||
| "invalid_response"
|
||||
| "other"
|
||||
| null;
|
||||
should_prompt_error?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -27,6 +27,11 @@ export interface OpenClawActionResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface OpenClawCommandPreview {
|
||||
title: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
export interface OpenClawGatewayStatusInfo {
|
||||
status: OpenClawGatewayStatus;
|
||||
port: number;
|
||||
@@ -89,6 +94,13 @@ export async function openclawInstall(): Promise<OpenClawActionResult> {
|
||||
return safeInvoke("openclaw_install");
|
||||
}
|
||||
|
||||
export async function openclawGetCommandPreview(
|
||||
operation: "install" | "uninstall" | "start" | "stop" | "restart",
|
||||
port?: number,
|
||||
): Promise<OpenClawCommandPreview> {
|
||||
return safeInvoke("openclaw_get_command_preview", { operation, port });
|
||||
}
|
||||
|
||||
export async function openclawUninstall(): Promise<OpenClawActionResult> {
|
||||
return safeInvoke("openclaw_uninstall");
|
||||
}
|
||||
@@ -138,6 +150,12 @@ export async function listenOpenClawInstallProgress(
|
||||
);
|
||||
}
|
||||
|
||||
export async function openclawGetProgressLogs(): Promise<
|
||||
OpenClawInstallProgressEvent[]
|
||||
> {
|
||||
return safeInvoke("openclaw_get_progress_logs");
|
||||
}
|
||||
|
||||
export const openclawApi = {
|
||||
checkInstalled: openclawCheckInstalled,
|
||||
checkNodeVersion: openclawCheckNodeVersion,
|
||||
@@ -145,6 +163,7 @@ export const openclawApi = {
|
||||
getNodeDownloadUrl: openclawGetNodeDownloadUrl,
|
||||
getGitDownloadUrl: openclawGetGitDownloadUrl,
|
||||
install: openclawInstall,
|
||||
getCommandPreview: openclawGetCommandPreview,
|
||||
uninstall: openclawUninstall,
|
||||
startGateway: openclawStartGateway,
|
||||
stopGateway: openclawStopGateway,
|
||||
@@ -153,6 +172,7 @@ export const openclawApi = {
|
||||
checkHealth: openclawCheckHealth,
|
||||
getDashboardUrl: openclawGetDashboardUrl,
|
||||
getChannels: openclawGetChannels,
|
||||
getProgressLogs: openclawGetProgressLogs,
|
||||
syncProviderConfig: openclawSyncProviderConfig,
|
||||
listenInstallProgress: listenOpenClawInstallProgress,
|
||||
};
|
||||
|
||||
@@ -135,6 +135,8 @@ export interface OpenClawPageParams {
|
||||
export interface AgentPageParams {
|
||||
projectId?: string;
|
||||
contentId?: string;
|
||||
/** 进入 Agent 时自动发送的首条用户消息 */
|
||||
initialUserPrompt?: string;
|
||||
/** 首屏主题(用于左侧导航直达创作主题) */
|
||||
theme?: string;
|
||||
/** 是否锁定主题(锁定后不在首屏显示主题切换) */
|
||||
|
||||
Reference in New Issue
Block a user