Merge branch 'develop' of https://github.com/HBAI-Ltd/Toonflow-app into develop

This commit is contained in:
zhishi 2026-04-28 14:17:09 +08:00
commit 8197cfcdec
18 changed files with 1050 additions and 1405 deletions

View File

@ -183,15 +183,14 @@ jobs:
| 操作系统 | 架构 | 文件 | 说明 |
|---------|------|------|------|
| 🪟 Windows | 通用 | `ToonFlow-*-win-setup.exe` | **推荐**,适用于大多数 Windows 电脑 |
| 🪟 Windows | x64 | `ToonFlow-*-win-x64-setup.exe` | 适用于 64 位 Windows 电脑 |
| 🪟 Windows | x64 | `ToonFlow-*-win-x64-setup.exe` | **推荐**,适用于大多数 Windows 电脑 |
| 🪟 Windows | ARM64 | `ToonFlow-*-win-arm64-setup.exe` | 适用于 ARM 架构 Windows 设备 |
| 🍎 macOS | Apple Silicon | `ToonFlow-*-mac-arm64.dmg` | 适用于 M1/M2/M3/M4 芯片的 Mac |
| 🍎 macOS | Intel | `ToonFlow-*-mac-x64.dmg` | 适用于 Intel 芯片的 Mac |
| 🐧 Linux | x86_64 | `ToonFlow-*-linux-x86_64.AppImage` | 适用于大多数 Linux 发行版 |
| 🐧 Linux | x64 | `ToonFlow-*-linux-x86_64.AppImage` | 适用于大多数 Linux 发行版 |
| 🐧 Linux | ARM64 | `ToonFlow-*-linux-arm64.AppImage` | 适用于 ARM 架构的 Linux 设备 |
> 💡 **不确定选哪个?** Windows 用户通常选 **win-setup.exe**Mac 用户查看「关于本机」M 系列芯片选 **arm64.dmg**Intel 选 **x64.dmg**。
> 💡 **不确定选哪个?** Windows 用户通常选 **win-x64-setup.exe**Mac 用户查看「关于本机」M 系列芯片选 **arm64.dmg**Intel 选 **x64.dmg**。
## 🚀 安装说明

View File

@ -465,7 +465,7 @@ const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<str
const isStartEndRequired = currentMode.includes("startEndRequired");
const isEndFrameOptional = currentMode.includes("endFrameOptional");
const isStartFrameOptional = currentMode.includes("startFrameOptional");
const hasMultiRef = currentMode.some((m) => Array.isArray(m));
const hasMultiRef = Array.isArray(currentMode) && currentMode.some((m) => Array.isArray(m));
// 提取不同类型的引用
const imageRefs = (config.referenceList || []).filter((r) => r.type === "image");
@ -635,4 +635,4 @@ exports.videoRequest = videoRequest;
exports.ttsRequest = ttsRequest;
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
export {};
export {};

427
data/vendor/yunwu.ts vendored
View File

