mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: finalize v1.3.0 follow-up fixes
This commit is contained in:
@@ -10,20 +10,15 @@ React 组件层,包含 UI 组件和业务组件。
|
||||
## 文件索引
|
||||
|
||||
- `agent/` - AI Agent 聊天页面组件
|
||||
- `clients/` - 客户端管理组件
|
||||
- `config/` - 配置管理组件
|
||||
- `connect/` - Lime Connect 组件(中转商 API Key 添加)
|
||||
- `flow-monitor/` - LLM 流量监控组件
|
||||
- `general-chat/` - 通用对话功能组件(三栏布局:会话列表 + 聊天区域 + 画布)
|
||||
- `mcp/` - MCP 服务器管理组件(配置管理、运行时控制、工具/提示词/资源浏览与调用)
|
||||
- `plugins/` - 插件管理组件
|
||||
- `provider-pool/` - Provider 凭证池管理组件
|
||||
- `resilience/` - 弹性策略配置组件
|
||||
- `routing/` - 路由规则配置组件
|
||||
- `screenshot-chat/` - 截图对话功能组件(实验室功能)
|
||||
- `settings-v2/` - 设置页面组件(当前主实现)
|
||||
- `skills/` - 技能管理组件
|
||||
- `switch/` - 开关控制组件
|
||||
- `terminal/` - 内置终端组件(使用 Tauri Commands)
|
||||
- `tools/` - 工具页面组件
|
||||
- `widgets/` - 右侧小部件栏组件(移植自 Waveterm)
|
||||
|
||||
@@ -2058,40 +2058,40 @@ export function AgentChatWorkspace({
|
||||
setTaskFiles,
|
||||
});
|
||||
const { handleBackHome, resetTopicLocalState } = useWorkspaceResetRuntime({
|
||||
clearMessages,
|
||||
clearRuntimeTeamState,
|
||||
clearProjectSelectionRuntime,
|
||||
resetRestoredSessionState,
|
||||
resetProjectSelection,
|
||||
resetGuideState,
|
||||
hasHandledNewChatRequest,
|
||||
markNewChatRequestHandled,
|
||||
createFreshSession,
|
||||
defaultTopicSidebarVisible,
|
||||
normalizedInitialTheme: normalizedEntryTheme,
|
||||
initialCreationMode,
|
||||
newChatAt,
|
||||
initialSessionName,
|
||||
projectId,
|
||||
externalProjectId,
|
||||
onNavigate: _onNavigate,
|
||||
autoCollapsedTopicSidebarRef,
|
||||
processedMessageIdsRef: processedMessageIds,
|
||||
setInput,
|
||||
setSelectedText,
|
||||
setLayoutMode,
|
||||
setShowSidebar,
|
||||
setCanvasState,
|
||||
setGeneralCanvasState,
|
||||
setTaskFiles,
|
||||
setSelectedFileId,
|
||||
setBrowserTaskPreflight,
|
||||
setMentionedCharacters,
|
||||
setProject,
|
||||
setProjectMemory,
|
||||
setActiveTheme,
|
||||
setCreationMode,
|
||||
});
|
||||
clearMessages,
|
||||
clearRuntimeTeamState,
|
||||
clearProjectSelectionRuntime,
|
||||
resetRestoredSessionState,
|
||||
resetProjectSelection,
|
||||
resetGuideState,
|
||||
hasHandledNewChatRequest,
|
||||
markNewChatRequestHandled,
|
||||
createFreshSession,
|
||||
defaultTopicSidebarVisible,
|
||||
normalizedInitialTheme: normalizedEntryTheme,
|
||||
initialCreationMode,
|
||||
newChatAt,
|
||||
initialSessionName,
|
||||
projectId,
|
||||
externalProjectId,
|
||||
onNavigate: _onNavigate,
|
||||
autoCollapsedTopicSidebarRef,
|
||||
processedMessageIdsRef: processedMessageIds,
|
||||
setInput,
|
||||
setSelectedText,
|
||||
setLayoutMode,
|
||||
setShowSidebar,
|
||||
setCanvasState,
|
||||
setGeneralCanvasState,
|
||||
setTaskFiles,
|
||||
setSelectedFileId,
|
||||
setBrowserTaskPreflight,
|
||||
setMentionedCharacters,
|
||||
setProject,
|
||||
setProjectMemory,
|
||||
setActiveTheme,
|
||||
setCreationMode,
|
||||
});
|
||||
|
||||
const { switchTopic } = useWorkspaceTopicSwitch({
|
||||
projectId,
|
||||
@@ -2216,6 +2216,7 @@ export function AgentChatWorkspace({
|
||||
ensureBrowserAssistCanvas,
|
||||
handleAutoLaunchMatchedSiteSkill:
|
||||
workspaceServiceSkillEntryActions.handleAutoLaunchMatchedSiteSkill,
|
||||
handleImageWorkbenchCommand,
|
||||
resolveImageWorkbenchSkillRequest,
|
||||
});
|
||||
|
||||
@@ -2739,9 +2740,6 @@ export function AgentChatWorkspace({
|
||||
input,
|
||||
setInput,
|
||||
currentGate,
|
||||
pendingA2UIForm,
|
||||
handleInputbarA2UISubmit,
|
||||
a2uiSubmissionNotice,
|
||||
themeWorkbenchSidebarRuntime,
|
||||
steps,
|
||||
themeWorkbenchRunState,
|
||||
|
||||
+6
-3
@@ -119,6 +119,9 @@ export const InputbarComposerSection: React.FC<
|
||||
buildSkillSelectionBindings(skillSelection);
|
||||
const resolvedProviderType = inputAdapter.model?.providerType;
|
||||
const resolvedModel = inputAdapter.model?.model;
|
||||
const resolvedSetProviderType =
|
||||
inputAdapter.actions.setProviderType || (() => undefined);
|
||||
const resolvedSetModel = inputAdapter.actions.setModel || (() => undefined);
|
||||
const shouldShowVisionNotice =
|
||||
currentPendingImages.length > 0 &&
|
||||
Boolean(resolvedProviderType?.trim()) &&
|
||||
@@ -127,7 +130,7 @@ export const InputbarComposerSection: React.FC<
|
||||
topExtra || shouldShowVisionNotice ? (
|
||||
<>
|
||||
{topExtra}
|
||||
{shouldShowVisionNotice ? (
|
||||
{shouldShowVisionNotice && resolvedProviderType && resolvedModel ? (
|
||||
<InputbarVisionCapabilityNotice
|
||||
providerType={resolvedProviderType}
|
||||
model={resolvedModel}
|
||||
@@ -235,9 +238,9 @@ export const InputbarComposerSection: React.FC<
|
||||
<InputbarModelExtra
|
||||
isFullscreen={isFullscreen}
|
||||
providerType={inputAdapter.model?.providerType}
|
||||
setProviderType={inputAdapter.actions.setProviderType}
|
||||
setProviderType={resolvedSetProviderType}
|
||||
model={inputAdapter.model?.model}
|
||||
setModel={inputAdapter.actions.setModel}
|
||||
setModel={resolvedSetModel}
|
||||
activeTheme={activeTheme}
|
||||
onManageProviders={onManageProviders}
|
||||
executionRuntime={executionRuntime}
|
||||
|
||||
@@ -218,10 +218,7 @@ describe("TeamSelectorPanel", () => {
|
||||
});
|
||||
|
||||
it("命中稳妥模式模型时不应再展示 Team 横幅提示", async () => {
|
||||
const { container } = renderPanel({
|
||||
providerType: "openai",
|
||||
model: "glm-4.7",
|
||||
});
|
||||
const { container } = renderPanel();
|
||||
|
||||
await flushEffects();
|
||||
|
||||
|
||||
@@ -728,12 +728,12 @@ export async function tryExecuteSlashSkillCommand(
|
||||
let imageTaskPreview: MessageImageWorkbenchPreview | null = null;
|
||||
const patched = prev.map((msg) => {
|
||||
if (msg.id !== assistantMsgId) return msg;
|
||||
const currentToolArguments =
|
||||
streamEvent.arguments ||
|
||||
msg.toolCalls?.find((tc) => tc.id === streamEvent.tool_id)
|
||||
?.arguments;
|
||||
const currentToolCall = msg.toolCalls?.find(
|
||||
(tc) => tc.id === streamEvent.tool_id,
|
||||
);
|
||||
const currentToolArguments = currentToolCall?.arguments;
|
||||
imageTaskPreview = buildImageTaskPreviewFromToolResult({
|
||||
toolName: streamEvent.tool_name,
|
||||
toolName: currentToolCall?.name || "",
|
||||
toolArguments: currentToolArguments,
|
||||
toolResult: asRecord(normalizedResult) || undefined,
|
||||
fallbackPrompt: command.userInput || rawContent,
|
||||
|
||||
@@ -7,6 +7,11 @@ import {
|
||||
readFilePreview,
|
||||
type FilePreview,
|
||||
} from "@/lib/api/fileBrowser";
|
||||
import {
|
||||
getMediaTaskArtifact,
|
||||
listMediaTaskArtifacts,
|
||||
type MediaTaskArtifactOutput,
|
||||
} from "@/lib/api/mediaTasks";
|
||||
import { safeListen } from "@/lib/dev-bridge";
|
||||
import type { Message } from "../types";
|
||||
import {
|
||||
@@ -26,6 +31,11 @@ vi.mock("@/lib/api/fileBrowser", () => ({
|
||||
readFilePreview: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/mediaTasks", () => ({
|
||||
getMediaTaskArtifact: vi.fn(),
|
||||
listMediaTaskArtifacts: vi.fn(),
|
||||
}));
|
||||
|
||||
type HookProps = Parameters<typeof useWorkspaceImageTaskPreviewRuntime>[0];
|
||||
type RuntimeHarnessProps = Omit<
|
||||
HookProps,
|
||||
@@ -85,6 +95,27 @@ function createDirectoryListingResult(
|
||||
};
|
||||
}
|
||||
|
||||
function createArtifactOutput(
|
||||
overrides: Partial<MediaTaskArtifactOutput> & {
|
||||
record: MediaTaskArtifactOutput["record"];
|
||||
task_id: string;
|
||||
task_type: string;
|
||||
},
|
||||
): MediaTaskArtifactOutput {
|
||||
return {
|
||||
success: true,
|
||||
task_family: "image",
|
||||
status: "completed",
|
||||
normalized_status: "succeeded",
|
||||
path: `.lime/tasks/${overrides.task_type}/${overrides.task_id}.json`,
|
||||
absolute_path: `/workspace/project-image-1/.lime/tasks/${overrides.task_type}/${overrides.task_id}.json`,
|
||||
artifact_path: `.lime/tasks/${overrides.task_type}/${overrides.task_id}.json`,
|
||||
absolute_artifact_path: `/workspace/project-image-1/.lime/tasks/${overrides.task_type}/${overrides.task_id}.json`,
|
||||
reused_existing: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderHook(
|
||||
props?: Partial<RuntimeHarnessProps>,
|
||||
options?: { initialMessages?: Message[] },
|
||||
@@ -160,6 +191,15 @@ function renderHook(
|
||||
};
|
||||
}
|
||||
|
||||
function getDocumentCanvasContent(
|
||||
canvasState: RuntimeHarnessProps["canvasState"],
|
||||
): string {
|
||||
if (!canvasState || canvasState.type !== "document") {
|
||||
throw new Error("期望 document canvasState");
|
||||
}
|
||||
return canvasState.content;
|
||||
}
|
||||
|
||||
describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
beforeEach(() => {
|
||||
(
|
||||
@@ -170,6 +210,12 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
vi.resetAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(safeListen).mockResolvedValue(vi.fn());
|
||||
vi.mocked(getMediaTaskArtifact).mockRejectedValue(
|
||||
new Error("task artifact unavailable"),
|
||||
);
|
||||
vi.mocked(listMediaTaskArtifacts).mockRejectedValue(
|
||||
new Error("task artifact list unavailable"),
|
||||
);
|
||||
vi.mocked(listDirectory).mockResolvedValue(
|
||||
createDirectoryListingResult(
|
||||
"/workspace/project-image-1/.lime/tasks",
|
||||
@@ -419,6 +465,98 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("task file 预览被截断时,应回退到媒体任务接口继续回填图片结果", async () => {
|
||||
let listener: CreationTaskListener | null = null;
|
||||
vi.mocked(safeListen).mockImplementationOnce(async (_event, handler) => {
|
||||
listener = handler;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
const taskId = "task-image-large-preview-1";
|
||||
const dataUrl = `data:image/png;base64,${"A".repeat(4096)}`;
|
||||
vi.mocked(readFilePreview).mockResolvedValueOnce({
|
||||
path: `/workspace/project-image-1/.lime/tasks/image_generate/${taskId}.json`,
|
||||
content:
|
||||
'{"task_id":"task-image-large-preview-1","result":{"images":[{"url":"data:image/png;base64,AAAA',
|
||||
isBinary: false,
|
||||
size: 512 * 1024,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getMediaTaskArtifact).mockResolvedValueOnce(
|
||||
createArtifactOutput({
|
||||
task_id: taskId,
|
||||
task_type: "image_generate",
|
||||
current_attempt_id: "attempt-large-preview-1",
|
||||
record: withDefaultTaskContext({
|
||||
task_id: taskId,
|
||||
task_type: "image_generate",
|
||||
task_family: "image",
|
||||
status: "completed",
|
||||
normalized_status: "succeeded",
|
||||
created_at: "2026-04-04T11:00:00Z",
|
||||
current_attempt_id: "attempt-large-preview-1",
|
||||
payload: {
|
||||
prompt: "[img:高保真青柠品牌主视觉]",
|
||||
count: 1,
|
||||
size: "1024x1024",
|
||||
},
|
||||
result: {
|
||||
images: [{ url: dataUrl }],
|
||||
},
|
||||
attempts: [
|
||||
{
|
||||
attempt_id: "attempt-large-preview-1",
|
||||
provider: "fal",
|
||||
model: "flux-pro",
|
||||
result_snapshot: {
|
||||
images: [{ url: dataUrl }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const { render, getValue } = renderHook();
|
||||
await render();
|
||||
|
||||
await act(async () => {
|
||||
listener?.({
|
||||
payload: withDefaultTaskContext({
|
||||
task_id: taskId,
|
||||
task_type: "image_generate",
|
||||
task_family: "image",
|
||||
status: "completed",
|
||||
path: `.lime/tasks/image_generate/${taskId}.json`,
|
||||
}),
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(getMediaTaskArtifact).toHaveBeenCalledWith({
|
||||
projectRootPath: DEFAULT_PROJECT_ROOT_PATH,
|
||||
taskRef: taskId,
|
||||
});
|
||||
expect(getValue().messages).toEqual([
|
||||
expect.objectContaining({
|
||||
id: `image-workbench:${taskId}:assistant`,
|
||||
imageWorkbenchPreview: expect.objectContaining({
|
||||
taskId,
|
||||
status: "complete",
|
||||
imageUrl: dataUrl,
|
||||
prompt: "高保真青柠品牌主视觉",
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(getValue().imageWorkbenchState.outputs).toEqual([
|
||||
expect.objectContaining({
|
||||
taskId,
|
||||
url: dataUrl,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("已存在同 taskId 的 skill 消息时,应把 task 预览合并回原消息而不是再插一条伪造消息", async () => {
|
||||
let listener: CreationTaskListener | null = null;
|
||||
vi.mocked(safeListen).mockImplementationOnce(async (event, handler) => {
|
||||
@@ -713,6 +851,66 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("应优先通过媒体任务接口恢复最近的图片任务,避免大 task file 截断", async () => {
|
||||
const taskId = "task-image-restored-api";
|
||||
vi.mocked(listMediaTaskArtifacts).mockResolvedValueOnce({
|
||||
success: true,
|
||||
workspace_root: DEFAULT_PROJECT_ROOT_PATH,
|
||||
artifact_root: `${DEFAULT_PROJECT_ROOT_PATH}/.lime/tasks`,
|
||||
filters: {
|
||||
task_family: "image",
|
||||
limit: 32,
|
||||
},
|
||||
total: 1,
|
||||
tasks: [
|
||||
createArtifactOutput({
|
||||
task_id: taskId,
|
||||
task_type: "image_generate",
|
||||
record: withDefaultTaskContext({
|
||||
task_id: taskId,
|
||||
task_type: "image_generate",
|
||||
task_family: "image",
|
||||
status: "completed",
|
||||
normalized_status: "succeeded",
|
||||
created_at: "2026-04-04T10:00:00Z",
|
||||
payload: {
|
||||
prompt: "[img:通过 API 恢复的青柠主视觉]",
|
||||
count: 1,
|
||||
size: "1024x1024",
|
||||
},
|
||||
result: {
|
||||
images: [{ url: "https://example.com/restored-api-lime.png" }],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { render, getValue } = renderHook();
|
||||
await render();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(getValue().messages).toEqual([
|
||||
expect.objectContaining({
|
||||
id: `image-workbench:${taskId}:assistant`,
|
||||
imageWorkbenchPreview: expect.objectContaining({
|
||||
taskId,
|
||||
imageUrl: "https://example.com/restored-api-lime.png",
|
||||
prompt: "通过 API 恢复的青柠主视觉",
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
expect(listMediaTaskArtifacts).toHaveBeenCalledWith({
|
||||
projectRootPath: DEFAULT_PROJECT_ROOT_PATH,
|
||||
taskFamily: "image",
|
||||
limit: 32,
|
||||
});
|
||||
expect(listDirectory).not.toHaveBeenCalled();
|
||||
expect(readFilePreview).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("应在进入会话时从 task file 恢复最近的图片任务", async () => {
|
||||
vi.mocked(listDirectory)
|
||||
.mockResolvedValueOnce(
|
||||
@@ -1022,10 +1220,10 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
});
|
||||
|
||||
expect(getValue().canvasState?.type).toBe("document");
|
||||
expect(getValue().canvasState?.content).toContain(
|
||||
expect(getDocumentCanvasContent(getValue().canvasState)).toContain(
|
||||
"https://example.com/restored-inline-lime.png",
|
||||
);
|
||||
expect(getValue().canvasState?.content).not.toContain(
|
||||
expect(getDocumentCanvasContent(getValue().canvasState)).not.toContain(
|
||||
"pending-image-task://",
|
||||
);
|
||||
});
|
||||
@@ -1122,7 +1320,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const nextContent = getValue().canvasState?.content || "";
|
||||
const nextContent = getDocumentCanvasContent(getValue().canvasState);
|
||||
const imageIndex = nextContent.indexOf(
|
||||
"https://example.com/restored-inline-section.png",
|
||||
);
|
||||
@@ -1442,11 +1640,13 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
});
|
||||
|
||||
expect(getValue().canvasState?.type).toBe("document");
|
||||
expect(getValue().canvasState?.content).toContain("pending-image-task://");
|
||||
expect(getValue().canvasState?.content).toContain(
|
||||
expect(getDocumentCanvasContent(getValue().canvasState)).toContain(
|
||||
"pending-image-task://",
|
||||
);
|
||||
expect(getDocumentCanvasContent(getValue().canvasState)).toContain(
|
||||
"lime:image-task-slot:document-slot-inline-1",
|
||||
);
|
||||
const pendingContent = getValue().canvasState?.content || "";
|
||||
const pendingContent = getDocumentCanvasContent(getValue().canvasState);
|
||||
const placeholderIndex = pendingContent.indexOf("pending-image-task://");
|
||||
expect(placeholderIndex).toBeGreaterThan(
|
||||
pendingContent.indexOf("这里是被选中的核心观点段落。"),
|
||||
@@ -1499,10 +1699,10 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(getValue().canvasState?.content).toContain(
|
||||
expect(getDocumentCanvasContent(getValue().canvasState)).toContain(
|
||||
"https://example.com/generated-inline-lime.png",
|
||||
);
|
||||
const finalContent = getValue().canvasState?.content || "";
|
||||
const finalContent = getDocumentCanvasContent(getValue().canvasState);
|
||||
const imageIndex = finalContent.indexOf(
|
||||
"https://example.com/generated-inline-lime.png",
|
||||
);
|
||||
@@ -1512,7 +1712,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => {
|
||||
expect(imageIndex).toBeLessThan(
|
||||
finalContent.indexOf("这里是核心观点补充说明。"),
|
||||
);
|
||||
expect(getValue().canvasState?.content).not.toContain(
|
||||
expect(getDocumentCanvasContent(getValue().canvasState)).not.toContain(
|
||||
"pending-image-task://",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useEffect, useRef, type Dispatch, type SetStateAction } from "react";
|
||||
import { listDirectory, readFilePreview } from "@/lib/api/fileBrowser";
|
||||
import type { MediaTaskArtifactOutput } from "@/lib/api/mediaTasks";
|
||||
import {
|
||||
getMediaTaskArtifact,
|
||||
listMediaTaskArtifacts,
|
||||
type MediaTaskArtifactOutput,
|
||||
} from "@/lib/api/mediaTasks";
|
||||
import { safeListen } from "@/lib/dev-bridge";
|
||||
import { resolveAbsoluteWorkspacePath } from "./workspacePath";
|
||||
import type { CanvasStateUnion } from "@/lib/workspace/workbenchCanvas";
|
||||
@@ -92,6 +96,17 @@ interface TaskContextMetadata {
|
||||
contentId?: string;
|
||||
}
|
||||
|
||||
interface LoadedImageTaskSnapshot {
|
||||
snapshot: ParsedImageTaskSnapshot;
|
||||
taskRecord: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface RestoredImageTaskSnapshot extends LoadedImageTaskSnapshot {
|
||||
absolutePath: string;
|
||||
taskType: string;
|
||||
taskFamily: string;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
@@ -1257,12 +1272,7 @@ export function buildImageTaskSnapshotFromArtifactOutput(params: {
|
||||
contentId?: string | null;
|
||||
canvasState: CanvasStateUnion | null;
|
||||
}): ParsedImageTaskSnapshot | null {
|
||||
const record =
|
||||
params.artifact.record &&
|
||||
typeof params.artifact.record === "object" &&
|
||||
!Array.isArray(params.artifact.record)
|
||||
? (params.artifact.record as Record<string, unknown>)
|
||||
: null;
|
||||
const record = asRecord(params.artifact.record);
|
||||
|
||||
if (record) {
|
||||
return (
|
||||
@@ -1313,12 +1323,17 @@ function upsertPreviewMessage(
|
||||
);
|
||||
if (taskMatchedIndex >= 0) {
|
||||
const existingMessage = nextMessages[taskMatchedIndex];
|
||||
const existingPreview = existingMessage.imageWorkbenchPreview;
|
||||
const nextPreview = nextMessage.imageWorkbenchPreview;
|
||||
nextMessages[taskMatchedIndex] = {
|
||||
...existingMessage,
|
||||
imageWorkbenchPreview: {
|
||||
...(existingMessage.imageWorkbenchPreview || {}),
|
||||
...nextMessage.imageWorkbenchPreview,
|
||||
},
|
||||
imageWorkbenchPreview:
|
||||
existingPreview && nextPreview
|
||||
? {
|
||||
...existingPreview,
|
||||
...nextPreview,
|
||||
}
|
||||
: nextPreview || existingPreview,
|
||||
runtimeStatus:
|
||||
nextMessage.runtimeStatus ?? existingMessage.runtimeStatus,
|
||||
};
|
||||
@@ -1419,6 +1434,63 @@ export function useWorkspaceImageTaskPreviewRuntime({
|
||||
let cancelled = false;
|
||||
let unlisten: (() => void) | null = null;
|
||||
|
||||
const loadTaskSnapshotFromArtifactApi = async (
|
||||
taskId: string,
|
||||
): Promise<LoadedImageTaskSnapshot | null> => {
|
||||
const currentProjectRootPath =
|
||||
runtimeContextRef.current.projectRootPath?.trim();
|
||||
if (!currentProjectRootPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const artifact = await getMediaTaskArtifact({
|
||||
projectRootPath: currentProjectRootPath,
|
||||
taskRef: taskId,
|
||||
});
|
||||
if (cancelled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const taskRecord = asRecord(artifact.record);
|
||||
if (!taskRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot = buildImageTaskSnapshotFromArtifactOutput({
|
||||
artifact,
|
||||
projectId: runtimeContextRef.current.projectId,
|
||||
contentId: runtimeContextRef.current.contentId,
|
||||
canvasState: runtimeContextRef.current.canvasState,
|
||||
});
|
||||
if (!snapshot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
taskRecord,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const applyLoadedTaskSnapshot = (params: LoadedImageTaskSnapshot) => {
|
||||
setChatMessages((previous) =>
|
||||
upsertPreviewMessage(previous, params.snapshot.message),
|
||||
);
|
||||
updateCurrentImageWorkbenchState((current) =>
|
||||
mergeImageTaskSnapshot(current, params.snapshot),
|
||||
);
|
||||
syncDocumentInlineImageTask({
|
||||
taskRecord: params.taskRecord,
|
||||
taskId: params.snapshot.taskId,
|
||||
outputs: params.snapshot.outputs,
|
||||
setCanvasState,
|
||||
});
|
||||
};
|
||||
|
||||
const scheduleNextPoll = (taskId: string) => {
|
||||
const trackedTask = trackedTasks.get(taskId);
|
||||
if (!trackedTask || cancelled) {
|
||||
@@ -1442,47 +1514,52 @@ export function useWorkspaceImageTaskPreviewRuntime({
|
||||
trackedTask.polling = true;
|
||||
|
||||
try {
|
||||
const preview = await readFilePreview(
|
||||
trackedTask.absolutePath,
|
||||
IMAGE_TASK_FILE_PREVIEW_MAX_SIZE,
|
||||
);
|
||||
if (cancelled || !trackedTasks.has(taskId)) {
|
||||
return;
|
||||
let loadedSnapshot: LoadedImageTaskSnapshot | null = null;
|
||||
|
||||
try {
|
||||
const preview = await readFilePreview(
|
||||
trackedTask.absolutePath,
|
||||
IMAGE_TASK_FILE_PREVIEW_MAX_SIZE,
|
||||
);
|
||||
if (cancelled || !trackedTasks.has(taskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!preview.error && preview.content?.trim()) {
|
||||
const parsed = JSON.parse(preview.content) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const snapshot = buildParsedImageTaskSnapshot({
|
||||
taskRecord: parsed,
|
||||
taskId: trackedTask.taskId,
|
||||
taskType: trackedTask.taskType,
|
||||
projectId: runtimeContextRef.current.projectId,
|
||||
contentId: runtimeContextRef.current.contentId,
|
||||
canvasState: runtimeContextRef.current.canvasState,
|
||||
});
|
||||
if (snapshot) {
|
||||
loadedSnapshot = {
|
||||
snapshot,
|
||||
taskRecord: parsed,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
loadedSnapshot = null;
|
||||
}
|
||||
|
||||
if (preview.error || !preview.content?.trim()) {
|
||||
if (!loadedSnapshot) {
|
||||
loadedSnapshot = await loadTaskSnapshotFromArtifactApi(taskId);
|
||||
}
|
||||
if (!loadedSnapshot) {
|
||||
scheduleNextPoll(taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(preview.content) as Record<string, unknown>;
|
||||
const snapshot = buildParsedImageTaskSnapshot({
|
||||
taskRecord: parsed,
|
||||
taskId: trackedTask.taskId,
|
||||
taskType: trackedTask.taskType,
|
||||
projectId: runtimeContextRef.current.projectId,
|
||||
contentId: runtimeContextRef.current.contentId,
|
||||
canvasState: runtimeContextRef.current.canvasState,
|
||||
});
|
||||
if (!snapshot) {
|
||||
scheduleNextPoll(taskId);
|
||||
return;
|
||||
}
|
||||
applyLoadedTaskSnapshot(loadedSnapshot);
|
||||
|
||||
setChatMessages((previous) =>
|
||||
upsertPreviewMessage(previous, snapshot.message),
|
||||
);
|
||||
updateCurrentImageWorkbenchState((current) =>
|
||||
mergeImageTaskSnapshot(current, snapshot),
|
||||
);
|
||||
syncDocumentInlineImageTask({
|
||||
taskRecord: parsed,
|
||||
taskId,
|
||||
outputs: snapshot.outputs,
|
||||
setCanvasState,
|
||||
});
|
||||
|
||||
if (snapshot.terminal) {
|
||||
if (loadedSnapshot.snapshot.terminal) {
|
||||
trackedTasks.delete(taskId);
|
||||
return;
|
||||
}
|
||||
@@ -1505,81 +1582,141 @@ export function useWorkspaceImageTaskPreviewRuntime({
|
||||
return;
|
||||
}
|
||||
|
||||
const candidatePaths = await collectImageTaskCandidatePaths(
|
||||
currentProjectRootPath,
|
||||
);
|
||||
if (cancelled || candidatePaths.length === 0) {
|
||||
return;
|
||||
}
|
||||
let restoredSnapshots: RestoredImageTaskSnapshot[] | null = null;
|
||||
|
||||
const restoredSnapshots: Array<{
|
||||
snapshot: ParsedImageTaskSnapshot;
|
||||
taskRecord: Record<string, unknown>;
|
||||
absolutePath: string;
|
||||
taskType: string;
|
||||
taskFamily: string;
|
||||
}> = [];
|
||||
const seenTaskIds = new Set<string>();
|
||||
try {
|
||||
const artifactList = await listMediaTaskArtifacts({
|
||||
projectRootPath: currentProjectRootPath,
|
||||
taskFamily: "image",
|
||||
limit: IMAGE_TASK_RESTORE_LIMIT * 4,
|
||||
});
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const candidatePath of candidatePaths) {
|
||||
try {
|
||||
const preview = await readFilePreview(
|
||||
candidatePath,
|
||||
IMAGE_TASK_FILE_PREVIEW_MAX_SIZE,
|
||||
);
|
||||
if (cancelled || preview.error || !preview.content?.trim()) {
|
||||
continue;
|
||||
restoredSnapshots = artifactList.tasks.reduce<
|
||||
RestoredImageTaskSnapshot[]
|
||||
>((items, artifact) => {
|
||||
const taskRecord = asRecord(artifact.record);
|
||||
if (!taskRecord) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(preview.content) as Record<string, unknown>;
|
||||
if (
|
||||
!shouldRestoreImageTaskRecord({
|
||||
taskRecord: parsed,
|
||||
taskRecord,
|
||||
sessionId: runtimeContextRef.current.sessionId,
|
||||
projectId: runtimeContextRef.current.projectId,
|
||||
contentId: runtimeContextRef.current.contentId,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
return items;
|
||||
}
|
||||
|
||||
const taskId = readString([parsed], ["task_id", "taskId"]);
|
||||
const taskType = readString([parsed], ["task_type", "taskType"]);
|
||||
const taskFamily = normalizeTaskFamily(
|
||||
taskType || "",
|
||||
readString([parsed], ["task_family", "taskFamily"]),
|
||||
artifact.task_type,
|
||||
artifact.task_family,
|
||||
);
|
||||
if (
|
||||
!taskId ||
|
||||
!taskType ||
|
||||
taskFamily !== "image" ||
|
||||
seenTaskIds.has(taskId)
|
||||
) {
|
||||
continue;
|
||||
if (taskFamily !== "image") {
|
||||
return items;
|
||||
}
|
||||
|
||||
const snapshot = buildParsedImageTaskSnapshot({
|
||||
taskRecord: parsed,
|
||||
taskId,
|
||||
taskType,
|
||||
const snapshot = buildImageTaskSnapshotFromArtifactOutput({
|
||||
artifact,
|
||||
projectId: runtimeContextRef.current.projectId,
|
||||
contentId: runtimeContextRef.current.contentId,
|
||||
canvasState: runtimeContextRef.current.canvasState,
|
||||
});
|
||||
if (!snapshot) {
|
||||
continue;
|
||||
return items;
|
||||
}
|
||||
|
||||
seenTaskIds.add(taskId);
|
||||
restoredSnapshots.push({
|
||||
items.push({
|
||||
snapshot,
|
||||
taskRecord: parsed,
|
||||
absolutePath: candidatePath,
|
||||
taskType,
|
||||
taskRecord,
|
||||
absolutePath: artifact.absolute_path,
|
||||
taskType: artifact.task_type,
|
||||
taskFamily,
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
return items;
|
||||
}, []);
|
||||
} catch {
|
||||
restoredSnapshots = null;
|
||||
}
|
||||
|
||||
if (restoredSnapshots === null) {
|
||||
const candidatePaths = await collectImageTaskCandidatePaths(
|
||||
currentProjectRootPath,
|
||||
);
|
||||
if (cancelled || candidatePaths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
restoredSnapshots = [];
|
||||
const seenTaskIds = new Set<string>();
|
||||
|
||||
for (const candidatePath of candidatePaths) {
|
||||
try {
|
||||
const preview = await readFilePreview(
|
||||
candidatePath,
|
||||
IMAGE_TASK_FILE_PREVIEW_MAX_SIZE,
|
||||
);
|
||||
if (cancelled || preview.error || !preview.content?.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(preview.content) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
if (
|
||||
!shouldRestoreImageTaskRecord({
|
||||
taskRecord: parsed,
|
||||
sessionId: runtimeContextRef.current.sessionId,
|
||||
projectId: runtimeContextRef.current.projectId,
|
||||
contentId: runtimeContextRef.current.contentId,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const taskId = readString([parsed], ["task_id", "taskId"]);
|
||||
const taskType = readString([parsed], ["task_type", "taskType"]);
|
||||
const taskFamily = normalizeTaskFamily(
|
||||
taskType || "",
|
||||
readString([parsed], ["task_family", "taskFamily"]),
|
||||
);
|
||||
if (
|
||||
!taskId ||
|
||||
!taskType ||
|
||||
taskFamily !== "image" ||
|
||||
seenTaskIds.has(taskId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const snapshot = buildParsedImageTaskSnapshot({
|
||||
taskRecord: parsed,
|
||||
taskId,
|
||||
taskType,
|
||||
projectId: runtimeContextRef.current.projectId,
|
||||
contentId: runtimeContextRef.current.contentId,
|
||||
canvasState: runtimeContextRef.current.canvasState,
|
||||
});
|
||||
if (!snapshot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenTaskIds.add(taskId);
|
||||
restoredSnapshots.push({
|
||||
snapshot,
|
||||
taskRecord: parsed,
|
||||
absolutePath: candidatePath,
|
||||
taskType,
|
||||
taskFamily,
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1689,7 +1826,7 @@ export function useWorkspaceImageTaskPreviewRuntime({
|
||||
canvasState: runtimeContextRef.current.canvasState,
|
||||
})
|
||||
: null;
|
||||
if (pendingSnapshot) {
|
||||
if (pendingSnapshot && taskId && taskType) {
|
||||
setChatMessages((previous) =>
|
||||
upsertPreviewMessage(previous, pendingSnapshot.message),
|
||||
);
|
||||
|
||||
@@ -906,8 +906,8 @@ export function useWorkspaceImageWorkbenchActionRuntime({
|
||||
: "claw-image-workbench",
|
||||
slotId: documentInlineSlotId,
|
||||
anchorHint: documentInlineAnchorHint,
|
||||
anchorSectionTitle: documentInlineAnchorSectionTitle,
|
||||
anchorText: documentInlineAnchorText,
|
||||
anchorSectionTitle: documentInlineAnchorSectionTitle ?? undefined,
|
||||
anchorText: documentInlineAnchorText ?? undefined,
|
||||
providerId: imageWorkbenchSelectedProviderId,
|
||||
model: imageWorkbenchSelectedModelId,
|
||||
sessionId: imageWorkbenchSessionKey,
|
||||
@@ -932,11 +932,7 @@ export function useWorkspaceImageWorkbenchActionRuntime({
|
||||
);
|
||||
syncDocumentInlineImageTask({
|
||||
taskRecord:
|
||||
createdArtifact.record &&
|
||||
typeof createdArtifact.record === "object" &&
|
||||
!Array.isArray(createdArtifact.record)
|
||||
? (createdArtifact.record as Record<string, unknown>)
|
||||
: {
|
||||
asRecord(createdArtifact.record) || {
|
||||
task_id: createdArtifact.task_id,
|
||||
task_type: createdArtifact.task_type,
|
||||
status: createdArtifact.status,
|
||||
|
||||
@@ -31,7 +31,12 @@ const mockSetChatToolPreferences = vi.fn();
|
||||
const mockSetRuntimeTeamDispatchPreview = vi.fn();
|
||||
const mockEnsureBrowserAssistCanvas = vi.fn(async () => true);
|
||||
const mockHandleAutoLaunchMatchedSiteSkill = vi.fn(async () => undefined);
|
||||
const mockResolveImageWorkbenchSkillRequest = vi.fn(() => null);
|
||||
const mockHandleImageWorkbenchCommand = vi.fn<
|
||||
HookProps["handleImageWorkbenchCommand"]
|
||||
>(async () => true);
|
||||
const mockResolveImageWorkbenchSkillRequest = vi.fn<
|
||||
HookProps["resolveImageWorkbenchSkillRequest"]
|
||||
>(() => null);
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
@@ -198,6 +203,8 @@ function mountHook(initialProps?: Partial<HookProps>): HookHarness {
|
||||
mockEnsureBrowserAssistCanvas as HookProps["ensureBrowserAssistCanvas"],
|
||||
handleAutoLaunchMatchedSiteSkill:
|
||||
mockHandleAutoLaunchMatchedSiteSkill as HookProps["handleAutoLaunchMatchedSiteSkill"],
|
||||
handleImageWorkbenchCommand:
|
||||
mockHandleImageWorkbenchCommand as HookProps["handleImageWorkbenchCommand"],
|
||||
resolveImageWorkbenchSkillRequest:
|
||||
mockResolveImageWorkbenchSkillRequest as HookProps["resolveImageWorkbenchSkillRequest"],
|
||||
...initialProps,
|
||||
@@ -237,6 +244,7 @@ describe("useWorkspaceSendActions", () => {
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
vi.clearAllMocks();
|
||||
mockHandleImageWorkbenchCommand.mockResolvedValue(true);
|
||||
mockResolveImageWorkbenchSkillRequest.mockReturnValue(null);
|
||||
});
|
||||
|
||||
@@ -433,17 +441,7 @@ describe("useWorkspaceSendActions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("纯文本 @配图 应保留原始消息,并通过 image_skill_launch metadata 交给 Agent 调度技能", async () => {
|
||||
mockResolveImageWorkbenchSkillRequest.mockReturnValueOnce({
|
||||
images: [],
|
||||
requestContext: {
|
||||
kind: "image_task",
|
||||
image_task: {
|
||||
mode: "generate",
|
||||
prompt: "一张春日咖啡馆插画",
|
||||
},
|
||||
},
|
||||
});
|
||||
it("纯文本 @配图 应直接命中本地图片任务主链,而不是走 Agent 工具搜索", async () => {
|
||||
const harness = mountHook({
|
||||
input: "@配图 生成 一张春日咖啡馆插画,16:9,出 2 张",
|
||||
});
|
||||
@@ -454,22 +452,22 @@ describe("useWorkspaceSendActions", () => {
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockResolveImageWorkbenchSkillRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessage.mock.calls[0]?.[0]).toBe(
|
||||
"@配图 生成 一张春日咖啡馆插画,16:9,出 2 张",
|
||||
expect(mockHandleImageWorkbenchCommand).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandleImageWorkbenchCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rawText: "@配图 生成 一张春日咖啡馆插画,16:9,出 2 张",
|
||||
parsedCommand: expect.objectContaining({
|
||||
trigger: "@配图",
|
||||
mode: "generate",
|
||||
prompt: "一张春日咖啡馆插画",
|
||||
aspectRatio: "16:9",
|
||||
count: 2,
|
||||
}),
|
||||
images: [],
|
||||
}),
|
||||
);
|
||||
expect(mockSendMessage.mock.calls[0]?.[8]).toMatchObject({
|
||||
requestMetadata: {
|
||||
harness: {
|
||||
allow_model_skills: true,
|
||||
image_skill_launch: {
|
||||
skill_name: "image_generate",
|
||||
kind: "image_task",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockResolveImageWorkbenchSkillRequest).not.toHaveBeenCalled();
|
||||
expect(mockSendMessage).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
@@ -516,16 +514,7 @@ describe("useWorkspaceSendActions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("带引用图的图片命令应保留原始消息,并把结构化 image_skill_launch metadata 交给 Agent", async () => {
|
||||
mockResolveImageWorkbenchSkillRequest.mockReturnValueOnce({
|
||||
images: [],
|
||||
requestContext: {
|
||||
kind: "image_task",
|
||||
image_task: {
|
||||
mode: "edit",
|
||||
},
|
||||
},
|
||||
});
|
||||
it("带引用图的图片命令应直接交给本地图片任务运行时", async () => {
|
||||
const harness = mountHook({
|
||||
input: "@配图 编辑 #img-2 去掉角标,保留主体",
|
||||
});
|
||||
@@ -536,8 +525,8 @@ describe("useWorkspaceSendActions", () => {
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockResolveImageWorkbenchSkillRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockResolveImageWorkbenchSkillRequest).toHaveBeenCalledWith(
|
||||
expect(mockHandleImageWorkbenchCommand).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandleImageWorkbenchCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rawText: "@配图 编辑 #img-2 去掉角标,保留主体",
|
||||
parsedCommand: expect.objectContaining({
|
||||
@@ -548,40 +537,14 @@ describe("useWorkspaceSendActions", () => {
|
||||
images: [],
|
||||
}),
|
||||
);
|
||||
expect(mockSendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessage.mock.calls[0]?.[0]).toBe(
|
||||
"@配图 编辑 #img-2 去掉角标,保留主体",
|
||||
);
|
||||
expect(mockSendMessage.mock.calls[0]?.[8]).toMatchObject({
|
||||
requestMetadata: {
|
||||
harness: {
|
||||
allow_model_skills: true,
|
||||
image_skill_launch: {
|
||||
skill_name: "image_generate",
|
||||
kind: "image_task",
|
||||
image_task: {
|
||||
mode: "edit",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockResolveImageWorkbenchSkillRequest).not.toHaveBeenCalled();
|
||||
expect(mockSendMessage).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("@修图 应接入当前图片 skillRequest 主链", async () => {
|
||||
mockResolveImageWorkbenchSkillRequest.mockReturnValueOnce({
|
||||
images: [],
|
||||
requestContext: {
|
||||
kind: "image_task",
|
||||
image_task: {
|
||||
mode: "edit",
|
||||
target_output_ref_id: "img-2",
|
||||
},
|
||||
},
|
||||
});
|
||||
it("@修图 应直达当前图片任务主链", async () => {
|
||||
const harness = mountHook({
|
||||
input: "@修图 #img-2 去掉角标,保留主体",
|
||||
});
|
||||
@@ -592,8 +555,8 @@ describe("useWorkspaceSendActions", () => {
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockResolveImageWorkbenchSkillRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockResolveImageWorkbenchSkillRequest).toHaveBeenCalledWith(
|
||||
expect(mockHandleImageWorkbenchCommand).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandleImageWorkbenchCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rawText: "@修图 #img-2 去掉角标,保留主体",
|
||||
parsedCommand: expect.objectContaining({
|
||||
@@ -604,41 +567,14 @@ describe("useWorkspaceSendActions", () => {
|
||||
images: [],
|
||||
}),
|
||||
);
|
||||
expect(mockSendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessage.mock.calls[0]?.[0]).toBe(
|
||||
"@修图 #img-2 去掉角标,保留主体",
|
||||
);
|
||||
expect(mockSendMessage.mock.calls[0]?.[8]).toMatchObject({
|
||||
requestMetadata: {
|
||||
harness: {
|
||||
allow_model_skills: true,
|
||||
image_skill_launch: {
|
||||
skill_name: "image_generate",
|
||||
kind: "image_task",
|
||||
image_task: {
|
||||
mode: "edit",
|
||||
target_output_ref_id: "img-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockResolveImageWorkbenchSkillRequest).not.toHaveBeenCalled();
|
||||
expect(mockSendMessage).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("@重绘 应接入当前图片 skillRequest 主链", async () => {
|
||||
mockResolveImageWorkbenchSkillRequest.mockReturnValueOnce({
|
||||
images: [],
|
||||
requestContext: {
|
||||
kind: "image_task",
|
||||
image_task: {
|
||||
mode: "variation",
|
||||
target_output_ref_id: "img-2",
|
||||
},
|
||||
},
|
||||
});
|
||||
it("@重绘 应直达当前图片任务主链", async () => {
|
||||
const harness = mountHook({
|
||||
input: "@重绘 #img-2 更偏插画风,保留主视觉",
|
||||
});
|
||||
@@ -649,8 +585,8 @@ describe("useWorkspaceSendActions", () => {
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockResolveImageWorkbenchSkillRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockResolveImageWorkbenchSkillRequest).toHaveBeenCalledWith(
|
||||
expect(mockHandleImageWorkbenchCommand).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandleImageWorkbenchCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rawText: "@重绘 #img-2 更偏插画风,保留主视觉",
|
||||
parsedCommand: expect.objectContaining({
|
||||
@@ -661,9 +597,39 @@ describe("useWorkspaceSendActions", () => {
|
||||
images: [],
|
||||
}),
|
||||
);
|
||||
expect(mockResolveImageWorkbenchSkillRequest).not.toHaveBeenCalled();
|
||||
expect(mockSendMessage).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("/image 仍应保留原始消息,并通过 image_skill_launch metadata 交给 Agent", async () => {
|
||||
mockResolveImageWorkbenchSkillRequest.mockReturnValueOnce({
|
||||
images: [],
|
||||
requestContext: {
|
||||
kind: "image_task",
|
||||
image_task: {
|
||||
mode: "generate",
|
||||
prompt: "春日咖啡馆插画",
|
||||
},
|
||||
},
|
||||
});
|
||||
const harness = mountHook({
|
||||
input: "/image 春日咖啡馆插画,16:9,出 2 张",
|
||||
});
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
const started = await harness.getValue().handleSend();
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockHandleImageWorkbenchCommand).not.toHaveBeenCalled();
|
||||
expect(mockResolveImageWorkbenchSkillRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessage.mock.calls[0]?.[0]).toBe(
|
||||
"@重绘 #img-2 更偏插画风,保留主视觉",
|
||||
"/image 春日咖啡馆插画,16:9,出 2 张",
|
||||
);
|
||||
expect(mockSendMessage.mock.calls[0]?.[8]).toMatchObject({
|
||||
requestMetadata: {
|
||||
@@ -672,10 +638,6 @@ describe("useWorkspaceSendActions", () => {
|
||||
image_skill_launch: {
|
||||
skill_name: "image_generate",
|
||||
kind: "image_task",
|
||||
image_task: {
|
||||
mode: "variation",
|
||||
target_output_ref_id: "img-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -358,6 +358,11 @@ interface UseWorkspaceSendActionsParams {
|
||||
handleAutoLaunchMatchedSiteSkill: (
|
||||
match: AutoMatchedSiteSkill<ServiceSkillHomeItem>,
|
||||
) => Promise<void>;
|
||||
handleImageWorkbenchCommand: (input: {
|
||||
rawText: string;
|
||||
parsedCommand: ParsedImageWorkbenchCommand;
|
||||
images: MessageImage[];
|
||||
}) => Promise<boolean>;
|
||||
resolveImageWorkbenchSkillRequest: (input: {
|
||||
rawText: string;
|
||||
parsedCommand: ParsedImageWorkbenchCommand;
|
||||
@@ -440,6 +445,7 @@ export function useWorkspaceSendActions({
|
||||
setRuntimeTeamDispatchPreview,
|
||||
ensureBrowserAssistCanvas,
|
||||
handleAutoLaunchMatchedSiteSkill,
|
||||
handleImageWorkbenchCommand,
|
||||
resolveImageWorkbenchSkillRequest,
|
||||
}: UseWorkspaceSendActionsParams) {
|
||||
const [submissionPreview, setSubmissionPreview] =
|
||||
@@ -494,6 +500,16 @@ export function useWorkspaceSendActions({
|
||||
? parseImageWorkbenchCommand(sourceText)
|
||||
: null;
|
||||
if (parsedImageWorkbenchCommand) {
|
||||
if (parsedImageWorkbenchCommand.trigger.trim().startsWith("@")) {
|
||||
return {
|
||||
kind: "done",
|
||||
result: await handleImageWorkbenchCommand({
|
||||
rawText: sourceText,
|
||||
parsedCommand: parsedImageWorkbenchCommand,
|
||||
images: effectiveImages,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const skillRequest = resolveImageWorkbenchSkillRequest({
|
||||
rawText: sourceText,
|
||||
parsedCommand: parsedImageWorkbenchCommand,
|
||||
@@ -712,6 +728,7 @@ export function useWorkspaceSendActions({
|
||||
ensureBrowserAssistCanvas,
|
||||
executionStrategy,
|
||||
handleAutoLaunchMatchedSiteSkill,
|
||||
handleImageWorkbenchCommand,
|
||||
resolveImageWorkbenchSkillRequest,
|
||||
input,
|
||||
maybeStartBrowserTaskPreflight,
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"查看会话详情回调": "View Session Detail Callback",
|
||||
"主组件": "Main Component",
|
||||
"状态": "Status",
|
||||
"// === Resilience Settings (src/components/resilience/*.tsx) ===": "",
|
||||
"重试配置": "Retry Configuration",
|
||||
"故障转移": "Failover",
|
||||
"容错配置": "Fault Tolerance Configuration",
|
||||
@@ -2602,7 +2601,6 @@
|
||||
"系统每": "System checks every",
|
||||
"秒自动检查凭证文件变化": "seconds for credential file changes automatically",
|
||||
"如有更新会自动重新加载并记录日志": "Will automatically reload and log if there are updates",
|
||||
"// === components\\resilience\\FailoverSettings.tsx ===": "",
|
||||
"发生故障时": "When a failure occurs",
|
||||
"系统可自动切换到其他可用": "System can automatically switch to other available",
|
||||
"切换日志记录所有自动切换事件": "Switch log records all automatic switching events",
|
||||
@@ -2617,7 +2615,6 @@
|
||||
"暂无切换记录": "No switch records",
|
||||
"当发生自动切换时": "When automatic switching occurs",
|
||||
"记录将显示在这里": "Records will be displayed here",
|
||||
"// === components\\resilience\\RetrySettings.tsx ===": "",
|
||||
"当请求失败时": "When a request fails",
|
||||
"系统会自动重试指定次数": "System will automatically retry the specified number of times",
|
||||
"使用指数退避策略": "Using exponential backoff strategy",
|
||||
|
||||
@@ -404,7 +404,6 @@
|
||||
"查看会话详情回调": "查看会话详情回调",
|
||||
"主组件": "主组件",
|
||||
"状态": "状态",
|
||||
"// === Resilience Settings (src/components/resilience/*.tsx) ===": "",
|
||||
"重试配置": "重试配置",
|
||||
"故障转移": "故障转移",
|
||||
"容错配置": "容错配置",
|
||||
@@ -2584,7 +2583,6 @@
|
||||
"系统每": "系统每",
|
||||
"秒自动检查凭证文件变化": "秒自动检查凭证文件变化",
|
||||
"如有更新会自动重新加载并记录日志": "如有更新会自动重新加载并记录日志",
|
||||
"// === components\\resilience\\FailoverSettings.tsx ===": "",
|
||||
"发生故障时": "发生故障时",
|
||||
"系统可自动切换到其他可用": "系统可自动切换到其他可用",
|
||||
"切换日志记录所有自动切换事件": "切换日志记录所有自动切换事件",
|
||||
@@ -2599,7 +2597,6 @@
|
||||
"暂无切换记录": "暂无切换记录",
|
||||
"当发生自动切换时": "当发生自动切换时",
|
||||
"记录将显示在这里": "记录将显示在这里",
|
||||
"// === components\\resilience\\RetrySettings.tsx ===": "",
|
||||
"当请求失败时": "当请求失败时",
|
||||
"系统会自动重试指定次数": "系统会自动重试指定次数",
|
||||
"使用指数退避策略": "使用指数退避策略",
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { safeInvoke } from "@/lib/dev-bridge";
|
||||
|
||||
// Retry configuration
|
||||
export interface RetryConfig {
|
||||
max_retries: number;
|
||||
base_delay_ms: number;
|
||||
max_delay_ms: number;
|
||||
retryable_codes: number[];
|
||||
}
|
||||
|
||||
// Failover configuration
|
||||
export interface FailoverConfig {
|
||||
auto_switch: boolean;
|
||||
switch_on_quota: boolean;
|
||||
}
|
||||
|
||||
// Switch log entry
|
||||
export interface SwitchLogEntry {
|
||||
from_provider: string;
|
||||
to_provider: string;
|
||||
failure_type: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export const resilienceApi = {
|
||||
// Retry config
|
||||
async getRetryConfig(): Promise<RetryConfig> {
|
||||
return safeInvoke("get_retry_config");
|
||||
},
|
||||
|
||||
async updateRetryConfig(config: RetryConfig): Promise<void> {
|
||||
return safeInvoke("update_retry_config", { config });
|
||||
},
|
||||
|
||||
// Failover config
|
||||
async getFailoverConfig(): Promise<FailoverConfig> {
|
||||
return safeInvoke("get_failover_config");
|
||||
},
|
||||
|
||||
async updateFailoverConfig(config: FailoverConfig): Promise<void> {
|
||||
return safeInvoke("update_failover_config", { config });
|
||||
},
|
||||
|
||||
// Switch log
|
||||
async getSwitchLog(): Promise<SwitchLogEntry[]> {
|
||||
return safeInvoke("get_switch_log");
|
||||
},
|
||||
|
||||
async clearSwitchLog(): Promise<void> {
|
||||
return safeInvoke("clear_switch_log");
|
||||
},
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import { safeInvoke } from "@/lib/dev-bridge";
|
||||
|
||||
export interface RateLimitConfig {
|
||||
enabled: boolean;
|
||||
requests_per_minute: number;
|
||||
window_secs: number;
|
||||
}
|
||||
|
||||
export interface ConversationConfig {
|
||||
trim_enabled: boolean;
|
||||
max_messages: number;
|
||||
summary_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface HintRouteEntry {
|
||||
hint: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface PairingConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export async function getRateLimitConfig(): Promise<RateLimitConfig> {
|
||||
return await safeInvoke("get_rate_limit_config");
|
||||
}
|
||||
|
||||
export async function updateRateLimitConfig(config: RateLimitConfig): Promise<void> {
|
||||
return await safeInvoke("update_rate_limit_config", { config });
|
||||
}
|
||||
|
||||
export async function getConversationConfig(): Promise<ConversationConfig> {
|
||||
return await safeInvoke("get_conversation_config");
|
||||
}
|
||||
|
||||
export async function updateConversationConfig(config: ConversationConfig): Promise<void> {
|
||||
return await safeInvoke("update_conversation_config", { config });
|
||||
}
|
||||
|
||||
export async function getHintRoutes(): Promise<HintRouteEntry[]> {
|
||||
return await safeInvoke("get_hint_routes");
|
||||
}
|
||||
|
||||
export async function updateHintRoutes(routes: HintRouteEntry[]): Promise<void> {
|
||||
return await safeInvoke("update_hint_routes", { routes });
|
||||
}
|
||||
|
||||
export async function getPairingConfig(): Promise<PairingConfig> {
|
||||
return await safeInvoke("get_pairing_config");
|
||||
}
|
||||
|
||||
export async function updatePairingConfig(config: PairingConfig): Promise<void> {
|
||||
return await safeInvoke("update_pairing_config", { config });
|
||||
}
|
||||
@@ -3858,14 +3858,6 @@ const defaultMocks: Record<string, any> = {
|
||||
// Usage 相关
|
||||
get_kiro_usage: () => ({ usage: {} }),
|
||||
|
||||
// Resilience 相关
|
||||
get_retry_config: () => ({ config: {} }),
|
||||
update_retry_config: () => ({ success: true }),
|
||||
get_failover_config: () => ({ config: {} }),
|
||||
update_failover_config: () => ({ success: true }),
|
||||
get_switch_log: () => ({ logs: [] }),
|
||||
clear_switch_log: () => ({ success: true }),
|
||||
|
||||
// Machine ID 相关
|
||||
get_current_machine_id: () => ({ machine_id: "" }),
|
||||
set_machine_id: () => ({ success: true }),
|
||||
|
||||
Reference in New Issue
Block a user