mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 00:34:22 +08:00
feat(agent): add site tools support for canvas project listing and media generation
This commit is contained in:
@@ -9,6 +9,7 @@ import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { useAgentStore, type AgentAttachment, type AgentChatItem, type AgentEventLog, type AgentPanelTab, type AgentPendingToolCall, type AgentThreadSummary } from "@/stores/use-agent-store";
|
||||
import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
import { isSiteTool, runSiteTool, SITE_TOOL_LABELS } from "@/lib/agent/agent-site-tools";
|
||||
import { AgentChatComposer, AgentChatMessage, AgentPanelTabs, AgentPendingToolCard, AgentWorkingMessage, type CanvasAgentChatAttachment } from "./canvas-agent-chat-ui";
|
||||
|
||||
const MAX_ATTACHMENTS = 6;
|
||||
@@ -252,6 +253,23 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
};
|
||||
|
||||
const runToolCall = async (endpoint: string, token: string, payload: AgentPendingToolCall) => {
|
||||
if (isSiteTool(payload.name)) {
|
||||
try {
|
||||
setAgentState({ activity: SITE_TOOL_LABELS[payload.name], waiting: true });
|
||||
addEventLog(toolName(payload.name), payload, payload);
|
||||
const result = await runSiteTool(payload.name, payload.input || {}, navigate);
|
||||
await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, result });
|
||||
setAgentState({ activity: "工具完成", waiting: true });
|
||||
addEventLog(`${toolName(payload.name)}完成`, result, result);
|
||||
addMessage({ role: "tool", title: `${toolName(payload.name)}完成`, text: siteToolSummary(payload.name, result), detail: { requestId: payload.requestId, name: payload.name, input: payload.input, result } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "工具执行失败";
|
||||
setAgentState({ activity: "工具失败", waiting: false });
|
||||
addMessage({ role: "tool", title: "工具失败", text: message, detail: payload });
|
||||
await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, error: message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const input: { ops?: CanvasAgentOp[]; path?: string } = payload.input || {};
|
||||
setAgentState({ activity: payload.name === "canvas_apply_ops" ? "执行画布操作" : payload.name === "site_navigate" ? "跳转页面" : "读取画布", waiting: true });
|
||||
@@ -877,9 +895,21 @@ function toolName(name: string) {
|
||||
if (name === "canvas_set_viewport") return "调整视口";
|
||||
if (name === "canvas_run_generation") return "触发生成";
|
||||
if (name === "site_navigate") return "网站跳转";
|
||||
if (isSiteTool(name)) return SITE_TOOL_LABELS[name];
|
||||
return name;
|
||||
}
|
||||
|
||||
function siteToolSummary(name: string, result: unknown) {
|
||||
const data = result && typeof result === "object" ? (result as Record<string, unknown>) : {};
|
||||
if (name === "canvas_list_projects") return `共 ${numberField(data, "total")} 个画布`;
|
||||
if (name === "prompts_search") return `找到 ${numberField(data, "total")} 条提示词`;
|
||||
if (name === "assets_list") return `共 ${numberField(data, "total")} 个素材`;
|
||||
if (name === "assets_add") return "已加入我的素材";
|
||||
if (name === "workbench_image_generate" || name === "workbench_video_generate") return typeof data.note === "string" ? data.note : "已在工作台执行";
|
||||
if (name === "workbench_image_get_config" || name === "workbench_video_get_config") return "已读取工作台配置";
|
||||
return "已完成";
|
||||
}
|
||||
|
||||
function isReadTool(name: string) {
|
||||
return name === "canvas_get_state" || name === "canvas_get_selection" || name === "canvas_export_snapshot";
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ const aspectOptions = [
|
||||
{ value: "auto", label: "auto", width: 0, height: 0, icon: "auto" },
|
||||
];
|
||||
|
||||
export const imageQualityOptions = qualityOptions.map((item) => ({ value: item.value, label: item.label }));
|
||||
export const imageAspectOptions = aspectOptions.map((item) => ({ value: item.size || item.value, label: item.label }));
|
||||
|
||||
type ImageSettingsPanelProps = {
|
||||
config: AiConfig;
|
||||
onConfigChange: (key: "quality" | "size" | "count", value: string) => void;
|
||||
|
||||
@@ -22,6 +22,10 @@ const sizeOptions = [
|
||||
|
||||
const secondOptions = [6, 10, 12, 16, 20];
|
||||
|
||||
export const videoResolutionOptions = resolutionOptions.map((item) => ({ value: item.value, label: item.label }));
|
||||
export const videoSizeOptions = sizeOptions.map((item) => ({ value: item.value, label: item.label }));
|
||||
export const videoSecondOptions = secondOptions.map((value) => String(value));
|
||||
|
||||
type VideoSettingsPanelProps = {
|
||||
config: AiConfig;
|
||||
onConfigChange: (key: "vquality" | "size" | "videoSeconds" | "videoGenerateAudio" | "videoWatermark", value: string) => void;
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import type { NavigateFunction } from "react-router-dom";
|
||||
|
||||
import { fetchPrompts } from "@/services/api/prompts";
|
||||
import { uploadImage } from "@/services/image-storage";
|
||||
import { imageAspectOptions, imageQualityOptions } from "@/components/image-settings-panel";
|
||||
import { videoResolutionOptions, videoSecondOptions, videoSizeOptions } from "@/components/video-settings-panel";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { modelOptionLabel, modelOptionName, normalizeModelOptionValue, useConfigStore } from "@/stores/use-config-store";
|
||||
import { useWorkbenchAgentStore } from "@/stores/use-workbench-agent-store";
|
||||
|
||||
// 在网页端执行 Agent 的「站点级」工具(画布列表、工作台生成、提示词搜索、素材增删查等)。
|
||||
// 这些工具的数据都在浏览器本地(localforage / zustand),因此由本模块直接读写对应 store 后返回结果。
|
||||
|
||||
export const SITE_TOOL_NAMES = [
|
||||
"canvas_list_projects",
|
||||
"workbench_image_get_config",
|
||||
"workbench_image_generate",
|
||||
"workbench_video_get_config",
|
||||
"workbench_video_generate",
|
||||
"prompts_search",
|
||||
"assets_list",
|
||||
"assets_add",
|
||||
] as const;
|
||||
|
||||
export type SiteToolName = (typeof SITE_TOOL_NAMES)[number];
|
||||
|
||||
export function isSiteTool(name: string): name is SiteToolName {
|
||||
return (SITE_TOOL_NAMES as readonly string[]).includes(name);
|
||||
}
|
||||
|
||||
export const SITE_TOOL_LABELS: Record<SiteToolName, string> = {
|
||||
canvas_list_projects: "画布列表",
|
||||
workbench_image_get_config: "生图配置",
|
||||
workbench_image_generate: "生图工作台生成",
|
||||
workbench_video_get_config: "视频配置",
|
||||
workbench_video_generate: "视频创作台生成",
|
||||
prompts_search: "搜索提示词",
|
||||
assets_list: "素材列表",
|
||||
assets_add: "添加素材",
|
||||
};
|
||||
|
||||
type SiteToolInput = Record<string, unknown>;
|
||||
|
||||
export async function runSiteTool(name: SiteToolName, input: SiteToolInput, navigate: NavigateFunction): Promise<unknown> {
|
||||
switch (name) {
|
||||
case "canvas_list_projects":
|
||||
return listCanvasProjects(input);
|
||||
case "workbench_image_get_config":
|
||||
return getImageConfig();
|
||||
case "workbench_image_generate":
|
||||
return runImageWorkbench(input, navigate);
|
||||
case "workbench_video_get_config":
|
||||
return getVideoConfig();
|
||||
case "workbench_video_generate":
|
||||
return runVideoWorkbench(input, navigate);
|
||||
case "prompts_search":
|
||||
return searchPrompts(input);
|
||||
case "assets_list":
|
||||
return listAssets(input);
|
||||
case "assets_add":
|
||||
return addAsset(input);
|
||||
default:
|
||||
throw new Error(`未知工具:${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function listCanvasProjects(input: SiteToolInput) {
|
||||
const { projects, hydrated } = useCanvasStore.getState();
|
||||
if (!hydrated) throw new Error("画布还在加载中,请稍后重试");
|
||||
const keyword = String(input.keyword || "").trim().toLowerCase();
|
||||
const filtered = keyword ? projects.filter((project) => project.title.toLowerCase().includes(keyword)) : projects;
|
||||
const { page, pageSize, start, end } = paginate(input, filtered.length, 20);
|
||||
const items = filtered.slice(start, end).map((project) => ({
|
||||
id: project.id,
|
||||
title: project.title,
|
||||
createdAt: project.createdAt,
|
||||
updatedAt: project.updatedAt,
|
||||
nodeCount: project.nodes.length,
|
||||
connectionCount: project.connections.length,
|
||||
}));
|
||||
return { total: filtered.length, page, pageSize, items, hint: "用 site_navigate 跳转 /canvas/{id} 打开对应画布" };
|
||||
}
|
||||
|
||||
function getImageConfig() {
|
||||
const { config } = useConfigStore.getState();
|
||||
const model = config.imageModel || config.model;
|
||||
return {
|
||||
current: { model, modelName: modelOptionName(model), quality: config.quality || "auto", size: config.size || "1:1", count: config.count || "1" },
|
||||
models: config.imageModels.map((value) => ({ value, label: modelOptionLabel(config, value) })),
|
||||
qualityOptions: imageQualityOptions,
|
||||
sizeOptions: imageAspectOptions,
|
||||
countRange: { min: 1, max: 15 },
|
||||
};
|
||||
}
|
||||
|
||||
function runImageWorkbench(input: SiteToolInput, navigate: NavigateFunction) {
|
||||
const configStore = useConfigStore.getState();
|
||||
const applied: Record<string, unknown> = {};
|
||||
if (typeof input.model === "string" && input.model.trim()) {
|
||||
const value = normalizeModelOptionValue(input.model, configStore.config.channels) || input.model;
|
||||
configStore.updateConfig("imageModel", value);
|
||||
applied.model = value;
|
||||
}
|
||||
if (typeof input.quality === "string" && input.quality.trim()) {
|
||||
configStore.updateConfig("quality", input.quality);
|
||||
applied.quality = input.quality;
|
||||
}
|
||||
if (typeof input.size === "string" && input.size.trim()) {
|
||||
configStore.updateConfig("size", input.size);
|
||||
applied.size = input.size;
|
||||
}
|
||||
if (input.count != null) {
|
||||
const count = String(Math.max(1, Math.min(15, Math.floor(Number(input.count)) || 1)));
|
||||
configStore.updateConfig("count", count);
|
||||
applied.count = count;
|
||||
}
|
||||
const prompt = typeof input.prompt === "string" ? input.prompt : undefined;
|
||||
const run = input.run !== false;
|
||||
navigate("/image");
|
||||
useWorkbenchAgentStore.getState().dispatchImage({ prompt, run });
|
||||
return { ok: true, navigated: "/image", prompt, run, applied, note: run ? "已跳转生图工作台并触发生成,结果请稍后在工作台查看" : "已跳转生图工作台并填入参数,未触发生成" };
|
||||
}
|
||||
|
||||
function getVideoConfig() {
|
||||
const { config } = useConfigStore.getState();
|
||||
const model = config.videoModel || config.model;
|
||||
return {
|
||||
current: {
|
||||
model,
|
||||
modelName: modelOptionName(model),
|
||||
size: config.size || "1280x720",
|
||||
seconds: config.videoSeconds || "6",
|
||||
resolution: config.vquality || "720",
|
||||
generateAudio: config.videoGenerateAudio !== "false",
|
||||
watermark: config.videoWatermark === "true",
|
||||
},
|
||||
models: config.videoModels.map((value) => ({ value, label: modelOptionLabel(config, value) })),
|
||||
sizeOptions: videoSizeOptions,
|
||||
secondsOptions: videoSecondOptions,
|
||||
resolutionOptions: videoResolutionOptions,
|
||||
};
|
||||
}
|
||||
|
||||
function runVideoWorkbench(input: SiteToolInput, navigate: NavigateFunction) {
|
||||
const configStore = useConfigStore.getState();
|
||||
const applied: Record<string, unknown> = {};
|
||||
if (typeof input.model === "string" && input.model.trim()) {
|
||||
const value = normalizeModelOptionValue(input.model, configStore.config.channels) || input.model;
|
||||
configStore.updateConfig("videoModel", value);
|
||||
applied.model = value;
|
||||
}
|
||||
if (typeof input.size === "string" && input.size.trim()) {
|
||||
configStore.updateConfig("size", input.size);
|
||||
applied.size = input.size;
|
||||
}
|
||||
if (typeof input.seconds === "string" && input.seconds.trim()) {
|
||||
configStore.updateConfig("videoSeconds", input.seconds);
|
||||
applied.seconds = input.seconds;
|
||||
}
|
||||
if (typeof input.resolution === "string" && input.resolution.trim()) {
|
||||
configStore.updateConfig("vquality", input.resolution);
|
||||
applied.resolution = input.resolution;
|
||||
}
|
||||
if (typeof input.generateAudio === "boolean") {
|
||||
configStore.updateConfig("videoGenerateAudio", String(input.generateAudio));
|
||||
applied.generateAudio = input.generateAudio;
|
||||
}
|
||||
if (typeof input.watermark === "boolean") {
|
||||
configStore.updateConfig("videoWatermark", String(input.watermark));
|
||||
applied.watermark = input.watermark;
|
||||
}
|
||||
const prompt = typeof input.prompt === "string" ? input.prompt : undefined;
|
||||
const run = input.run !== false;
|
||||
navigate("/video");
|
||||
useWorkbenchAgentStore.getState().dispatchVideo({ prompt, run });
|
||||
return { ok: true, navigated: "/video", prompt, run, applied, note: run ? "已跳转视频创作台并触发生成,结果请稍后在工作台查看" : "已跳转视频创作台并填入参数,未触发生成" };
|
||||
}
|
||||
|
||||
async function searchPrompts(input: SiteToolInput) {
|
||||
const page = Math.max(1, Math.floor(Number(input.page)) || 1);
|
||||
const pageSize = Math.max(1, Math.min(50, Math.floor(Number(input.pageSize)) || 20));
|
||||
const tags = Array.isArray(input.tags) ? input.tags.filter((tag): tag is string => typeof tag === "string") : [];
|
||||
const result = await fetchPrompts({ keyword: String(input.keyword || ""), category: String(input.category || "全部"), tag: tags, page, pageSize });
|
||||
return {
|
||||
total: result.total,
|
||||
page,
|
||||
pageSize,
|
||||
categories: result.categories,
|
||||
tags: result.tags.slice(0, 60),
|
||||
items: result.items.map((prompt) => ({ id: prompt.id, title: prompt.title, prompt: prompt.prompt, category: prompt.category, tags: prompt.tags, coverUrl: prompt.coverUrl, githubUrl: prompt.githubUrl })),
|
||||
};
|
||||
}
|
||||
|
||||
function listAssets(input: SiteToolInput) {
|
||||
const { assets, hydrated } = useAssetStore.getState();
|
||||
if (!hydrated) throw new Error("素材还在加载中,请稍后重试");
|
||||
const kind = input.kind === "text" || input.kind === "image" || input.kind === "video" ? input.kind : "all";
|
||||
const keyword = String(input.keyword || "").trim().toLowerCase();
|
||||
const filtered = assets.filter((asset) => {
|
||||
if (kind !== "all" && asset.kind !== kind) return false;
|
||||
if (!keyword) return true;
|
||||
return [asset.title, asset.note, asset.source, ...asset.tags].filter(Boolean).join(" ").toLowerCase().includes(keyword);
|
||||
});
|
||||
const { page, pageSize, start, end } = paginate(input, filtered.length, 20);
|
||||
const items = filtered.slice(start, end).map((asset) => ({
|
||||
id: asset.id,
|
||||
kind: asset.kind,
|
||||
title: asset.title,
|
||||
tags: asset.tags,
|
||||
source: asset.source,
|
||||
note: asset.note,
|
||||
createdAt: asset.createdAt,
|
||||
updatedAt: asset.updatedAt,
|
||||
coverUrl: asset.coverUrl || undefined,
|
||||
content: asset.kind === "text" ? asset.data.content : undefined,
|
||||
}));
|
||||
return { total: filtered.length, page, pageSize, items };
|
||||
}
|
||||
|
||||
async function addAsset(input: SiteToolInput) {
|
||||
const kind = input.kind;
|
||||
const title = String(input.title || "").trim();
|
||||
if (!title) throw new Error("请提供素材标题 title");
|
||||
const tags = Array.isArray(input.tags) ? input.tags.filter((tag): tag is string => typeof tag === "string") : [];
|
||||
const source = typeof input.source === "string" ? input.source : "Agent";
|
||||
const note = typeof input.note === "string" ? input.note : undefined;
|
||||
const store = useAssetStore.getState();
|
||||
if (kind === "text") {
|
||||
const content = String(input.content || "").trim();
|
||||
if (!content) throw new Error("kind=text 时需要提供 content 文本内容");
|
||||
const id = store.addAsset({ kind: "text", title, coverUrl: "", tags, source, note, data: { content } });
|
||||
return { ok: true, id, kind: "text" };
|
||||
}
|
||||
if (kind === "image") {
|
||||
const imageUrl = String(input.imageUrl || "").trim();
|
||||
if (!imageUrl) throw new Error("kind=image 时需要提供 imageUrl(图片地址或 dataURL)");
|
||||
let stored;
|
||||
try {
|
||||
stored = await uploadImage(imageUrl);
|
||||
} catch {
|
||||
throw new Error("无法读取该图片地址,请改用 dataURL 或可跨域访问的图片链接");
|
||||
}
|
||||
const id = store.addAsset({ kind: "image", title, coverUrl: stored.url, tags, source, note, data: { dataUrl: stored.url, storageKey: stored.storageKey, width: stored.width, height: stored.height, bytes: stored.bytes, mimeType: stored.mimeType } });
|
||||
return { ok: true, id, kind: "image" };
|
||||
}
|
||||
throw new Error("assets_add 仅支持 kind=text 或 kind=image");
|
||||
}
|
||||
|
||||
function paginate(input: SiteToolInput, total: number, defaultSize: number) {
|
||||
const pageSize = Math.max(1, Math.min(100, Math.floor(Number(input.pageSize)) || defaultSize));
|
||||
const maxPage = Math.max(1, Math.ceil(total / pageSize));
|
||||
const page = Math.min(maxPage, Math.max(1, Math.floor(Number(input.page)) || 1));
|
||||
const start = (page - 1) * pageSize;
|
||||
return { page, pageSize, start, end: start + pageSize };
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { formatBytes, formatDuration, getDataUrlByteSize, readImageMeta } from "
|
||||
import { requestEdit, requestGeneration } from "@/services/api/image";
|
||||
import { deleteStoredImages, resolveImageUrl, uploadImage } from "@/services/image-storage";
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { useWorkbenchAgentStore } from "@/stores/use-workbench-agent-store";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
|
||||
type GeneratedImage = {
|
||||
@@ -88,6 +89,10 @@ export default function ImagePage() {
|
||||
const [selectedLogIds, setSelectedLogIds] = useState<string[]>([]);
|
||||
const [previewLog, setPreviewLog] = useState<GenerationLog | null>(null);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [autoRunToken, setAutoRunToken] = useState(0);
|
||||
const imageCommand = useWorkbenchAgentStore((state) => state.imageCommand);
|
||||
const clearImageCommand = useWorkbenchAgentStore((state) => state.clearImageCommand);
|
||||
const processedCommandRef = useRef(0);
|
||||
|
||||
const model = effectiveConfig.imageModel || effectiveConfig.model;
|
||||
const canGenerate = Boolean(prompt.trim());
|
||||
@@ -191,6 +196,21 @@ export default function ImagePage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 响应 Agent 面板下发的生图命令:填入提示词,并按需自动触发生成。
|
||||
useEffect(() => {
|
||||
if (!imageCommand || imageCommand.nonce === processedCommandRef.current) return;
|
||||
processedCommandRef.current = imageCommand.nonce;
|
||||
clearImageCommand();
|
||||
if (typeof imageCommand.prompt === "string") setPrompt(imageCommand.prompt);
|
||||
if (imageCommand.run && !running) setAutoRunToken((value) => value + 1);
|
||||
}, [imageCommand, clearImageCommand, running]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRunToken) return;
|
||||
void generate();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoRunToken]);
|
||||
|
||||
const downloadImage = (image: GeneratedImage, index: number) => {
|
||||
saveAs(image.dataUrl, `image-${index + 1}.png`);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { deleteStoredMedia, resolveMediaUrl, uploadMediaFile } from "@/services/
|
||||
import { resolveImageUrl, uploadImage } from "@/services/image-storage";
|
||||
import { createVideoGenerationTask, pollVideoGenerationTask, storeGeneratedVideo, type VideoGenerationTask } from "@/services/api/video";
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { useWorkbenchAgentStore } from "@/stores/use-workbench-agent-store";
|
||||
import { modelOptionLabel, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
@@ -93,6 +94,10 @@ export default function VideoPage() {
|
||||
const [selectedLogIds, setSelectedLogIds] = useState<string[]>([]);
|
||||
const [previewLog, setPreviewLog] = useState<GenerationLog | null>(null);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [autoRunToken, setAutoRunToken] = useState(0);
|
||||
const videoCommand = useWorkbenchAgentStore((state) => state.videoCommand);
|
||||
const clearVideoCommand = useWorkbenchAgentStore((state) => state.clearVideoCommand);
|
||||
const processedCommandRef = useRef(0);
|
||||
|
||||
const model = effectiveConfig.videoModel || effectiveConfig.model;
|
||||
const canGenerate = Boolean(prompt.trim());
|
||||
@@ -187,6 +192,21 @@ export default function VideoPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 响应 Agent 面板下发的视频命令:填入提示词,并按需自动触发生成。
|
||||
useEffect(() => {
|
||||
if (!videoCommand || videoCommand.nonce === processedCommandRef.current) return;
|
||||
processedCommandRef.current = videoCommand.nonce;
|
||||
clearVideoCommand();
|
||||
if (typeof videoCommand.prompt === "string") setPrompt(videoCommand.prompt);
|
||||
if (videoCommand.run && !running) setAutoRunToken((value) => value + 1);
|
||||
}, [videoCommand, clearVideoCommand, running]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRunToken) return;
|
||||
void generate();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoRunToken]);
|
||||
|
||||
const buildRequestSnapshot = () => {
|
||||
const text = prompt.trim();
|
||||
if (!text) {
|
||||
|
||||
@@ -6,7 +6,7 @@ export type AgentChatRole = "user" | "assistant" | "system" | "tool" | "error";
|
||||
export type AgentAttachment = { id: string; name: string; type: string; size: number; url: string; dataUrl: string };
|
||||
export type AgentChatItem = { id: string; role: AgentChatRole; title?: string; text: string; meta?: string; detail?: unknown; attachments?: AgentAttachment[]; streamId?: string };
|
||||
export type AgentEventLog = { id: string; time: string; title: string; text: string; raw?: unknown };
|
||||
export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[]; path?: string } };
|
||||
export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[]; path?: string } & Record<string, unknown> };
|
||||
export type AgentCanvasContext = { snapshot: CanvasAgentSnapshot; applyOps: (ops?: CanvasAgentOp[]) => CanvasAgentSnapshot; undoOps: () => CanvasAgentSnapshot | null; canUndo: boolean };
|
||||
export type AgentThreadSummary = { id: string; preview: string; name?: string | null; cwd?: string; status?: string; source?: unknown; createdAt?: number; updatedAt?: number };
|
||||
export type AgentPanelTab = "chat" | "setup" | "history" | "log";
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
// Agent 面板通过该 store 向生图/视频工作台派发命令:设置提示词、并可选自动点击生成。
|
||||
// 参数(模型/质量/尺寸/张数等)由 Agent 面板直接写入 use-config-store,工作台页面从 config 读取;
|
||||
// prompt 与 run 通过这里下发,页面用 nonce 判断是否为新命令,消费后调用 clear 清空。
|
||||
|
||||
export type WorkbenchCommand = {
|
||||
nonce: number;
|
||||
prompt?: string;
|
||||
run: boolean;
|
||||
};
|
||||
|
||||
type WorkbenchAgentStore = {
|
||||
imageCommand: WorkbenchCommand | null;
|
||||
videoCommand: WorkbenchCommand | null;
|
||||
dispatchImage: (command: Omit<WorkbenchCommand, "nonce">) => void;
|
||||
dispatchVideo: (command: Omit<WorkbenchCommand, "nonce">) => void;
|
||||
clearImageCommand: () => void;
|
||||
clearVideoCommand: () => void;
|
||||
};
|
||||
|
||||
let nonce = 0;
|
||||
const nextNonce = () => (nonce += 1);
|
||||
|
||||
export const useWorkbenchAgentStore = create<WorkbenchAgentStore>((set) => ({
|
||||
imageCommand: null,
|
||||
videoCommand: null,
|
||||
dispatchImage: (command) => set({ imageCommand: { ...command, nonce: nextNonce() } }),
|
||||
dispatchVideo: (command) => set({ videoCommand: { ...command, nonce: nextNonce() } }),
|
||||
clearImageCommand: () => set({ imageCommand: null }),
|
||||
clearVideoCommand: () => set({ videoCommand: null }),
|
||||
}));
|
||||
Reference in New Issue
Block a user