@ -1,427 +0,0 @@
// ==================== 类型定义 ====================
// 文本模型
interface TextModel {
name: string;
modelName: string;
type: "text";
think: boolean;
}
// 图像模型
interface ImageModel {
name: string;
modelName: string;
type: "image";
mode: ("text" | "singleImage" | "multiReference")[];
associationSkills?: string;
}
// 视频模型
interface VideoModel {
name: string;
modelName: string;
type: "video";
mode: (
| "singleImage"
| "startEndRequired"
| "endFrameOptional"
| "startFrameOptional"
| "text"
| ("videoReference" | "imageReference" | "audioReference" | "textReference")[]
)[];
associationSkills?: string;
audio: "optional" | false | true;
durationResolutionMap: { duration: number[]; resolution: string[] }[];
}
interface TTSModel {
name: string;
modelName: string;
type: "tts";
voices: { title: string; voice: string }[];
}
interface VendorConfig {
id: string;
author: string;
description?: string;
name: string;
icon?: string;
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
inputValues: Record<string, string>;
models: (TextModel | ImageModel | VideoModel)[];
}
// ==================== 全局工具函数声明 ====================
declare const zipImage: (completeBase64: string, size: number) => Promise<string>;
declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise<string>;
declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise<string>;
declare const urlToBase64: (url: string) => Promise<string>;
declare const pollTask: (
fn: () => Promise<{ completed: boolean; data?: string; error?: string }>,
interval?: number,
timeout?: number,
) => Promise<{ completed: boolean; data?: string; error?: string }>;
declare const axios: any;
declare const createOpenAI: any;
declare const createDeepSeek: any;
declare const createZhipu: any;
declare const createQwen: any;
declare const createAnthropic: any;
declare const createOpenAICompatible: any;
declare const createXai: any;
declare const createMinimax: any;
declare const createGoogleGenerativeAI: any;
declare const logger: (logstring: string) => void;
declare const jsonwebtoken: any;
// ==================== 供应商数据 ====================
const vendor: VendorConfig = {
id: "yunwu",
author: "Toonflow",
description: "OpenAI标准格式接口您可以修改请求地址并手动添加缺失的模型。",
name: "云雾中转",
icon: "",
inputs: [
{ key: "apiKey", label: "API密钥", type: "password", required: true },
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "以v1结束示例https://yunwu.ai/v1" },
],
inputValues: {
apiKey: "",
baseUrl: "https://yunwu.ai/v1",
},
models: [
{
name: "Doubao-Seedream-5.0-lite",
type: "image",
modelName: "doubao-seedream-5-0-260128",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Gemini-3-Pro-Image-Preview",
type: "image",
modelName: "gemini-3.1-flash-image-preview",
mode: ["text", "singleImage", "multiReference"],
associationSkills: "高质量图像生成,支持文本生成图像、图像编辑",
},
{
name: "Claude-sonnet-4.6",
type: "text",
modelName: "claude-sonnet-4-6",
think: false,
},
{
name: "Claude-haiku-4.5-20251001",
type: "text",
modelName: "claude-haiku-4-5-20251001",
think: false,
},
{
name: "Grok-Video-3",
type: "video",
modelName: "grok-video-3",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [
{ duration: [6, 10], resolution: ["720P", "1080P"] }
],
associationSkills: "文本生成视频,支持图片垫图"
}
],
};
exports.vendor = vendor;
// ==================== 适配器函数 ====================
// 文本请求函数
const textRequest = (textModel: TextModel) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
if (!vendor.inputValues.baseUrl) throw new Error("缺少请求地址(baseUrl)");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
const baseURL = vendor.inputValues.baseUrl;
return createOpenAI({
baseURL: baseURL,
apiKey: apiKey,
}).chat(textModel.modelName);
};
exports.textRequest = textRequest;
// 图片请求函数(修正版:使用 /v1/chat/completions 兼容接口)
interface ImageConfig {
prompt: string;
imageBase64: string[];
size: "1K" | "2K" | "4K";
aspectRatio: `${number}:${number}`;
}
const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {
const { apiKey, baseUrl } = vendor.inputValues;
if (!apiKey) throw new Error("缺少API Key");
if (!baseUrl) throw new Error("缺少请求地址(baseUrl)");
const cleanApiKey = apiKey.replace(/^Bearer\s+/i, "");
const baseURL = baseUrl.replace(/\/$/, "");
const endpoint = baseURL + "/chat/completions";
// 构建用户消息内容(支持多图垫图)
const content: any[] = [
{
type: "text",
text: imageConfig.prompt,
},
];
// 添加参考图片(垫图)
if (imageConfig.imageBase64 && imageConfig.imageBase64.length > 0) {
for (const imgBase64 of imageConfig.imageBase64) {
let dataUrl = imgBase64;
if (!imgBase64.startsWith("data:image")) {
dataUrl = `data:image/png;base64,${imgBase64}`;
}
content.push({
type: "image_url",
image_url: { url: dataUrl },
});
}
}
// 注意:云雾中转站可能支持通过额外参数传递图像尺寸/比例,
// 若不确定,可将 size 和 aspectRatio 拼接到 prompt 中(推荐)。
// 这里采用追加提示词的方式,确保模型理解期望的分辨率和比例。
let finalPrompt = imageConfig.prompt;
const sizeMap: Record<string, string> = { "1K": "1024x1024", "2K": "2048x2048", "4K": "4096x4096" };
const resolution = sizeMap[imageConfig.size] || "1024x1024";
finalPrompt += `\n请生成一张比例为 ${imageConfig.aspectRatio}、分辨率不低于 ${resolution} 的图片。`;
content[0].text = finalPrompt;
const requestBody = {
model: imageModel.modelName,
messages: [
{
role: "user",
content: content,
},
],
max_tokens: 4096, // 防止输出截断
response_format: { type: "json_object" }, // 部分中转站需要 JSON 输出
};
logger(`[图像生成] 请求URL: ${endpoint}`);
logger(`[图像生成] 模型: ${imageModel.modelName}`);
logger(`[图像生成] 参考图片数量: ${imageConfig.imageBase64?.length || 0}`);
try {
const response = await axios.post(endpoint, requestBody, {
headers: {
"Authorization": `Bearer ${cleanApiKey}`,
"Content-Type": "application/json",
},
timeout: 120000,
});
if (response.status !== 200) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(response.data)}`);
}
const assistantMessage = response.data?.choices?.[0]?.message?.content;
if (!assistantMessage) {
throw new Error("响应中没有 assistant 消息内容");
}
// 提取图像数据(支持直接返回 base64 data URL 或普通 URL
let imageBase64: string | null = null;
// 情况1消息内容本身就是 data:image 开头
if (assistantMessage.startsWith("data:image")) {
imageBase64 = assistantMessage;
}
// 情况2包含 Markdown 图片链接 ![alt](url)
else {
const markdownMatch = assistantMessage.match(/!\[.*?\]\((.*?)\)/);
if (markdownMatch && markdownMatch[1]) {
const url = markdownMatch[1];
if (url.startsWith("data:image")) {
imageBase64 = url;
} else {
imageBase64 = await urlToBase64(url);
}
}
// 情况3直接是纯文本 URL
else if (assistantMessage.match(/^https?:\/\/[^\s]+\.(png|jpg|jpeg|gif|webp)/i)) {
imageBase64 = await urlToBase64(assistantMessage);
}
}
if (!imageBase64) {
// 最后尝试:也许整个 content 就是 base64 字符串(无前缀)
if (/^[A-Za-z0-9+/=]+$/.test(assistantMessage) && assistantMessage.length > 100) {
imageBase64 = `data:image/png;base64,${assistantMessage}`;
} else {
throw new Error(`无法从响应中提取图像数据: ${assistantMessage.substring(0, 200)}`);
}
}
logger(`[图像生成] 成功,图片大小: ${(imageBase64.length / 1024).toFixed(2)} KB`);
return imageBase64;
} catch (error: any) {
logger(`[图像生成] 失败: ${error.message}`);
if (error.response) {
logger(`[图像生成] API 错误详情: ${JSON.stringify(error.response.data)}`);
throw new Error(`图像生成失败: ${error.response.data?.error?.message || error.message}`);
}
throw error;
}
};
exports.imageRequest = imageRequest;
// 视频请求函数(保持原有实现,若云雾中转站有专用视频接口可按需修改)
interface VideoConfig {
duration: number;
resolution: string;
aspectRatio: "16:9" | "9:16";
prompt: string;
imageBase64?: string[];
audio?: boolean;
mode:
| "singleImage"
| "multiImage"
| "gridImage"
| "startEndRequired"
| "endFrameOptional"
| "startFrameOptional"
| "text"
| ("video" | "image" | "audio" | "text")[];
}
const videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {
const { apiKey, baseUrl: rawBaseUrl } = vendor.inputValues;
if (!apiKey) throw new Error("缺少API Key");
const baseUrl = rawBaseUrl?.trim();
if (!baseUrl) throw new Error("缺少请求地址(baseUrl)");
const createEndpoint = baseUrl.replace(/\/$/, "") + "/video/create";
const queryEndpoint = baseUrl.replace(/\/$/, "") + "/video/query";
let images: string[] | undefined;
if (videoConfig.imageBase64 && videoConfig.imageBase64.length > 0) {
logger(`[视频生成] 原始图片数组: ${JSON.stringify(videoConfig.imageBase64)}`);
images = videoConfig.imageBase64
.filter(img => img && typeof img === 'string' && img.length > 0)
.map(img => {
if (img.startsWith("data:image")) return img;
return `data:image/png;base64,${img}`;
});
if (images.length === 0) {
logger(`[视频生成] 警告: 所有图片都无效,将忽略图片参数`);
images = undefined;
} else {
logger(`[视频生成] 有效图片数量: ${images.length}`);
}
}
let aspectRatioParam: string;
switch (videoConfig.aspectRatio) {
case "16:9": aspectRatioParam = "3:2"; break;
case "9:16": aspectRatioParam = "2:3"; break;
default: aspectRatioParam = "1:1";
}
let sizeParam: string = "720P";
if (videoConfig.resolution && videoConfig.resolution.includes("1080")) sizeParam = "1080P";
const createBody: any = {
model: videoModel.modelName,
prompt: videoConfig.prompt,
aspect_ratio: aspectRatioParam,
size: sizeParam,
};
if (images && images.length > 0) createBody.images = images;
try {
logger(`[视频生成] 创建请求体: ${JSON.stringify({ ...createBody, images: images ? `${images.length}张图片` : undefined })}`);
const createResp = await axios.post(createEndpoint, createBody, {
headers: {
"Authorization": `Bearer ${apiKey.replace(/^Bearer\s+/i, "")}`,
"Content-Type": "application/json",
},
timeout: 30000,
});
if (createResp.status !== 200 || !createResp.data?.id) {
throw new Error(`创建任务失败: ${JSON.stringify(createResp.data)}`);
}
const taskId = createResp.data.id;
logger(`[视频生成] 任务已创建ID: ${taskId}`);
const pollResult = await pollTask(
async () => {
try {
const queryResp = await axios.get(queryEndpoint, {
params: { id: taskId },
headers: {
"Authorization": `Bearer ${apiKey.replace(/^Bearer\s+/i, "")}`,
"Content-Type": "application/json",
},
timeout: 15000,
});
if (queryResp.status !== 200) {
return { completed: false, error: `查询失败: HTTP ${queryResp.status}` };
}
const data = queryResp.data;
const status = data.status;
logger(`[视频生成] 任务状态: ${status}`);
if (status === "succeeded" || status === "completed" || status === "success") {
if (data.video_url) {
return { completed: true, data: data.video_url };
} else {
return { completed: false, error: "任务成功但未返回视频URL" };
}
} else if (status === "failed" || status === "error") {
return { completed: false, error: `视频生成失败: ${data.error || "未知错误"}` };
} else {
return { completed: false };
}
} catch (err: any) {
logger(`[视频生成] 轮询出错: ${err.message}`);
return { completed: false, error: err.message };
}
},
3000,
300000
);
if (!pollResult.completed) {
throw new Error(pollResult.error || "视频生成超时或失败");
}
const videoUrl = pollResult.data;
logger(`[视频生成] 成功视频URL: ${videoUrl}`);
return videoUrl;
} catch (error: any) {
logger(`[视频生成] 失败: ${error.message}`);
if (error.response) {
logger(`[视频生成] API 错误详情: ${JSON.stringify(error.response.data)}`);
throw new Error(`视频生成失败: ${error.response.data?.error?.message || error.message}`);
}
throw error;
}
};
exports.videoRequest = videoRequest;
// TTS 请求函数(占位)
interface TTSConfig {
text: string;
voice: string;
speechRate: number;
pitchRate: number;
volume: number;
}
const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {
return null;
};
exports.ttsRequest = ttsRequest;

View File

@ -1 +1 @@
1.1.5
1.1.6

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "toonflow",
"version": "1.1.5",
"version": "1.1.6",
"description": "Toonflow 是一款 AI 短剧漫剧工具,能够利用 AI 技术将小说自动转化为剧本,并结合 AI 生成的图片和视频,实现高效的短剧创作。",
"author": "HBAI-Ltd <ltlctools@outlook.com>",
"license": "Apache-2.0",

View File

@ -1,6 +1,6 @@
import { Socket } from "socket.io";
import { tool } from "ai";
import { z } from "zod";
import { tool, jsonSchema } from "ai";
import u from "@/utils";
import Memory from "@/utils/agent/memory";
import { createSkillTools, parseFrontmatter, scanSkills, useSkill } from "@/utils/agent/skillsTools";
@ -138,9 +138,11 @@ async function createSubAgent(parentCtx: AgentContext) {
return fullResponse;
}
const promptInput = z.object({
prompt: z.string().describe("交给子Agent的任务简约描述100字以内"),
});
const promptInput = z
.object({
prompt: z.string().describe("交给子Agent的任务简约描述100字以内"),
})
.toJSONSchema();
const projectInfo = await u.db("o_project").where("id", resTool.data.projectId).first();
if (!projectInfo) throw new Error(`项目不存在ID: ${resTool.data.projectId}`);
@ -195,7 +197,7 @@ async function createSubAgent(parentCtx: AgentContext) {
//衍生资产分析与信息写入
const run_sub_agent_derive_assets = tool({
description: "运行执行subAgent来完成衍生资产分析与信息写入相关任务",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "production_execution_derive_assets.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
@ -217,7 +219,7 @@ async function createSubAgent(parentCtx: AgentContext) {
//衍生资产图片生成
const run_sub_agent_generate_assets = tool({
description: "运行执行subAgent来完成衍生资产图片生成相关任务",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "production_execution_generate_assets.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
@ -239,7 +241,7 @@ async function createSubAgent(parentCtx: AgentContext) {
//拍摄计划
const run_sub_agent_director_plan = tool({
description: "运行执行subAgent来完成导演规划相关任务",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "production_execution_director_plan.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
@ -264,7 +266,7 @@ async function createSubAgent(parentCtx: AgentContext) {
//分镜图生成
const run_sub_agent_storyboard_gen = tool({
description: "运行执行subAgent来完成分镜图生成相关任务",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "production_execution_storyboard_gen.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
@ -298,7 +300,7 @@ async function createSubAgent(parentCtx: AgentContext) {
//分镜面板写入
const run_sub_agent_storyboard_panel = tool({
description: "运行执行subAgent来完成分镜面板写入相关任务",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "production_execution_storyboard_panel.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
@ -324,7 +326,7 @@ async function createSubAgent(parentCtx: AgentContext) {
//分镜表写入
const run_sub_agent_storyboard_table = tool({
description: "运行执行subAgent来完成分镜表构建相关任务",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "production_execution_storyboard_table.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
@ -348,7 +350,7 @@ async function createSubAgent(parentCtx: AgentContext) {
const run_sub_agent_supervision = tool({
description: "运行监督层subAgent执行独立任务完成后返回结果",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "production_agent_supervision.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");

View File

@ -1,4 +1,4 @@
import { tool, Tool } from "ai";
import { tool, jsonSchema, Tool } from "ai";
import { z } from "zod";
import _ from "lodash";
import ResTool from "@/socket/resTool";
@ -69,9 +69,13 @@ export default (toolCpnfig: ToolConfig) => {
const tools: Record<string, Tool> = {
get_flowData: tool({
description: "获取工作区数据",
inputSchema: z.object({
key: keySchema.describe("数据key"),
}),
inputSchema: jsonSchema<{ key: keyof FlowData }>(
z
.object({
key: keySchema.describe("数据key"),
})
.toJSONSchema(),
),
execute: async ({ key }) => {
const thinking = msg.thinking(`正在获取${flowDataKeyLabels[key]}工作区数据...`);
console.log("[tools] get_flowData", key);
@ -84,18 +88,22 @@ export default (toolCpnfig: ToolConfig) => {
}),
add_deriveAsset: tool({
description: "新增或更新衍生资产",
inputSchema: z.object({
assetsId: z.number().describe("关联的资产ID"),
id: z.preprocess(
(val) => {
if (val === "null" || val === "" || val === undefined) return null;
return val;
},
z.number().nullable().describe("衍生资产ID,如果新增则为空")),
name: z.string().describe("衍生资产名称"),
desc: z.string().describe("衍生资产描述"),
}),
execute: async (deriveAsset) => {
inputSchema: jsonSchema<{ assetsId: number; id: number | null; name: string; desc: string }>(
z
.object({
assetsId: z.number().describe("关联的资产ID"),
id: z.number().nullable().describe("衍生资产ID,如果新增则为空"),
name: z.string().describe("衍生资产名称"),
desc: z.string().describe("衍生资产描述"),
})
.toJSONSchema(),
),
execute: async (raw) => {
// 容错LLM 偶尔传 "null" 字符串或空串,统一规范为 null
const idRaw = raw.id as unknown;
const normalizedId = idRaw === "null" || idRaw === "" || idRaw === undefined ? null : (idRaw as number | null);
const deriveAsset = { ...raw, id: normalizedId };
const thinking = msg.thinking("正在操作资产...");
const { projectId, scriptId } = resTool.data;
const startTime = Date.now();
@ -128,10 +136,14 @@ export default (toolCpnfig: ToolConfig) => {
}),
del_deriveAsset: tool({
description: "删除衍生资产",
inputSchema: z.object({
assetsId: z.number().describe("关联的资产ID"),
id: z.number().describe("衍生资产ID"),
}),
inputSchema: jsonSchema<{ assetsId: number; id: number }>(
z
.object({
assetsId: z.number().describe("关联的资产ID"),
id: z.number().describe("衍生资产ID"),
})
.toJSONSchema(),
),
execute: async ({ assetsId, id }) => {
const thinking = msg.thinking("正在操作资产...");
const { scriptId } = resTool.data;
@ -146,9 +158,13 @@ export default (toolCpnfig: ToolConfig) => {
}),
generate_deriveAsset: tool({
description: "生成衍生资产图片",
inputSchema: z.object({
ids: z.array(z.number()).describe("需要生成的 衍生资产ID"),
}),
inputSchema: jsonSchema<{ ids: number[] }>(
z
.object({
ids: z.array(z.number()).describe("需要生成的 衍生资产ID"),
})
.toJSONSchema(),
),
execute: async ({ ids }) => {
const thinking = msg.thinking("正在生成衍生资产...");
new Promise((resolve) => socket.emit("generateDeriveAsset", { ids }, (res: any) => resolve(res)))
@ -168,9 +184,13 @@ export default (toolCpnfig: ToolConfig) => {
}),
generate_storyboard: tool({
description: "生成分镜图片",
inputSchema: z.object({
ids: z.array(z.number()).describe("必须获取真实的分镜ID支持批量生成"),
}),
inputSchema: jsonSchema<{ ids: number[] }>(
z
.object({
ids: z.array(z.number()).describe("必须获取真实的分镜ID支持批量生成"),
})
.toJSONSchema(),
),
execute: async ({ ids }) => {
const thinking = msg.thinking("正在生成分镜...");
new Promise((resolve) => socket.emit("generateStoryboard", { ids }, (res: any) => resolve(res)))

View File

@ -1,5 +1,5 @@
import { Socket } from "socket.io";
import { tool } from "ai";
import { tool, jsonSchema } from "ai";
import { z } from "zod";
import u from "@/utils";
import Memory from "@/utils/agent/memory";
@ -133,13 +133,15 @@ function createSubAgent(parentCtx: AgentContext) {
return fullResponse;
}
const promptInput = z.object({
prompt: z.string().describe("交给子Agent的任务简约描述100字以内"),
});
const promptInput = z
.object({
prompt: z.string().describe("交给子Agent的任务简约描述100字以内"),
})
.toJSONSchema();
const run_sub_agent_storySkeleton = tool({
description: "运行执行subAgent来完成故事骨架相关任务",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "script_execution_skeleton.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
@ -159,7 +161,7 @@ function createSubAgent(parentCtx: AgentContext) {
const run_sub_agent_adaptationStrategy = tool({
description: "运行执行subAgent来完成改编策略相关任务",
inputSchema: promptInput,
inputSchema: jsonSchema<{ prompt: string }>(promptInput),
execute: async ({ prompt }) => {
const skill = path.join(u.getPath("skills"), "script_execution_adaptation.md");
const systemPrompt = await fs.promises.readFile(skill, "utf-8");

View File

@ -1,4 +1,4 @@
import { tool, Tool } from "ai";
import { tool, jsonSchema, Tool } from "ai";
import u from "@/utils";
import { z } from "zod";
import _ from "lodash";
@ -33,9 +33,13 @@ export default (toolCpnfig: ToolConfig) => {
const tools: Record<string, Tool> = {
get_novel_events: tool({
description: "获取章节事件",
inputSchema: z.object({
chapterIndexs: z.array(z.number()).describe("章节的编号"),
}),
inputSchema: jsonSchema<{ chapterIndexs: number[] }>(
z
.object({
chapterIndexs: z.array(z.number()).describe("章节的编号"),
})
.toJSONSchema(),
),
execute: async ({ chapterIndexs }) => {
console.log("[tools] get_novel_events", chapterIndexs);
const thinking = msg.thinking("正在查询章节事件...");
@ -45,7 +49,7 @@ export default (toolCpnfig: ToolConfig) => {
.select("id", "chapterIndex as index", "reel", "chapter", "chapterData", "event", "eventState")
.whereIn("chapterIndex", chapterIndexs);
thinking.appendText("正在查询章节编号: " + chapterIndexs.join(","));
const eventString = data.map((i: any) => [`${i.index}章,标题${i.chapter},事件:${i.event}`].join("\n")).join("\n");
const eventString = data.map((i: any) => [`${i.index}章,标题:${i.chapter},事件:${i.event}`].join("\n")).join("\n");
thinking.appendText("查询结果:\n" + eventString);
thinking.updateTitle("查询章节事件完成");
thinking.complete();
@ -54,9 +58,13 @@ export default (toolCpnfig: ToolConfig) => {
}),
get_planData: tool({
description: "获取工作区数据",
inputSchema: z.object({
key: keySchema.describe("数据key"),
}),
inputSchema: jsonSchema<{ key: keyof planData }>(
z
.object({
key: keySchema.describe("数据key"),
})
.toJSONSchema(),
),
execute: async ({ key }) => {
console.log("[tools] get_planData", key);
const thinking = msg.thinking(`正在获取${planDataKeyLabels[key]}工作区数据...`);
@ -69,9 +77,13 @@ export default (toolCpnfig: ToolConfig) => {
}),
get_novel_text: tool({
description: "获取小说章节原始文本内容",
inputSchema: z.object({
chapterIndex: z.string().describe("章节编号"),
}),
inputSchema: jsonSchema<{ chapterIndex: string }>(
z
.object({
chapterIndex: z.string().describe("章节编号"),
})
.toJSONSchema(),
),
execute: async ({ chapterIndex }) => {
console.log("[tools] get_novel_text", "[tools] get_novel_text", chapterIndex);
const thinking = msg.thinking(`正在获取小说章节原文...`);
@ -85,9 +97,13 @@ export default (toolCpnfig: ToolConfig) => {
}),
get_script_content: tool({
description: "获取剧本本内容",
inputSchema: z.object({
ids: z.array(z.string()).describe("脚本id"),
}),
inputSchema: jsonSchema<{ ids: string[] }>(
z
.object({
ids: z.array(z.string()).describe("脚本id"),
})
.toJSONSchema(),
),
execute: async ({ ids }) => {
console.log("[tools] get_script_content", "[tools] get_script_content", ids);
const thinking = msg.thinking(`正在获取脚本内容...`);

View File

@ -3,7 +3,7 @@ import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { tool } from "ai";
import { tool, jsonSchema } from "ai";
const router = express.Router();
// 获取资产
@ -22,26 +22,37 @@ export default router.post(
.whereIn("id", assetsIds)
.andWhere("projectId", projectId)
.select("id", "name", "describe");
console.log("%c Line:20 🍎 assetsData", "background:#b03734", assetsData);
const audioData = await u.db("o_assets").where("type", "audio").whereNull("assetsId").andWhere("projectId", projectId).select("id", "name", "describe");
console.log("%c Line:26 🍋 audioData", "background:#ea7e5c", audioData);
const audioData = await u
.db("o_assets")
.where("type", "audio")
.whereNull("assetsId")
.andWhere("projectId", projectId)
.select("id", "name", "describe");
async function processGroup() {
try {
const resultTool = tool({
description: "返回结果时必须调用这个工具",
inputSchema: z.object({
result: z.array(z.object({
id: z.number(),
audioIds: z.array(z.number()).describe("适配的音频id 无适配内容可以为 空数组")
})).describe("适配的音色列表id为资产idaudioIds为适配的音频id 无适配内容可以为 空数组")
}),
inputSchema: jsonSchema<{ result: { id: number; audioIds: number[] }[] }>(
z
.object({
result: z
.array(
z.object({
id: z.number(),
audioIds: z.array(z.number()).describe("适配的音频id 无适配内容可以为 空数组"),
}),
)
.describe("适配的音色列表id为资产idaudioIds为适配的音频id 无适配内容可以为 空数组"),
})
.toJSONSchema(),
),
execute: async ({ result }) => {
console.log("[tools] extractAssets result", result);
for (const item of result) {
await u.db("o_assetsRole2Audio").where("assetsRoleId", item.id).delete()
await u.db("o_assetsRole2Audio").where("assetsRoleId", item.id).delete();
if (item.audioIds.length)
await u.db("o_assetsRole2Audio").insert(item.audioIds.map(i => ({ assetsRoleId: item.id, assetsAudioId: i })))
await u.db("o_assetsRole2Audio").insert(item.audioIds.map((i) => ({ assetsRoleId: item.id, assetsAudioId: i })));
}
return "无需回复用户任何内容";
},
@ -55,21 +66,20 @@ export default router.post(
},
{
role: "user",
content: `音频内容:${audioData.map(i => `Id:${i.id},音色名称:${i.name},描述:${i.describe}`).join("\n")}\n\n
${assetsData.map(i => `ID:${i.id},名称:${i.name},描述:${i.describe}`).join("\n")}\n\n
content: `音频内容:${audioData.map((i) => `Id:${i.id},音色名称:${i.name},描述:${i.describe}`).join("\n")}\n\n
${assetsData.map((i) => `ID:${i.id},名称:${i.name},描述:${i.describe}`).join("\n")}\n\n
`,
},
],
tools: { resultTool },
});
console.log("%c Line:44 🍞 text", "background:#f5ce50", text);
} catch (e) {
console.error(`提取失败:`, e);
return;
}
}
await processGroup()
await processGroup();
res.status(200).send(success());
},
);

