refactor(agent): 将 tools 模块拆分为独立文件并优化导入路径
将 `tools.ts` 拆分为按功能划分的独立文件,并存放于 `tools/` 目录下,同时更新导入路径;优化 agent 系统提示语,移除冗余的「美图 Agent」前缀。
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import Anthropic from '@anthropic-ai/sdk';
|
import Anthropic from '@anthropic-ai/sdk';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { tools, ToolDefinition } from './tools';
|
import { tools, ToolDefinition } from './tools/index';
|
||||||
import { getDb } from '../db';
|
import { getDb } from '../db';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -116,7 +116,7 @@ export class VideoAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `你是美图 Agent,一个专业的短视频创作助手。你可以帮助用户完成从创意到成片的完整流程。
|
return `你是专业的短视频创作助手。你可以帮助用户完成从创意到成片的完整流程。
|
||||||
|
|
||||||
## 当前可用账号
|
## 当前可用账号
|
||||||
${accountList}
|
${accountList}
|
||||||
|
|||||||
@@ -1,438 +0,0 @@
|
|||||||
import { spawn, execSync } from 'child_process';
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
|
||||||
import os from 'os';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
||||||
const PIPELINE_SCRIPT = path.join(PROJECT_ROOT, '.claude', 'skills', 'video-from-script', 'scripts', 'pipeline.js');
|
|
||||||
const ACCOUNTS_DIR = path.join(PROJECT_ROOT, 'accounts');
|
|
||||||
const OUTPUT_DIR = path.join(PROJECT_ROOT, 'output');
|
|
||||||
|
|
||||||
export interface ToolDefinition {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
input_schema: {
|
|
||||||
type: 'object';
|
|
||||||
properties: Record<string, { type: string; description: string }>;
|
|
||||||
required?: string[];
|
|
||||||
};
|
|
||||||
execute: (params: Record<string, unknown>) => Promise<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helpers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function loadJSON(filePath: string): unknown {
|
|
||||||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Write items JSON to a temp file and invoke pipeline init, returning the manifest path */
|
|
||||||
function runInit(params: {
|
|
||||||
account: string;
|
|
||||||
mode: string;
|
|
||||||
items: unknown[];
|
|
||||||
imageModel?: string;
|
|
||||||
videoModel?: string;
|
|
||||||
format?: string;
|
|
||||||
}): string {
|
|
||||||
// Write items to a temp file to avoid shell escaping issues with JSON
|
|
||||||
const tmpFile = path.join(os.tmpdir(), `pipeline-items-${Date.now()}.json`);
|
|
||||||
fs.writeFileSync(tmpFile, JSON.stringify(params.items), 'utf-8');
|
|
||||||
try {
|
|
||||||
const args = [
|
|
||||||
`"${PIPELINE_SCRIPT}"`, 'init',
|
|
||||||
`--account "${params.account}"`,
|
|
||||||
`--mode ${params.mode}`,
|
|
||||||
`--items-file "${tmpFile}"`,
|
|
||||||
params.imageModel ? `--image-model ${params.imageModel}` : '',
|
|
||||||
params.videoModel ? `--video-model ${params.videoModel}` : '',
|
|
||||||
params.format ? `--format ${params.format}` : '',
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
const output = execSync(`node ${args}`, { cwd: PROJECT_ROOT, encoding: 'utf-8' });
|
|
||||||
// Parse manifest path from init output: "Manifest 已创建: <path>"
|
|
||||||
const match = output.match(/Manifest 已创建: (.+)/);
|
|
||||||
if (!match) throw new Error(`Failed to parse manifest path from init output: ${output}`);
|
|
||||||
return match[1].trim();
|
|
||||||
} finally {
|
|
||||||
// Clean up temp file (pipeline init also deletes it, but be safe)
|
|
||||||
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tool Registry
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export const tools: ToolDefinition[] = [
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Existing tools (kept exactly as before)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
{
|
|
||||||
name: 'list_accounts',
|
|
||||||
description: '列出所有可用账号,返回每个账号的名称、描述、生图模型和视频模型',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {},
|
|
||||||
required: [],
|
|
||||||
},
|
|
||||||
execute: async () => {
|
|
||||||
if (!fs.existsSync(ACCOUNTS_DIR)) return '暂无账号';
|
|
||||||
const dirs = fs.readdirSync(ACCOUNTS_DIR, { withFileTypes: true })
|
|
||||||
.filter((d) => d.isDirectory() && !d.name.startsWith('_') && !d.name.startsWith('.'))
|
|
||||||
.map((d) => {
|
|
||||||
const configPath = path.join(ACCOUNTS_DIR, d.name, 'account.json');
|
|
||||||
if (fs.existsSync(configPath)) {
|
|
||||||
const cfg = loadJSON(configPath) as Record<string, string>;
|
|
||||||
return `${d.name} - ${cfg.description || '无描述'} (生图:${cfg.imageModel} 视频:${cfg.videoModel} 画幅:${cfg.defaultFormat})`;
|
|
||||||
}
|
|
||||||
return d.name;
|
|
||||||
});
|
|
||||||
return dirs.join('\n') || '暂无账号';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create_account',
|
|
||||||
description: '创建新的短视频账号。需要提供账号ID、名称和描述。创建后可在 accounts/ 目录下找到配置。',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string', description: '账号唯一标识,英文小写,如 military-account' },
|
|
||||||
name: { type: 'string', description: '账号显示名称,中文,如 军事账号' },
|
|
||||||
desc: { type: 'string', description: '账号描述,说明视频风格和主题' },
|
|
||||||
imageModel: { type: 'string', description: '生图模型: gemini, mj, gpt, kling' },
|
|
||||||
videoModel: { type: 'string', description: '视频模型: veo3-fast, veo3-fast-frames, kling, grok' },
|
|
||||||
format: { type: 'string', description: '画幅: 9:16 (竖屏), 16:9 (横屏), 1:1 (方形)' },
|
|
||||||
},
|
|
||||||
required: ['id', 'name'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { id, name, desc, imageModel, videoModel, format } = params as Record<string, string>;
|
|
||||||
const cmd = [
|
|
||||||
`node "${PIPELINE_SCRIPT}" create-account`,
|
|
||||||
`--id "${id}"`,
|
|
||||||
`--name "${name}"`,
|
|
||||||
`--desc "${desc || ''}"`,
|
|
||||||
`--video-model ${videoModel || 'veo3-fast'}`,
|
|
||||||
imageModel ? `--image-model ${imageModel}` : '',
|
|
||||||
format ? `--format ${format}` : '',
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
const result = execSync(cmd, { cwd: PROJECT_ROOT, encoding: 'utf-8' });
|
|
||||||
return `账号「${name}」创建成功。\n${result}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'pipeline_status',
|
|
||||||
description: '查看指定 manifest 的 pipeline 执行进度和各阶段状态',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
manifest: { type: 'string', description: 'manifest.json 的绝对路径' },
|
|
||||||
},
|
|
||||||
required: ['manifest'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { manifest } = params as { manifest: string };
|
|
||||||
const result = execSync(`node "${PIPELINE_SCRIPT}" status --manifest "${manifest}"`, {
|
|
||||||
cwd: PROJECT_ROOT, encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'run_pipeline_phase',
|
|
||||||
description: '执行视频创作 pipeline 的指定阶段。阶段顺序: images(生图) → upload(上传) → videos(生视频) → tts(配音) → assemble(成片组装)。执行前需确认 manifest.json 已存在。',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
manifest: { type: 'string', description: 'manifest.json 的绝对路径' },
|
|
||||||
phase: { type: 'string', description: '要执行的阶段: images, upload, videos, tts, assemble。多个阶段用逗号分隔如 images,upload' },
|
|
||||||
},
|
|
||||||
required: ['manifest', 'phase'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { manifest, phase } = params as { manifest: string; phase: string };
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const proc = spawn('node', [PIPELINE_SCRIPT, 'run', '--manifest', manifest, '--phase', phase], {
|
|
||||||
cwd: PROJECT_ROOT,
|
|
||||||
env: { ...process.env },
|
|
||||||
});
|
|
||||||
let output = '';
|
|
||||||
proc.stdout.on('data', (d: Buffer) => { output += d.toString(); });
|
|
||||||
proc.stderr.on('data', (d: Buffer) => { output += d.toString(); });
|
|
||||||
proc.on('close', (code) => {
|
|
||||||
code === 0 ? resolve(output || '执行成功') : reject(new Error(`Pipeline exit code ${code}: ${output.slice(-500)}`));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'get_account_config',
|
|
||||||
description: '获取指定账号的完整配置,包括模型选择、TTS语音、字幕风格等',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
accountId: { type: 'string', description: '账号ID,如 军事账号' },
|
|
||||||
},
|
|
||||||
required: ['accountId'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { accountId } = params as { accountId: string };
|
|
||||||
const configPath = path.join(ACCOUNTS_DIR, accountId, 'account.json');
|
|
||||||
if (!fs.existsSync(configPath)) return `账号「${accountId}」不存在`;
|
|
||||||
const cfg = loadJSON(configPath);
|
|
||||||
return JSON.stringify(cfg, null, 2);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// New tools: image/video generation, manifest management
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
{
|
|
||||||
name: 'generate_images',
|
|
||||||
description: '文生图:根据文本提示词生成图片。内部创建临时 manifest 并调用 pipeline images 阶段,支持批量生成。生成结果写入 output 目录,返回图片文件路径列表。',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
accountId: { type: 'string', description: '账号ID,用于继承模型、参考图等配置' },
|
|
||||||
prompt: { type: 'string', description: '图片提示词(imagePrompt),描述要生成的图片内容' },
|
|
||||||
count: { type: 'number', description: '生成图片数量,默认 1' },
|
|
||||||
imageModel: { type: 'string', description: '生图模型(可选,默认继承账号配置): gemini, mj, gpt-image, kling' },
|
|
||||||
},
|
|
||||||
required: ['accountId', 'prompt'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { accountId, prompt, count, imageModel } = params as Record<string, unknown>;
|
|
||||||
const num = Math.max(1, Number(count) || 1);
|
|
||||||
// Build items: each item needs shotDesc + imagePrompt for the images phase
|
|
||||||
const items = Array.from({ length: num }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
shotDesc: prompt as string,
|
|
||||||
script: '',
|
|
||||||
imagePrompt: prompt as string,
|
|
||||||
keyword: 'generated',
|
|
||||||
}));
|
|
||||||
// Init manifest (items passed as array, helper writes temp file)
|
|
||||||
const manifestPath = runInit({
|
|
||||||
account: accountId as string,
|
|
||||||
mode: 'single',
|
|
||||||
items,
|
|
||||||
imageModel: imageModel as string | undefined,
|
|
||||||
});
|
|
||||||
// Run images phase
|
|
||||||
execSync(`node "${PIPELINE_SCRIPT}" run --manifest "${manifestPath}" --phase images`, {
|
|
||||||
cwd: PROJECT_ROOT, encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
// Read back results
|
|
||||||
const manifest = loadJSON(manifestPath) as { items?: Array<{ id: number; file?: string; candidates?: string[]; status?: string }> };
|
|
||||||
const results = (manifest.items || []).map((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
file: item.file || null,
|
|
||||||
candidates: item.candidates || [],
|
|
||||||
status: item.status,
|
|
||||||
}));
|
|
||||||
return JSON.stringify({ manifestPath, images: results }, null, 2);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'generate_videos',
|
|
||||||
description: '图生视频:根据已有图片和提示词生成 AI 视频。内部创建临时 manifest,先上传图片再执行 videos 阶段。返回视频文件路径。',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
accountId: { type: 'string', description: '账号ID,用于继承视频模型等配置' },
|
|
||||||
imagePath: { type: 'string', description: '图片文件路径(本地绝对路径或相对 PROJECT_ROOT 的路径)' },
|
|
||||||
prompt: { type: 'string', description: '视频提示词(videoPrompt),描述图片如何动起来' },
|
|
||||||
videoModel: { type: 'string', description: '视频模型(可选,默认继承账号配置): veo3-fast, veo3-fast-frames, kling, grok' },
|
|
||||||
imagePrompt: { type: 'string', description: '图片提示词(可选),用于 manifest 记录' },
|
|
||||||
},
|
|
||||||
required: ['accountId', 'imagePath', 'prompt'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { accountId, imagePath, prompt, videoModel, imagePrompt } = params as Record<string, string>;
|
|
||||||
const resolvedImagePath = path.isAbsolute(imagePath)
|
|
||||||
? imagePath
|
|
||||||
: path.resolve(PROJECT_ROOT, imagePath);
|
|
||||||
if (!fs.existsSync(resolvedImagePath)) {
|
|
||||||
return `错误: 图片文件不存在: ${resolvedImagePath}`;
|
|
||||||
}
|
|
||||||
// Determine relative path from output dir
|
|
||||||
const baseName = path.basename(resolvedImagePath);
|
|
||||||
const item = {
|
|
||||||
id: 1,
|
|
||||||
shotDesc: imagePrompt || prompt,
|
|
||||||
script: '',
|
|
||||||
imagePrompt: imagePrompt || prompt,
|
|
||||||
videoPrompt: prompt,
|
|
||||||
keyword: 'generated',
|
|
||||||
file: `images/${baseName}`,
|
|
||||||
};
|
|
||||||
// Init manifest (items passed as array, helper writes temp file)
|
|
||||||
const manifestPath = runInit({
|
|
||||||
account: accountId,
|
|
||||||
mode: 'single',
|
|
||||||
items: [item],
|
|
||||||
videoModel: videoModel,
|
|
||||||
});
|
|
||||||
// Copy image into manifest's images dir
|
|
||||||
const manifestDir = path.dirname(manifestPath);
|
|
||||||
const imagesDir = path.join(manifestDir, 'images');
|
|
||||||
if (!fs.existsSync(imagesDir)) fs.mkdirSync(imagesDir, { recursive: true });
|
|
||||||
const targetPath = path.join(imagesDir, baseName);
|
|
||||||
if (resolvedImagePath !== targetPath) {
|
|
||||||
fs.copyFileSync(resolvedImagePath, targetPath);
|
|
||||||
}
|
|
||||||
// Run upload + videos phases
|
|
||||||
execSync(`node "${PIPELINE_SCRIPT}" run --manifest "${manifestPath}" --phase upload,videos`, {
|
|
||||||
cwd: PROJECT_ROOT, encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
// Read back results
|
|
||||||
const manifest = loadJSON(manifestPath) as {
|
|
||||||
items?: Array<{ id: number; video?: string; videoUrl?: string; videoDuration?: number; status?: string }>;
|
|
||||||
};
|
|
||||||
const results = (manifest.items || []).map((it) => ({
|
|
||||||
id: it.id,
|
|
||||||
video: it.video || null,
|
|
||||||
videoUrl: it.videoUrl || null,
|
|
||||||
videoDuration: it.videoDuration || null,
|
|
||||||
status: it.status,
|
|
||||||
}));
|
|
||||||
return JSON.stringify({ manifestPath, videos: results }, null, 2);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'get_account_prompts',
|
|
||||||
description: '获取指定账号的三个提示词模板内容:分镜提示词、图片提示词、视频提示词。这些模板定义了账号的视频风格和创意方向。',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
accountId: { type: 'string', description: '账号ID' },
|
|
||||||
},
|
|
||||||
required: ['accountId'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { accountId } = params as { accountId: string };
|
|
||||||
const accountDir = path.join(ACCOUNTS_DIR, accountId);
|
|
||||||
if (!fs.existsSync(accountDir)) return `账号「${accountId}」不存在`;
|
|
||||||
const promptsDir = path.join(accountDir, 'prompts');
|
|
||||||
const promptFiles = ['分镜.md', '图片提示词.md', '视频提示词.md'];
|
|
||||||
const result: Record<string, string> = {};
|
|
||||||
for (const file of promptFiles) {
|
|
||||||
const filePath = path.join(promptsDir, file);
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
result[file] = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
} else {
|
|
||||||
result[file] = `(文件不存在: ${filePath})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return JSON.stringify(result, null, 2);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'list_outputs',
|
|
||||||
description: '列出 output 目录下所有项目及其 manifest 状态,方便查看历史生成记录和进行中的任务。',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {},
|
|
||||||
required: [],
|
|
||||||
},
|
|
||||||
execute: async () => {
|
|
||||||
if (!fs.existsSync(OUTPUT_DIR)) return 'output 目录不存在';
|
|
||||||
const entries = fs.readdirSync(OUTPUT_DIR, { withFileTypes: true })
|
|
||||||
.filter((d) => d.isDirectory())
|
|
||||||
.map((d) => {
|
|
||||||
const manifestPath = path.join(OUTPUT_DIR, d.name, 'manifest.json');
|
|
||||||
if (!fs.existsSync(manifestPath)) return { name: d.name, status: 'no manifest' };
|
|
||||||
try {
|
|
||||||
const manifest = loadJSON(manifestPath) as {
|
|
||||||
account?: string;
|
|
||||||
imageModel?: string;
|
|
||||||
videoModel?: string;
|
|
||||||
mode?: string;
|
|
||||||
pipeline?: { phases?: Record<string, string> };
|
|
||||||
items?: Array<{ status?: string }>;
|
|
||||||
};
|
|
||||||
const phases = manifest.pipeline?.phases || {};
|
|
||||||
const itemCount = manifest.items?.length || 0;
|
|
||||||
const doneCount = manifest.items?.filter((i) => i.status === 'done').length || 0;
|
|
||||||
return {
|
|
||||||
name: d.name,
|
|
||||||
account: manifest.account || '?',
|
|
||||||
mode: manifest.mode || '?',
|
|
||||||
items: `${doneCount}/${itemCount} done`,
|
|
||||||
phases: Object.entries(phases).map(([k, v]) => `${k}:${v}`).join(', ') || 'pending',
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return { name: d.name, status: 'manifest parse error' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return JSON.stringify(entries, null, 2);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'get_manifest',
|
|
||||||
description: '读取指定 manifest.json 的完整内容,返回 JSON 字符串。',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
manifestPath: { type: 'string', description: 'manifest.json 的绝对路径或相对路径' },
|
|
||||||
},
|
|
||||||
required: ['manifestPath'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { manifestPath } = params as { manifestPath: string };
|
|
||||||
const resolved = path.isAbsolute(manifestPath)
|
|
||||||
? manifestPath
|
|
||||||
: path.resolve(PROJECT_ROOT, manifestPath);
|
|
||||||
if (!fs.existsSync(resolved)) return `manifest 不存在: ${resolved}`;
|
|
||||||
const manifest = loadJSON(resolved);
|
|
||||||
return JSON.stringify(manifest, null, 2);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create_manifest',
|
|
||||||
description: '初始化新的 manifest.json,调用 pipeline.js init 创建输出目录和项目骨架。这是视频创作 pipeline 的起点。',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
accountId: { type: 'string', description: '账号ID,决定模型、参考图等配置来源' },
|
|
||||||
mode: { type: 'string', description: '创作模式: single (单图模式) 或 framePair (首尾帧模式)' },
|
|
||||||
items: { type: 'string', description: '分镜数据 JSON 数组字符串。每个元素需包含 shotDesc (分镜描述) 和 script (旁白文案),可选 imagePrompt、videoPrompt、directorRef、keyword、duration' },
|
|
||||||
imageModel: { type: 'string', description: '生图模型(可选,默认继承账号配置): gemini, mj, gpt-image, kling' },
|
|
||||||
videoModel: { type: 'string', description: '视频模型(可选,默认继承账号配置): veo3-fast, veo3-fast-frames, kling, grok' },
|
|
||||||
format: { type: 'string', description: '画幅(可选,默认继承账号配置): 9:16, 16:9, 1:1' },
|
|
||||||
},
|
|
||||||
required: ['accountId', 'mode', 'items'],
|
|
||||||
},
|
|
||||||
execute: async (params) => {
|
|
||||||
const { accountId, mode, items, imageModel, videoModel, format } = params as Record<string, string>;
|
|
||||||
// Validate items JSON
|
|
||||||
let parsed: unknown;
|
|
||||||
try { parsed = JSON.parse(items); } catch { return '错误: items 不是合法的 JSON 字符串'; }
|
|
||||||
if (!Array.isArray(parsed) || (parsed as unknown[]).length === 0) return '错误: items 必须是非空 JSON 数组';
|
|
||||||
// Write items to temp file to avoid shell escaping issues
|
|
||||||
const tmpFile = path.join(os.tmpdir(), `pipeline-items-${Date.now()}.json`);
|
|
||||||
fs.writeFileSync(tmpFile, items, 'utf-8');
|
|
||||||
try {
|
|
||||||
const args = [
|
|
||||||
`"${PIPELINE_SCRIPT}"`, 'init',
|
|
||||||
`--account "${accountId}"`,
|
|
||||||
`--mode ${mode}`,
|
|
||||||
`--items-file "${tmpFile}"`,
|
|
||||||
imageModel ? `--image-model ${imageModel}` : '',
|
|
||||||
videoModel ? `--video-model ${videoModel}` : '',
|
|
||||||
format ? `--format ${format}` : '',
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
const output = execSync(`node ${args}`, { cwd: PROJECT_ROOT, encoding: 'utf-8' });
|
|
||||||
const match = output.match(/Manifest 已创建: (.+)/);
|
|
||||||
const manifestPath = match ? match[1].trim() : '(unable to parse path)';
|
|
||||||
return `Manifest 已创建: ${manifestPath}\n\n${output}`;
|
|
||||||
} finally {
|
|
||||||
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
34
web/server/agent/tools/create-account.ts
Normal file
34
web/server/agent/tools/create-account.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { PIPELINE_SCRIPT, PROJECT_ROOT } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const createAccount: ToolDefinition = {
|
||||||
|
name: 'create_account',
|
||||||
|
description: '创建新的短视频账号。需要提供账号ID、名称和描述。创建后可在 accounts/ 目录下找到配置。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: '账号唯一标识,英文小写,如 military-account' },
|
||||||
|
name: { type: 'string', description: '账号显示名称,中文,如 军事账号' },
|
||||||
|
desc: { type: 'string', description: '账号描述,说明视频风格和主题' },
|
||||||
|
imageModel: { type: 'string', description: '生图模型: gemini, mj, gpt, kling' },
|
||||||
|
videoModel: { type: 'string', description: '视频模型: veo3-fast, veo3-fast-frames, kling, grok' },
|
||||||
|
format: { type: 'string', description: '画幅: 9:16 (竖屏), 16:9 (横屏), 1:1 (方形)' },
|
||||||
|
},
|
||||||
|
required: ['id', 'name'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { id, name, desc, imageModel, videoModel, format } = params as Record<string, string>;
|
||||||
|
const cmd = [
|
||||||
|
`node "${PIPELINE_SCRIPT}" create-account`,
|
||||||
|
`--id "${id}"`,
|
||||||
|
`--name "${name}"`,
|
||||||
|
`--desc "${desc || ''}"`,
|
||||||
|
`--video-model ${videoModel || 'veo3-fast'}`,
|
||||||
|
imageModel ? `--image-model ${imageModel}` : '',
|
||||||
|
format ? `--format ${format}` : '',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
const result = execSync(cmd, { cwd: PROJECT_ROOT, encoding: 'utf-8' });
|
||||||
|
return `账号「${name}」创建成功。\n${result}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
50
web/server/agent/tools/create-manifest.ts
Normal file
50
web/server/agent/tools/create-manifest.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { PIPELINE_SCRIPT, PROJECT_ROOT } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const createManifest: ToolDefinition = {
|
||||||
|
name: 'create_manifest',
|
||||||
|
description: '初始化新的 manifest.json,调用 pipeline.js init 创建输出目录和项目骨架。这是视频创作 pipeline 的起点。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
accountId: { type: 'string', description: '账号ID,决定模型、参考图等配置来源' },
|
||||||
|
mode: { type: 'string', description: '创作模式: single (单图模式) 或 framePair (首尾帧模式)' },
|
||||||
|
items: { type: 'string', description: '分镜数据 JSON 数组字符串。每个元素需包含 shotDesc (分镜描述) 和 script (旁白文案),可选 imagePrompt、videoPrompt、directorRef、keyword、duration' },
|
||||||
|
imageModel: { type: 'string', description: '生图模型(可选,默认继承账号配置): gemini, mj, gpt-image, kling' },
|
||||||
|
videoModel: { type: 'string', description: '视频模型(可选,默认继承账号配置): veo3-fast, veo3-fast-frames, kling, grok' },
|
||||||
|
format: { type: 'string', description: '画幅(可选,默认继承账号配置): 9:16, 16:9, 1:1' },
|
||||||
|
},
|
||||||
|
required: ['accountId', 'mode', 'items'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { accountId, mode, items, imageModel, videoModel, format } = params as Record<string, string>;
|
||||||
|
// Validate items JSON
|
||||||
|
let parsed: unknown;
|
||||||
|
try { parsed = JSON.parse(items); } catch { return '错误: items 不是合法的 JSON 字符串'; }
|
||||||
|
if (!Array.isArray(parsed) || (parsed as unknown[]).length === 0) return '错误: items 必须是非空 JSON 数组';
|
||||||
|
// Write items to temp file to avoid shell escaping issues
|
||||||
|
const tmpFile = path.join(os.tmpdir(), `pipeline-items-${Date.now()}.json`);
|
||||||
|
fs.writeFileSync(tmpFile, items, 'utf-8');
|
||||||
|
try {
|
||||||
|
const args = [
|
||||||
|
`"${PIPELINE_SCRIPT}"`, 'init',
|
||||||
|
`--account "${accountId}"`,
|
||||||
|
`--mode ${mode}`,
|
||||||
|
`--items-file "${tmpFile}"`,
|
||||||
|
imageModel ? `--image-model ${imageModel}` : '',
|
||||||
|
videoModel ? `--video-model ${videoModel}` : '',
|
||||||
|
format ? `--format ${format}` : '',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
const output = execSync(`node ${args}`, { cwd: PROJECT_ROOT, encoding: 'utf-8' });
|
||||||
|
const match = output.match(/Manifest 已创建: (.+)/);
|
||||||
|
const manifestPath = match ? match[1].trim() : '(unable to parse path)';
|
||||||
|
return `Manifest 已创建: ${manifestPath}\n\n${output}`;
|
||||||
|
} finally {
|
||||||
|
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
46
web/server/agent/tools/generate-images.ts
Normal file
46
web/server/agent/tools/generate-images.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { runInit, loadJSON, PIPELINE_SCRIPT, PROJECT_ROOT } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const generateImages: ToolDefinition = {
|
||||||
|
name: 'generate_images',
|
||||||
|
description: '文生图:根据文本提示词生成图片。内部创建临时 manifest 并调用 pipeline images 阶段,支持批量生成。生成结果写入 output 目录,返回图片文件路径列表。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
accountId: { type: 'string', description: '账号ID,用于继承模型、参考图等配置' },
|
||||||
|
prompt: { type: 'string', description: '图片提示词(imagePrompt),描述要生成的图片内容' },
|
||||||
|
count: { type: 'number', description: '生成图片数量,默认 1' },
|
||||||
|
imageModel: { type: 'string', description: '生图模型(可选,默认继承账号配置): gemini, mj, gpt-image, kling' },
|
||||||
|
},
|
||||||
|
required: ['accountId', 'prompt'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { accountId, prompt, count, imageModel } = params as Record<string, unknown>;
|
||||||
|
const num = Math.max(1, Number(count) || 1);
|
||||||
|
const items = Array.from({ length: num }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
shotDesc: prompt as string,
|
||||||
|
script: '',
|
||||||
|
imagePrompt: prompt as string,
|
||||||
|
keyword: 'generated',
|
||||||
|
}));
|
||||||
|
const manifestPath = runInit({
|
||||||
|
account: accountId as string,
|
||||||
|
mode: 'single',
|
||||||
|
items,
|
||||||
|
imageModel: imageModel as string | undefined,
|
||||||
|
});
|
||||||
|
execSync(`node "${PIPELINE_SCRIPT}" run --manifest "${manifestPath}" --phase images`, {
|
||||||
|
cwd: PROJECT_ROOT, encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
const manifest = loadJSON(manifestPath) as { items?: Array<{ id: number; file?: string; candidates?: string[]; status?: string }> };
|
||||||
|
const results = (manifest.items || []).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
file: item.file || null,
|
||||||
|
candidates: item.candidates || [],
|
||||||
|
status: item.status,
|
||||||
|
}));
|
||||||
|
return JSON.stringify({ manifestPath, images: results }, null, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
69
web/server/agent/tools/generate-videos.ts
Normal file
69
web/server/agent/tools/generate-videos.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { runInit, loadJSON, PIPELINE_SCRIPT, PROJECT_ROOT } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const generateVideos: ToolDefinition = {
|
||||||
|
name: 'generate_videos',
|
||||||
|
description: '图生视频:根据已有图片和提示词生成 AI 视频。内部创建临时 manifest,先上传图片再执行 videos 阶段。返回视频文件路径。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
accountId: { type: 'string', description: '账号ID,用于继承视频模型等配置' },
|
||||||
|
imagePath: { type: 'string', description: '图片文件路径(本地绝对路径或相对 PROJECTROOT 的路径)' },
|
||||||
|
prompt: { type: 'string', description: '视频提示词(videoPrompt),描述图片如何动起来' },
|
||||||
|
videoModel: { type: 'string', description: '视频模型(可选,默认继承账号配置): veo3-fast, veo3-fast-frames, kling, grok' },
|
||||||
|
imagePrompt: { type: 'string', description: '图片提示词(可选),用于 manifest 记录' },
|
||||||
|
},
|
||||||
|
required: ['accountId', 'imagePath', 'prompt'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { accountId, imagePath, prompt, videoModel, imagePrompt } = params as Record<string, string>;
|
||||||
|
const resolvedImagePath = path.isAbsolute(imagePath)
|
||||||
|
? imagePath
|
||||||
|
: path.resolve(PROJECT_ROOT, imagePath);
|
||||||
|
if (!fs.existsSync(resolvedImagePath)) {
|
||||||
|
return `错误: 图片文件不存在: ${resolvedImagePath}`;
|
||||||
|
}
|
||||||
|
const baseName = path.basename(resolvedImagePath);
|
||||||
|
const item = {
|
||||||
|
id: 1,
|
||||||
|
shotDesc: imagePrompt || prompt,
|
||||||
|
script: '',
|
||||||
|
imagePrompt: imagePrompt || prompt,
|
||||||
|
videoPrompt: prompt,
|
||||||
|
keyword: 'generated',
|
||||||
|
file: `images/${baseName}`,
|
||||||
|
};
|
||||||
|
const manifestPath = runInit({
|
||||||
|
account: accountId,
|
||||||
|
mode: 'single',
|
||||||
|
items: [item],
|
||||||
|
videoModel: videoModel,
|
||||||
|
});
|
||||||
|
// Copy image into manifest's images dir
|
||||||
|
const manifestDir = path.dirname(manifestPath);
|
||||||
|
const imagesDir = path.join(manifestDir, 'images');
|
||||||
|
if (!fs.existsSync(imagesDir)) fs.mkdirSync(imagesDir, { recursive: true });
|
||||||
|
const targetPath = path.join(imagesDir, baseName);
|
||||||
|
if (resolvedImagePath !== targetPath) {
|
||||||
|
fs.copyFileSync(resolvedImagePath, targetPath);
|
||||||
|
}
|
||||||
|
// Run upload + videos phases
|
||||||
|
execSync(`node "${PIPELINE_SCRIPT}" run --manifest "${manifestPath}" --phase upload,videos`, {
|
||||||
|
cwd: PROJECT_ROOT, encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
const manifest = loadJSON(manifestPath) as {
|
||||||
|
items?: Array<{ id: number; video?: string; videoUrl?: string; videoDuration?: number; status?: string }>;
|
||||||
|
};
|
||||||
|
const results = (manifest.items || []).map((it) => ({
|
||||||
|
id: it.id,
|
||||||
|
video: it.video || null,
|
||||||
|
videoUrl: it.videoUrl || null,
|
||||||
|
videoDuration: it.videoDuration || null,
|
||||||
|
status: it.status,
|
||||||
|
}));
|
||||||
|
return JSON.stringify({ manifestPath, videos: results }, null, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
23
web/server/agent/tools/get-account-config.ts
Normal file
23
web/server/agent/tools/get-account-config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { ACCOUNTS_DIR, loadJSON } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const getAccountConfig: ToolDefinition = {
|
||||||
|
name: 'get_account_config',
|
||||||
|
description: '获取指定账号的完整配置,包括模型选择、TTS语音、字幕风格等',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
accountId: { type: 'string', description: '账号ID,如 军事账号' },
|
||||||
|
},
|
||||||
|
required: ['accountId'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { accountId } = params as { accountId: string };
|
||||||
|
const configPath = path.join(ACCOUNTS_DIR, accountId, 'account.json');
|
||||||
|
if (!fs.existsSync(configPath)) return `账号「${accountId}」不存在`;
|
||||||
|
const cfg = loadJSON(configPath);
|
||||||
|
return JSON.stringify(cfg, null, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
33
web/server/agent/tools/get-account-prompts.ts
Normal file
33
web/server/agent/tools/get-account-prompts.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { ACCOUNTS_DIR } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const getAccountPrompts: ToolDefinition = {
|
||||||
|
name: 'get_account_prompts',
|
||||||
|
description: '获取指定账号的三个提示词模板内容:分镜提示词、图片提示词、视频提示词。这些模板定义了账号的视频风格和创意方向。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
accountId: { type: 'string', description: '账号ID' },
|
||||||
|
},
|
||||||
|
required: ['accountId'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { accountId } = params as { accountId: string };
|
||||||
|
const accountDir = path.join(ACCOUNTS_DIR, accountId);
|
||||||
|
if (!fs.existsSync(accountDir)) return `账号「${accountId}」不存在`;
|
||||||
|
const promptsDir = path.join(accountDir, 'prompts');
|
||||||
|
const promptFiles = ['分镜.md', '图片提示词.md', '视频提示词.md'];
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const file of promptFiles) {
|
||||||
|
const filePath = path.join(promptsDir, file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
result[file] = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
} else {
|
||||||
|
result[file] = `(文件不存在: ${filePath})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.stringify(result, null, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
25
web/server/agent/tools/get-manifest.ts
Normal file
25
web/server/agent/tools/get-manifest.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { PROJECT_ROOT, loadJSON } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const getManifest: ToolDefinition = {
|
||||||
|
name: 'get_manifest',
|
||||||
|
description: '读取指定 manifest.json 的完整内容,返回 JSON 字符串。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
manifestPath: { type: 'string', description: 'manifest.json 的绝对路径或相对路径' },
|
||||||
|
},
|
||||||
|
required: ['manifestPath'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { manifestPath } = params as { manifestPath: string };
|
||||||
|
const resolved = path.isAbsolute(manifestPath)
|
||||||
|
? manifestPath
|
||||||
|
: path.resolve(PROJECT_ROOT, manifestPath);
|
||||||
|
if (!fs.existsSync(resolved)) return `manifest 不存在: ${resolved}`;
|
||||||
|
const manifest = loadJSON(resolved);
|
||||||
|
return JSON.stringify(manifest, null, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
28
web/server/agent/tools/index.ts
Normal file
28
web/server/agent/tools/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
export type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
import { listAccounts } from './list-accounts';
|
||||||
|
import { createAccount } from './create-account';
|
||||||
|
import { getAccountConfig } from './get-account-config';
|
||||||
|
import { getAccountPrompts } from './get-account-prompts';
|
||||||
|
import { pipelineStatus } from './pipeline-status';
|
||||||
|
import { runPipelinePhase } from './run-pipeline-phase';
|
||||||
|
import { generateImages } from './generate-images';
|
||||||
|
import { generateVideos } from './generate-videos';
|
||||||
|
import { listOutputs } from './list-outputs';
|
||||||
|
import { getManifest } from './get-manifest';
|
||||||
|
import { createManifest } from './create-manifest';
|
||||||
|
|
||||||
|
export const tools: ToolDefinition[] = [
|
||||||
|
listAccounts,
|
||||||
|
createAccount,
|
||||||
|
getAccountConfig,
|
||||||
|
getAccountPrompts,
|
||||||
|
pipelineStatus,
|
||||||
|
runPipelinePhase,
|
||||||
|
generateImages,
|
||||||
|
generateVideos,
|
||||||
|
listOutputs,
|
||||||
|
getManifest,
|
||||||
|
createManifest,
|
||||||
|
];
|
||||||
28
web/server/agent/tools/list-accounts.ts
Normal file
28
web/server/agent/tools/list-accounts.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { ACCOUNTS_DIR, loadJSON } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const listAccounts: ToolDefinition = {
|
||||||
|
name: 'list_accounts',
|
||||||
|
description: '列出所有可用账号,返回每个账号的名称、描述、生图模型和视频模型',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
execute: async () => {
|
||||||
|
if (!fs.existsSync(ACCOUNTS_DIR)) return '暂无账号';
|
||||||
|
const dirs = fs.readdirSync(ACCOUNTS_DIR, { withFileTypes: true })
|
||||||
|
.filter((d) => d.isDirectory() && !d.name.startsWith('_') && !d.name.startsWith('.'))
|
||||||
|
.map((d) => {
|
||||||
|
const configPath = path.join(ACCOUNTS_DIR, d.name, 'account.json');
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
const cfg = loadJSON(configPath) as Record<string, string>;
|
||||||
|
return `${d.name} - ${cfg.description || '无描述'} (生图:${cfg.imageModel} 视频:${cfg.videoModel} 画幅:${cfg.defaultFormat})`;
|
||||||
|
}
|
||||||
|
return d.name;
|
||||||
|
});
|
||||||
|
return dirs.join('\n') || '暂无账号';
|
||||||
|
},
|
||||||
|
};
|
||||||
46
web/server/agent/tools/list-outputs.ts
Normal file
46
web/server/agent/tools/list-outputs.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { OUTPUT_DIR, loadJSON } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const listOutputs: ToolDefinition = {
|
||||||
|
name: 'list_outputs',
|
||||||
|
description: '列出 output 目录下所有项目及其 manifest 状态,方便查看历史生成记录和进行中的任务。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
execute: async () => {
|
||||||
|
if (!fs.existsSync(OUTPUT_DIR)) return 'output 目录不存在';
|
||||||
|
const entries = fs.readdirSync(OUTPUT_DIR, { withFileTypes: true })
|
||||||
|
.filter((d) => d.isDirectory())
|
||||||
|
.map((d) => {
|
||||||
|
const manifestPath = path.join(OUTPUT_DIR, d.name, 'manifest.json');
|
||||||
|
if (!fs.existsSync(manifestPath)) return { name: d.name, status: 'no manifest' };
|
||||||
|
try {
|
||||||
|
const manifest = loadJSON(manifestPath) as {
|
||||||
|
account?: string;
|
||||||
|
imageModel?: string;
|
||||||
|
videoModel?: string;
|
||||||
|
mode?: string;
|
||||||
|
pipeline?: { phases?: Record<string, string> };
|
||||||
|
items?: Array<{ status?: string }>;
|
||||||
|
};
|
||||||
|
const phases = manifest.pipeline?.phases || {};
|
||||||
|
const itemCount = manifest.items?.length || 0;
|
||||||
|
const doneCount = manifest.items?.filter((i) => i.status === 'done').length || 0;
|
||||||
|
return {
|
||||||
|
name: d.name,
|
||||||
|
account: manifest.account || '?',
|
||||||
|
mode: manifest.mode || '?',
|
||||||
|
items: `${doneCount}/${itemCount} done`,
|
||||||
|
phases: Object.entries(phases).map(([k, v]) => `${k}:${v}`).join(', ') || 'pending',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { name: d.name, status: 'manifest parse error' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return JSON.stringify(entries, null, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
22
web/server/agent/tools/pipeline-status.ts
Normal file
22
web/server/agent/tools/pipeline-status.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { PIPELINE_SCRIPT, PROJECT_ROOT } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const pipelineStatus: ToolDefinition = {
|
||||||
|
name: 'pipeline_status',
|
||||||
|
description: '查看指定 manifest 的 pipeline 执行进度和各阶段状态',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
manifest: { type: 'string', description: 'manifest.json 的绝对路径' },
|
||||||
|
},
|
||||||
|
required: ['manifest'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { manifest } = params as { manifest: string };
|
||||||
|
const result = execSync(`node "${PIPELINE_SCRIPT}" status --manifest "${manifest}"`, {
|
||||||
|
cwd: PROJECT_ROOT, encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
};
|
||||||
31
web/server/agent/tools/run-pipeline-phase.ts
Normal file
31
web/server/agent/tools/run-pipeline-phase.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { PIPELINE_SCRIPT, PROJECT_ROOT } from './shared';
|
||||||
|
import type { ToolDefinition } from './types';
|
||||||
|
|
||||||
|
export const runPipelinePhase: ToolDefinition = {
|
||||||
|
name: 'run_pipeline_phase',
|
||||||
|
description: '执行视频创作 pipeline 的指定阶段。阶段顺序: images(生图) → upload(上传) → videos(生视频) → tts(配音) → assemble(成片组装)。执行前需确认 manifest.json 已存在。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
manifest: { type: 'string', description: 'manifest.json 的绝对路径' },
|
||||||
|
phase: { type: 'string', description: '要执行的阶段: images, upload, videos, tts, assemble。多个阶段用逗号分隔如 images,upload' },
|
||||||
|
},
|
||||||
|
required: ['manifest', 'phase'],
|
||||||
|
},
|
||||||
|
execute: async (params) => {
|
||||||
|
const { manifest, phase } = params as { manifest: string; phase: string };
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const proc = spawn('node', [PIPELINE_SCRIPT, 'run', '--manifest', manifest, '--phase', phase], {
|
||||||
|
cwd: PROJECT_ROOT,
|
||||||
|
env: { ...process.env },
|
||||||
|
});
|
||||||
|
let output = '';
|
||||||
|
proc.stdout.on('data', (d: Buffer) => { output += d.toString(); });
|
||||||
|
proc.stderr.on('data', (d: Buffer) => { output += d.toString(); });
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
code === 0 ? resolve(output || '执行成功') : reject(new Error(`Pipeline exit code ${code}: ${output.slice(-500)}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
46
web/server/agent/tools/shared.ts
Normal file
46
web/server/agent/tools/shared.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import os from 'os';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
export const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
||||||
|
export const PIPELINE_SCRIPT = path.join(PROJECT_ROOT, '.claude', 'skills', 'video-from-script', 'scripts', 'pipeline.js');
|
||||||
|
export const ACCOUNTS_DIR = path.join(PROJECT_ROOT, 'accounts');
|
||||||
|
export const OUTPUT_DIR = path.join(PROJECT_ROOT, 'output');
|
||||||
|
|
||||||
|
export function loadJSON(filePath: string): unknown {
|
||||||
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runInit(params: {
|
||||||
|
account: string;
|
||||||
|
mode: string;
|
||||||
|
items: unknown[];
|
||||||
|
imageModel?: string;
|
||||||
|
videoModel?: string;
|
||||||
|
format?: string;
|
||||||
|
}): string {
|
||||||
|
const tmpFile = path.join(os.tmpdir(), `pipeline-items-${Date.now()}.json`);
|
||||||
|
fs.writeFileSync(tmpFile, JSON.stringify(params.items), 'utf-8');
|
||||||
|
try {
|
||||||
|
const args = [
|
||||||
|
`"${PIPELINE_SCRIPT}"`, 'init',
|
||||||
|
`--account "${params.account}"`,
|
||||||
|
`--mode ${params.mode}`,
|
||||||
|
`--items-file "${tmpFile}"`,
|
||||||
|
params.imageModel ? `--image-model ${params.imageModel}` : '',
|
||||||
|
params.videoModel ? `--video-model ${params.videoModel}` : '',
|
||||||
|
params.format ? `--format ${params.format}` : '',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
const output = execSync(`node ${args}`, { cwd: PROJECT_ROOT, encoding: 'utf-8' });
|
||||||
|
const match = output.match(/Manifest 已创建: (.+)/);
|
||||||
|
if (!match) throw new Error(`Failed to parse manifest path from init output: ${output}`);
|
||||||
|
return match[1].trim();
|
||||||
|
} finally {
|
||||||
|
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
web/server/agent/tools/types.ts
Normal file
10
web/server/agent/tools/types.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface ToolDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
input_schema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, { type: string; description: string }>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
execute: (params: Record<string, unknown>) => Promise<string>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user