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:
coso
2026-03-10 09:20:40 +08:00
parent 276e2064c2
commit 2adf8c2bc2
33 changed files with 1484 additions and 305 deletions
+3
View File
@@ -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}
+36
View File
@@ -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}
+49 -8
View File
@@ -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>
+316 -19
View File
@@ -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}
+9
View File
@@ -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;
}
// ============================================================================
+20
View File
@@ -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,
};
+2
View File
@@ -135,6 +135,8 @@ export interface OpenClawPageParams {
export interface AgentPageParams {
projectId?: string;
contentId?: string;
/** 进入 Agent 时自动发送的首条用户消息 */
initialUserPrompt?: string;
/** 首屏主题(用于左侧导航直达创作主题) */
theme?: string;
/** 是否锁定主题(锁定后不在首屏显示主题切换) */