2026-05-07 02:36:28 +08:00
|
|
|
|
import { spawn, execSync } from 'child_process';
|
|
|
|
|
|
import path from 'path';
|
|
|
|
|
|
import fs from 'fs';
|
2026-05-07 03:22:15 +08:00
|
|
|
|
import { fileURLToPath } from 'url';
|
2026-05-07 02:36:28 +08:00
|
|
|
|
|
2026-05-07 03:22:15 +08:00
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
|
const __dirname = path.dirname(__filename);
|
2026-05-07 02:36:28 +08:00
|
|
|
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
|
|
|
|
const PIPELINE_SCRIPT = path.join(PROJECT_ROOT, '.claude', 'skills', 'video-from-script', 'scripts', 'pipeline.js');
|
|
|
|
|
|
|
|
|
|
|
|
export interface ToolDefinition {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
description: string;
|
2026-05-07 03:22:15 +08:00
|
|
|
|
input_schema: {
|
|
|
|
|
|
type: 'object';
|
|
|
|
|
|
properties: Record<string, { type: string; description: string }>;
|
|
|
|
|
|
required?: string[];
|
|
|
|
|
|
};
|
2026-05-07 02:36:28 +08:00
|
|
|
|
execute: (params: Record<string, unknown>) => Promise<string>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const tools: ToolDefinition[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'list_accounts',
|
2026-05-07 03:22:15 +08:00
|
|
|
|
description: '列出所有可用账号,返回每个账号的名称、描述、生图模型和视频模型',
|
|
|
|
|
|
input_schema: {
|
|
|
|
|
|
type: 'object',
|
|
|
|
|
|
properties: {},
|
|
|
|
|
|
required: [],
|
|
|
|
|
|
},
|
2026-05-07 02:36:28 +08:00
|
|
|
|
execute: async () => {
|
|
|
|
|
|
const accountsDir = path.join(PROJECT_ROOT, 'accounts');
|
|
|
|
|
|
const dirs = fs.readdirSync(accountsDir, { withFileTypes: true })
|
|
|
|
|
|
.filter((d) => d.isDirectory() && !d.name.startsWith('_') && !d.name.startsWith('.'))
|
|
|
|
|
|
.map((d) => {
|
|
|
|
|
|
const configPath = path.join(accountsDir, d.name, 'account.json');
|
|
|
|
|
|
if (fs.existsSync(configPath)) {
|
|
|
|
|
|
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
2026-05-07 03:22:15 +08:00
|
|
|
|
return `${d.name} - ${cfg.description || '无描述'} (生图:${cfg.imageModel} 视频:${cfg.videoModel} 画幅:${cfg.defaultFormat})`;
|
2026-05-07 02:36:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
return d.name;
|
|
|
|
|
|
});
|
2026-05-07 03:22:15 +08:00
|
|
|
|
return dirs.join('\n') || '暂无账号';
|
2026-05-07 02:36:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-07 03:22:15 +08:00
|
|
|
|
name: 'create_account',
|
|
|
|
|
|
description: '创建新的短视频账号。需要提供账号ID、名称和描述。创建后可在 accounts/ 目录下找到配置。',
|
|
|
|
|
|
input_schema: {
|
2026-05-07 02:36:28 +08:00
|
|
|
|
type: 'object',
|
|
|
|
|
|
properties: {
|
2026-05-07 03:22:15 +08:00
|
|
|
|
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 (方形)' },
|
2026-05-07 02:36:28 +08:00
|
|
|
|
},
|
2026-05-07 03:22:15 +08:00
|
|
|
|
required: ['id', 'name'],
|
2026-05-07 02:36:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
execute: async (params) => {
|
2026-05-07 03:22:15 +08:00
|
|
|
|
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}`;
|
2026-05-07 02:36:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'pipeline_status',
|
2026-05-07 03:22:15 +08:00
|
|
|
|
description: '查看指定 manifest 的 pipeline 执行进度和各阶段状态',
|
|
|
|
|
|
input_schema: {
|
2026-05-07 02:36:28 +08:00
|
|
|
|
type: 'object',
|
|
|
|
|
|
properties: {
|
2026-05-07 03:22:15 +08:00
|
|
|
|
manifest: { type: 'string', description: 'manifest.json 的绝对路径' },
|
2026-05-07 02:36:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
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;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-07 03:22:15 +08:00
|
|
|
|
name: 'run_pipeline_phase',
|
|
|
|
|
|
description: '执行视频创作 pipeline 的指定阶段。阶段顺序: images(生图) → upload(上传) → videos(生视频) → tts(配音) → assemble(成片组装)。执行前需确认 manifest.json 已存在。',
|
|
|
|
|
|
input_schema: {
|
2026-05-07 02:36:28 +08:00
|
|
|
|
type: 'object',
|
|
|
|
|
|
properties: {
|
2026-05-07 03:22:15 +08:00
|
|
|
|
manifest: { type: 'string', description: 'manifest.json 的绝对路径' },
|
|
|
|
|
|
phase: { type: 'string', description: '要执行的阶段: images, upload, videos, tts, assemble。多个阶段用逗号分隔如 images,upload' },
|
2026-05-07 02:36:28 +08:00
|
|
|
|
},
|
2026-05-07 03:22:15 +08:00
|
|
|
|
required: ['manifest', 'phase'],
|
2026-05-07 02:36:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
execute: async (params) => {
|
2026-05-07 03:22:15 +08:00
|
|
|
|
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(PROJECT_ROOT, 'accounts', accountId, 'account.json');
|
|
|
|
|
|
if (!fs.existsSync(configPath)) return `账号「${accountId}」不存在`;
|
|
|
|
|
|
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
|
|
|
|
return JSON.stringify(cfg, null, 2);
|
2026-05-07 02:36:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|