Files
MiragenFlow/web/src/lib/agent/agent-site-tools.ts
T

257 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, selectableModelsByCapability, 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: selectableModelsByCapability(config, "image").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: selectableModelsByCapability(config, "video").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 };
}