View File

@ -4,7 +4,7 @@ import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { useSkill } from "@/utils/agent/skillsTools";
import { tool } from "ai";
import { tool, jsonSchema } from "ai";
import { o_script } from "@/types/database";
const router = express.Router();
@ -131,10 +131,7 @@ export default router.post(
}
// 去重:相同 scriptId + assetId 只保留一条
const uniqueRows = [
...new Map(scriptAssetRows.map((r) => [`${r.scriptId}_${r.assetId}`, r])).values(),
];
const uniqueRows = [...new Map(scriptAssetRows.map((r) => [`${r.scriptId}_${r.assetId}`, r])).values()];
// 先删除本批 scriptId 的旧关联,再插入新的
await u.db("o_scriptAssets").whereIn("scriptId", batchScriptIds).delete();
@ -188,16 +185,19 @@ export default router.post(
try {
const resultTool = tool({
description: "返回结果时必须调用这个工具",
inputSchema: z.object({
newAssets: z
.array(NewAssetSchema)
.describe("新发现的资产列表(不在已有资产列表中的),需要完整的 prompt、name、desc、type 和使用该资产的 scriptIds"),
existingAssetRefs: z
.array(ExistingAssetRefSchema)
.describe("已有资产的引用列表(在已有资产列表中已存在的),只需给出资产名称和使用该资产的 scriptIds"),
}),
inputSchema: jsonSchema<{ newAssets: NewAsset[]; existingAssetRefs: ExistingAssetRef[] }>(
z
.object({
newAssets: z
.array(NewAssetSchema)
.describe("新发现的资产列表(不在已有资产列表中的),需要完整的 prompt、name、desc、type 和使用该资产的 scriptIds"),
existingAssetRefs: z
.array(ExistingAssetRefSchema)
.describe("已有资产的引用列表(在已有资产列表中已存在的),只需给出资产名称和使用该资产的 scriptIds"),
})
.toJSONSchema(),
),
execute: async ({ newAssets, existingAssetRefs }) => {
if (newAssets?.length) collectedNew = newAssets;
if (existingAssetRefs?.length) collectedExisting = existingAssetRefs;
return "无需回复用户任何内容";

View File

@ -6,23 +6,29 @@ const router = express.Router();
export default router.post("/", async (req, res) => {
const data = await u.db("o_vendorConfig").select("*");
const list = await Promise.all(
data.map(async (item) => {
const vendor = u.vendor.getVendor(item.id!);
return {
...item,
inputValues: JSON.parse(item.inputValues ?? "{}"),
models: await u.vendor.getModelList(item.id!),
code: u.vendor.getCode(item.id!),
description: vendor.description,
inputs: vendor.inputs,
author: vendor.author,
name: vendor.name,
version: vendor.version ?? "1.0",
};
}),
);
const list = (
await Promise.all(
data.map(async (item) => {
const vendor = u.vendor.getVendor(item.id!);
if (!vendor) {
await u.db("o_vendorConfig").where("id", item.id).delete();
return null
};
return {
...item,
inputValues: JSON.parse(item.inputValues ?? "{}"),
models: await u.vendor.getModelList(item.id!),
code: u.vendor.getCode(item.id!),
description: vendor.description ?? "",
inputs: vendor.inputs,
author: vendor.author,
name: vendor.name,
version: vendor.version ?? "1.0",
};
}),
)
).filter((i) => Boolean(i));
list.sort((a, b) => (a.id === "toonflow" ? -1 : b.id === "toonflow" ? 1 : 0));
list.sort((a, b) => (a!.id === "toonflow" ? -1 : b!.id === "toonflow" ? 1 : 0));
res.status(200).send(success(list));
});

View File

@ -3,7 +3,7 @@ import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
import { tool } from "ai";
import { tool, jsonSchema } from "ai";
const router = express.Router();
// 检查语言模型
@ -57,9 +57,13 @@ export default router.post(
const getWeatherTool = tool({
description: "Get the weather in a location",
inputSchema: z.object({
location: z.string().describe("The location to get the weather for"),
}),
inputSchema: jsonSchema<{ location: string }>(
z
.object({
location: z.string().describe("The location to get the weather for"),
})
.toJSONSchema(),
),
execute: async ({ location }) => {
return {
location,

View File

@ -1,4 +1,4 @@
// @db-hash 88c167ba73e2771e7b043419ca5089dd
// @db-hash 5364c2db0bf42b520761b813ce040489
//该文件由脚本自动生成,请勿手动修改
export interface memories {
@ -23,6 +23,7 @@ export interface o_agentDeploy {
'modelName'?: string | null;
'name'?: string | null;
'temperature'?: number | null;
'topP'?: number | null;
'type'?: string | null;
'vendorId'?: string | null;
}
@ -210,7 +211,6 @@ export interface o_user {
'password'?: string | null;
}
export interface o_vendorConfig {
'code'?: string | null;
'enable'?: number | null;
'id'?: string;
'inputValues'?: string | null;

View File

@ -2,7 +2,7 @@ import u from "@/utils";
import { v4 as uuidv4 } from "uuid";
import { getEmbedding, cosineSimilarity } from "./embedding";
import type { memories as MemoryRow } from "@/types/database";
import { tool } from "ai";
import { tool, jsonSchema } from "ai";
import { z } from "zod";
// ── 可调配置默认值 ──
@ -201,9 +201,13 @@ class Memory {
return {
deepRetrieve: tool({
description: "深度检索记忆:当你需要回忆与某个关键词相关的详细历史信息时使用此工具",
inputSchema: z.object({
keyword: z.string().describe("要检索的关键词"),
}),
inputSchema: jsonSchema<{ keyword: string }>(
z
.object({
keyword: z.string().describe("要检索的关键词"),
})
.toJSONSchema(),
),
execute: async ({ keyword }) => {
const results = await this.deepRetrieve(keyword);
if (results.length === 0) return { found: false, message: "未找到相关记忆" };

View File

@ -1,5 +1,5 @@
import { tool } from "ai";
import { z } from "zod";
import { tool, jsonSchema } from "ai";
import path from "path";
import isPathInside from "is-path-inside";
import getPath from "@/utils/getPath";
@ -185,9 +185,13 @@ export function createSkillTools(skills: { name: string; description: string }[]
return {
activate_skill: tool({
description: `激活一个技能,加载其完整指令和捆绑资源列表到上下文。可用技能:${skillNames.join(", ")}`,
inputSchema: z.object({
name: z.enum(skillNames as [string, ...string[]]).describe("要激活的技能名称"),
}),
inputSchema: jsonSchema<{ name: string }>(
z
.object({
name: z.enum(skillNames as [string, ...string[]]).describe("要激活的技能名称"),
})
.toJSONSchema(),
),
execute: async ({ name }) => {
if (activated.has(name)) {
console.log(`⚡[主技能] 技能 "${name}" 已激活,跳过重复注入`);
@ -222,9 +226,13 @@ export function createSkillTools(skills: { name: string; description: string }[]
}),
read_skill_file: tool({
description: "读取已激活技能目录下的资源文件。传入 activate_skill 返回的 skill_resources 中的文件路径。",
inputSchema: z.object({
filePath: z.string().describe("资源文件的相对路径,来自 activate_skill 返回的 skill_resources"),
}),
inputSchema: jsonSchema<{ filePath: string }>(
z
.object({
filePath: z.string().describe("资源文件的相对路径,来自 activate_skill 返回的 skill_resources"),
})
.toJSONSchema(),
),
execute: async ({ filePath }) => {
const normalizedInputPath = toUnixPath(filePath).trim();
if (!normalizedInputPath) {

View File

@ -25,6 +25,7 @@ export async function getModelList(id: string): Promise<Array<any>> {
const code = getCode(id);
const jsCode = transform(code, { transforms: ["typescript"] }).code;
const vendorData = u.vm(jsCode);
if(!vendorData || !vendorData.vendor || !vendorData.vendor.models) return [];
const combined = [...JSON.parse(JSON.stringify(vendorData.vendor.models)), ...JSON.parse(models?.models ?? "[]")];
const map = new Map<string, any>();
for (const m of combined) {