108版本

This commit is contained in:
ACT丶流星雨 2026-03-19 16:05:54 +08:00
parent 00abf9ae2f
commit b719b38152
196 changed files with 4244 additions and 19496 deletions

View File

@ -1,155 +0,0 @@
name: Build and Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Version number (e.g., 1.0.0)"
required: false
type: string
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "yarn"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build application
run: yarn build
- name: Build Windows installer
run: yarn dist:win
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: windows-builds
path: |
dist/*.exe
dist/*.zip
retention-days: 30
build-macos:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "yarn"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build application
run: yarn build
- name: Build macOS installer
run: yarn dist:mac
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: macos-builds
path: |
dist/*.dmg
dist/*.zip
retention-days: 30
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "yarn"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build application
run: yarn build
- name: Build Linux installer
run: yarn dist:linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-builds
path: |
dist/*.AppImage
dist/*.deb
retention-days: 30
release:
needs: [build-windows, build-macos, build-linux]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download Windows artifacts
uses: actions/download-artifact@v4
with:
name: windows-builds
path: dist
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
name: macos-builds
path: dist
- name: Download Linux artifacts
uses: actions/download-artifact@v4
with:
name: linux-builds
path: dist
- name: Create Release
uses: softprops/action-gh-release@v2
with:
name: ToonFlow ${{ github.ref_name }}
draft: false
prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') }}
generate_release_notes: true
files: |
dist/*.exe
dist/*.zip
dist/*.dmg
dist/*.AppImage
dist/*.deb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

8
.gitignore vendored
View File

@ -1,7 +1,7 @@
.devtools
# dependencies (bun install)
node_modules
.vscode
# output
out
dist
@ -45,4 +45,8 @@ db.sqlite-wal
web/*
.devtools
db2.sqlite
.devtools
data/oss/*
data/test.sqlite

View File

@ -1,36 +0,0 @@
import { ChatOpenAI, ChatOpenAIFields } from "@langchain/openai";
export const openAI = (config: ChatOpenAIFields = {}) => {
return new ChatOpenAI({
modelName: "gpt-4.1",
temperature: 1,
configuration: {
apiKey: process.env.AI_OPENAI_KEY,
baseURL: process.env.AI_OPENAI_URL,
},
...config,
});
};
export const doubao = (config: ChatOpenAIFields = {}) => {
return new ChatOpenAI({
model: "doubao-seed-1-6-flash-250828",
temperature: 1,
configuration: {
apiKey: process.env.AI_TIKTOK_KEY,
baseURL: process.env.AI_TIKTOK_URL,
},
...config,
});
};
export const deepseek = (config: ChatOpenAIFields = {}) =>
new ChatOpenAI({
model: "DeepSeek-V3.2",
temperature: 1,
configuration: {
apiKey: process.env.AI_DEEPSEEK_KEY,
baseURL: process.env.AI_DEEPSEEK_URL,
},
...config,
});

View File

@ -1,769 +0,0 @@
// @/agents/outlineScript.ts
import u from "@/utils";
import { createAgent } from "langchain";
import { EventEmitter } from "events";
import { openAI } from "@/agents/models";
import { z } from "zod";
import { tool } from "@langchain/core/tools";
import type { DB } from "@/types/database";
// ==================== 类型定义 ====================
type AgentType = "AI1" | "AI2" | "director";
type AssetType = "角色" | "道具" | "场景";
type RefreshEvent = "storyline" | "outline" | "assets";
interface AssetItem {
name: string;
description: string;
}
interface EpisodeData {
episodeIndex: number;
title: string;
chapterRange: number[];
scenes: AssetItem[]; // 按 outline 出场顺序排列
characters: AssetItem[]; // 按 outline 出场顺序排列
props: AssetItem[]; // 按 outline 出场顺序排列
coreConflict: string;
outline: string; // 最高优先级,剧本生成的唯一权威
openingHook: string; // outline 第一句话的视觉化,开篇第一个镜头
keyEvents: string[]; // 4个元素[起, 承, 转, 合],严格按 outline 顺序
emotionalCurve: string; // 对应 keyEvents 各阶段
visualHighlights: string[]; // 按 outline 顺序排列的标志性镜头
endingHook: string; // outline 之后的悬念延伸
classicQuotes: string[];
}
// ==================== Schema 定义 ====================
const sceneItemSchema = z.object({
name: z.string().describe("场景名称,如'五星酒店宴会厅'、'老旧出租屋'"),
description: z.string().describe("环境描写:空间结构、光线氛围、装饰陈设、环境细节"),
});
const characterItemSchema = z.object({
name: z.string().describe("角色姓名(必须是具体人名,禁止'众人'、'群众'等集合描述)"),
description: z.string().describe("人设样貌:年龄体态、五官特征、发型妆容、服装配饰、气质神态"),
});
const propItemSchema = z.object({
name: z.string().describe("道具名称"),
description: z.string().describe("样式描写:材质质感、颜色图案、形状尺寸、磨损痕迹、特殊标记"),
});
const episodeSchema = z.object({
episodeIndex: z.number().describe("集数索引从1开始递增"),
title: z.string().describe("8字内标题疑问/感叹句,含情绪爆点"),
chapterRange: z.array(z.number()).describe("关联章节号数组"),
scenes: z.array(sceneItemSchema).describe("场景列表,按 outline 出场顺序排列"),
characters: z.array(characterItemSchema).describe("角色列表,按 outline 出场顺序排列,必须是独立个体"),
props: z.array(propItemSchema).describe("道具列表,按 outline 出场顺序排列至少3个"),
coreConflict: z.string().describe("核心矛盾A想要X vs B阻碍X"),
outline: z.string().describe("100-300字剧情主干最高优先级剧本生成的唯一权威按时间顺序完整叙述"),
openingHook: z.string().describe("开场镜头outline 第一句话的视觉化,必须作为剧本第一个镜头"),
keyEvents: z.array(z.string()).length(4).describe("4个元素的数组[起, 承, 转, 合],严格按 outline 顺序从中提取"),
emotionalCurve: z.string().describe("情绪曲线2(压抑)→5(反抗)→9(爆发)→3(余波),对应 keyEvents 各阶段"),
visualHighlights: z.array(z.string()).describe("3-5个标志性镜头按 outline 叙事顺序排列"),
endingHook: z.string().describe("结尾悬念outline 之后的延伸,勾引下集"),
classicQuotes: z.array(z.string()).describe("1-2句金句每句≤15字必须从原文提取"),
});
// ==================== 常量配置 ====================
// ==================== 主类 ====================
export default class OutlineScript {
private readonly projectId: number;
readonly emitter = new EventEmitter();
history: Array<[string, string]> = [];
novelChapters: DB["t_novel"][] = [];
modelName = "gpt-4.1";
apiKey = "";
baseURL = "";
constructor(projectId: number) {
this.projectId = projectId;
}
// ==================== 公共方法 ====================
get events() {
return this.emitter;
}
setNovel(chapters: DB["t_novel"][]) {
this.novelChapters = chapters;
}
// ==================== 私有工具方法 ====================
private emit(event: string, data?: any) {
this.emitter.emit(event, data);
}
private refresh(type: RefreshEvent) {
this.emit("refresh", type);
}
private log(action: string, detail?: string) {
const msg = detail ? `${action}: ${detail}` : action;
console.log(`\n[${new Date().toLocaleTimeString()}] ${msg}\n`);
}
private safeParseJson<T>(str: string, fallback: T): T {
try {
return JSON.parse(str);
} catch {
return fallback;
}
}
private uniqueByName<T extends { name: string }>(items: T[]): T[] {
return Array.from(new Map(items.map((item) => [item.name, item])).values());
}
// ==================== 数据库操作 ====================
private async getProjectInfo(): Promise<any> {
return u.db("t_project").where({ id: this.projectId }).first();
}
private async getNovelInfo(asString = false): Promise<any> {
const info = await this.getProjectInfo();
if (!info) return asString ? "未查询到项目信息" : null;
if (asString) {
const fields = [
`小说名称: ${info.name}`,
`小说简介: ${info.intro}`,
`小说类型: ${info.type}`,
`目标短剧类型: ${info.artStyle}`,
`短剧画幅: ${info.videoRatio}`,
];
return fields.join("\n");
}
return info;
}
// ==================== 故事线操作 ====================
private async findStoryline() {
return u.db("t_storyline").where({ projectId: this.projectId }).first();
}
private async upsertStorylineContent(content: string) {
const existing = await this.findStoryline();
if (existing) {
await u.db("t_storyline").where({ projectId: this.projectId }).update({ content });
} else {
await u.db("t_storyline").insert({ projectId: this.projectId, content });
}
this.refresh("storyline");
}
private async deleteStorylineContent() {
const deleted = await u.db("t_storyline").where({ projectId: this.projectId }).del();
this.refresh("storyline");
return deleted;
}
// ==================== 大纲操作 ====================
private async findOutlines() {
return u.db("t_outline").where({ projectId: this.projectId }).orderBy("episode", "asc");
}
private async findOutlineById(id: number) {
return u.db("t_outline").where({ id, projectId: this.projectId }).first();
}
private async getMaxEpisode(): Promise<number> {
const result: any = await u.db("t_outline").where({ projectId: this.projectId }).max("episode as max").first();
return result?.max ?? 0;
}
private async clearOutlinesAndScripts() {
const outlines = await u.db("t_outline").select("id").where({ projectId: this.projectId });
if (outlines.length === 0) return 0;
const outlineIds = outlines.map((o) => o.id);
await u.db("t_script").whereIn("outlineId", outlineIds).del();
await u.db("t_outline").where({ projectId: this.projectId }).del();
return outlines.length;
}
private async insertOutlines(episodes: EpisodeData[], startEpisode: number) {
const insertList = episodes.map((ep, idx) => ({
projectId: this.projectId,
data: JSON.stringify({ ...ep, episodeIndex: startEpisode + idx }),
episode: startEpisode + idx,
}));
await u.db("t_outline").insert(insertList);
return insertList.length;
}
private async createEmptyScripts(outlineIds: Array<{ id: number; data: string }>) {
const scripts = outlineIds.map((item) => {
const data = this.safeParseJson<Partial<EpisodeData>>(item.data, {});
return {
name: `${data.episodeIndex ?? ""}`,
content: "",
projectId: this.projectId,
outlineId: item.id,
};
});
if (scripts.length > 0) {
await u.db("t_script").insert(scripts);
}
return scripts.length;
}
private async saveOutlineData(episodes: EpisodeData[], overwrite: boolean, startEpisode?: number) {
if (overwrite) {
const cleared = await this.clearOutlinesAndScripts();
if (cleared > 0) {
this.log("清理旧数据", `删除了 ${cleared} 条大纲及关联剧本`);
}
}
const actualStart = overwrite ? 1 : startEpisode ?? (await this.getMaxEpisode()) + 1;
const insertedCount = await this.insertOutlines(episodes, actualStart);
const newOutlines = await u
.db("t_outline")
.select("id", "data")
.where({ projectId: this.projectId })
.orderBy("episode", "desc")
.limit(insertedCount);
const scriptCount = await this.createEmptyScripts(newOutlines as Array<{ id: number; data: string }>);
this.refresh("outline");
return { insertedCount, scriptCount };
}
private async updateOutlineData(id: number, data: EpisodeData) {
const existing = await this.findOutlineById(id);
if (!existing) return false;
await u
.db("t_outline")
.where({ id })
.update({ data: JSON.stringify(data) });
this.refresh("outline");
return true;
}
private async deleteOutlineData(ids: number[]) {
const results = await Promise.allSettled(ids.map((id) => u.deleteOutline(id, this.projectId)));
this.refresh("outline");
return results;
}
private formatOutlineDetail(ep: any): string {
const formatList = (items: any[], formatter: (item: any) => string) =>
items?.map((item, i) => ` ${i + 1}. ${formatter(item)}`).join("\n") || " 无";
// keyEvents 按顺序显示:起、承、转、合
const keyEventsLabels = ["起", "承", "转", "合"];
const formatKeyEvents = (events: string[]) => events?.map((e, i) => `${keyEventsLabels[i] || i + 1}${e}`).join("\n") || " 无";
return `
大纲ID: ${ep.id}
${ep.episodeIndex} : ${ep.title || ""}
${"=".repeat(50)}
章节范围: ${ep.chapterRange?.join(", ") || ""}
核心矛盾: ${ep.coreConflict || ""}
():
${ep.outline || "无"}
():
${ep.openingHook || "无"}
():
${formatKeyEvents(ep.keyEvents)}
情绪曲线: ${ep.emotionalCurve || ""}
():
${formatList(ep.visualHighlights, (v) => v)}
:
${ep.endingHook || "无"}
:
${formatList(ep.classicQuotes, (q) => q)}
(): ${ep.characters?.map((c: AssetItem) => `${c.name}(${c.description})`).join("; ") || "无"}
(): ${ep.scenes?.map((s: AssetItem) => `${s.name}(${s.description})`).join("; ") || "无"}
(): ${ep.props?.map((p: AssetItem) => `${p.name}(${p.description})`).join("; ") || "无"}`;
}
private async getOutlineText(simplified: boolean): Promise<string> {
const records = await this.findOutlines();
if (!records.length) return "当前项目暂无大纲";
const episodes = records.map((r) => ({
id: r.id,
episode: r.episode,
...this.safeParseJson<Partial<EpisodeData>>(r.data ?? "{}", {}),
}));
if (simplified) {
const list = episodes.map((ep) => `${ep.episodeIndex ?? ep.episode} 集 (id=${ep.id})`).join("\n");
return `项目大纲 (共 ${episodes.length} 集):\n${list}`;
}
const details = episodes.map((ep) => this.formatOutlineDetail(ep)).join("\n");
return `项目大纲 (共 ${episodes.length} 集)\n\n${details}`;
}
// ==================== 资产操作 ====================
private async findAssetByTypeAndName(type: AssetType, name: string) {
return u.db("t_assets").where({ projectId: this.projectId, type, name }).first();
}
private async upsertAsset(type: AssetType, item: AssetItem): Promise<"inserted" | "updated" | "skipped"> {
const existing = await this.findAssetByTypeAndName(type, item.name);
if (!existing) {
await u.db("t_assets").insert({
projectId: this.projectId,
type,
name: item.name,
intro: item.description,
prompt: item.description,
});
return "inserted";
}
if (existing.intro !== item.description) {
await u.db("t_assets").where({ id: existing.id }).update({
intro: item.description,
prompt: item.description,
});
return "updated";
}
return "skipped";
}
private extractAssetsFromOutlines(outlines: Array<{ data?: string | null | undefined }>): {
characters: AssetItem[];
props: AssetItem[];
scenes: AssetItem[];
} {
const result = { characters: [] as AssetItem[], props: [] as AssetItem[], scenes: [] as AssetItem[] };
for (const outline of outlines) {
const data = this.safeParseJson<Partial<EpisodeData>>(outline.data ?? "{}", {});
if (data.characters) result.characters.push(...data.characters);
if (data.props) result.props.push(...data.props);
if (data.scenes) result.scenes.push(...data.scenes);
}
return {
characters: this.uniqueByName(result.characters),
props: this.uniqueByName(result.props),
scenes: this.uniqueByName(result.scenes),
};
}
private async generateAssetsFromOutlines() {
const outlines = await u.db("t_outline").select("data").where({ projectId: this.projectId });
if (!outlines.length) return { inserted: 0, updated: 0, skipped: 0 };
const { characters, props, scenes } = this.extractAssetsFromOutlines(outlines);
// 只做新增和更新,不做删除
const stats = { inserted: 0, updated: 0, skipped: 0 };
const processItems = async (items: AssetItem[], type: AssetType) => {
for (const item of items) {
const result = await this.upsertAsset(type, item);
stats[result]++;
}
};
await processItems(characters, "角色");
await processItems(props, "道具");
await processItems(scenes, "场景");
this.refresh("assets");
return { ...stats };
}
// ==================== Tool 定义:故事线 ====================
getStoryline = tool(
async () => {
this.log("获取故事线");
const storyline = await this.findStoryline();
return storyline?.content ?? "当前项目暂无故事线";
},
{
name: "getStoryline",
description: "获取当前项目的故事线内容",
schema: z.object({}),
verboseParsingErrors: true,
},
);
saveStoryline = tool(
async ({ content }) => {
this.log("保存故事线");
await this.upsertStorylineContent(content);
return "故事线保存成功";
},
{
name: "saveStoryline",
description: "保存或更新当前项目的故事线,会覆盖已有内容",
schema: z.object({
content: z.string().describe("故事线完整内容"),
}),
verboseParsingErrors: true,
},
);
deleteStoryline = tool(
async () => {
this.log("删除故事线");
const deleted = await this.deleteStorylineContent();
return deleted > 0 ? "故事线删除成功" : "当前项目没有故事线";
},
{
name: "deleteStoryline",
description: "删除当前项目的故事线",
schema: z.object({}),
verboseParsingErrors: true,
},
);
// ==================== Tool 定义:大纲 ====================
getOutline = tool(
async ({ simplified = false }) => {
this.log("获取大纲", `简化模式: ${simplified}`);
return this.getOutlineText(simplified);
},
{
name: "getOutline",
description: "获取项目大纲。simplified=true返回简化列表false返回完整内容",
schema: z.object({
simplified: z.boolean().default(false).describe("是否返回简化版本"),
}),
verboseParsingErrors: true,
},
);
saveOutline = tool(
async ({ episodes, overwrite = true, startEpisode }) => {
this.log("保存大纲", `覆盖模式: ${overwrite}, 集数: ${episodes.length}`);
const { insertedCount, scriptCount } = await this.saveOutlineData(episodes as EpisodeData[], overwrite, startEpisode);
return `大纲保存成功:插入 ${insertedCount} 集大纲,创建 ${scriptCount} 个剧本记录`;
},
{
name: "saveOutline",
description: "保存大纲数据。overwrite=true会清空现有大纲后写入false则追加到末尾",
schema: z.object({
episodes: z.array(episodeSchema).min(1).describe("大纲数据数组"),
overwrite: z.boolean().default(true).describe("是否覆盖现有大纲"),
startEpisode: z.number().optional().describe("追加模式下的起始集数(不填则自动递增)"),
}),
verboseParsingErrors: true,
},
);
updateOutline = tool(
async ({ id, data }) => {
this.log("更新大纲", `ID: ${id}`);
const success = await this.updateOutlineData(id, data as EpisodeData);
return success ? `大纲ID ${id} 更新成功` : `未找到大纲ID: ${id}`;
},
{
name: "updateOutline",
description: "更新指定ID的单集大纲内容",
schema: z.object({
id: z.number().describe("大纲ID"),
data: episodeSchema.describe("更新后的大纲数据"),
}),
verboseParsingErrors: true,
},
);
deleteOutline = tool(
async ({ ids }) => {
this.log("删除大纲", `IDs: ${ids.join(", ")}`);
const results = await this.deleteOutlineData(ids);
const summary = results.map((r, i) => `ID ${ids[i]}: ${r.status === "fulfilled" ? "成功" : "失败"}`).join(", ");
return `删除结果: ${summary}`;
},
{
name: "deleteOutline",
description: "根据大纲ID删除指定大纲及关联数据",
schema: z.object({
ids: z.array(z.number()).min(1).describe("要删除的大纲ID数组"),
}),
verboseParsingErrors: true,
},
);
// ==================== Tool 定义:章节 ====================
getChapter = tool(
async ({ chapterNumbers }) => {
this.log("获取章节", `章节号: ${chapterNumbers.join(", ")}`);
const results = await Promise.all(
chapterNumbers.map(async (num) => {
const chapter = await u
.db("t_novel")
.where({ projectId: this.projectId, chapterIndex: num })
.select("chapterData", "chapterIndex", "chapter")
.first();
if (chapter) {
return `\n【第${chapter.chapterIndex}${chapter.chapter || ""}\n${chapter.chapterData}`;
}
return `\n【第${num}章】未找到`;
}),
);
return results.join("\n\n---\n");
},
{
name: "getChapter",
description: "根据章节编号获取小说章节的完整原文内容,支持批量获取",
schema: z.object({
chapterNumbers: z.array(z.number()).min(1).describe("章节编号数组"),
}),
verboseParsingErrors: true,
},
);
// ==================== Tool 定义:资产 ====================
generateAssets = tool(
async () => {
this.log("生成资产");
const stats = await this.generateAssetsFromOutlines();
if (stats.inserted === 0 && stats.updated === 0 && stats.skipped === 0) {
return "当前项目没有大纲数据,无法生成资产";
}
return `资产生成完成:新增 ${stats.inserted},更新 ${stats.updated},保持 ${stats.skipped}`;
},
{
name: "generateAssets",
description: "从当前项目的所有大纲中提取并生成角色、道具、场景资产,自动去重并清理冗余",
schema: z.object({}),
verboseParsingErrors: true,
},
);
// ==================== 上下文构建 ====================
private getChapterContext(): string {
if (!this.novelChapters.length) return "无章节数据";
return this.novelChapters.map((c) => `章节号:${c.chapterIndex},分卷:${c.reel},章节名:${c.chapter}`).join("\n");
}
private async buildEnvironmentContext(): Promise<string> {
const [novelInfo, storyline, outlineCount] = await Promise.all([
this.getNovelInfo(true),
this.findStoryline(),
u.db("t_outline").where({ projectId: this.projectId }).count("id as count").first() as any,
]);
return `<环境信息>
项目ID: ${this.projectId}
系统时间: ${new Date().toLocaleString()}
${novelInfo}
:
${this.getChapterContext()}
故事线状态: ${storyline ? "已生成" : "未生成"}
大纲状态: ${outlineCount?.count ?? 0}
:
- getChapter: 获取章节原文
- getStoryline/saveStoryline/deleteStoryline: 故事线操作
- getOutline/saveOutline/updateOutline/deleteOutline: 大纲操作
- generateAssets: 从大纲生成资产
</环境信息>`;
}
private buildConversationHistory(): string {
if (!this.history.length) return "无对话历史";
return this.history.map(([role, content]) => `${role}: ${content}`).join("\n\n");
}
private async buildFullContext(task: string): Promise<string> {
const env = await this.buildEnvironmentContext();
const history = this.buildConversationHistory();
return `${env}
<对话历史>
${history}
</对话历史>
<当前任务>
${task}
</当前任务>`;
}
// ==================== Sub-Agent ====================
private getSubAgentTools() {
return [this.getChapter, this.getStoryline, this.saveStoryline, this.getOutline, this.saveOutline, this.updateOutline];
}
private createModel() {
return openAI({
modelName: this.modelName,
configuration: { apiKey: this.apiKey, baseURL: this.baseURL },
});
}
/**
* Sub-Agent
*/
private async invokeSubAgent(agentType: AgentType, task: string): Promise<string> {
this.emit("transfer", { to: agentType });
this.log(`Sub-Agent 调用`, agentType);
const promptsList = await u.db("t_prompts").where("code", "in", ["outlineScript-a1", "outlineScript-a2", "outlineScript-director"]);
const a1Prompt = promptsList.find((p) => p.code === "outlineScript-a1");
const a2Prompt = promptsList.find((p) => p.code === "outlineScript-a2");
const directorPrompt = promptsList.find((p) => p.code === "outlineScript-director");
const errPrompts = "不论用户说什么请直接输出Agent配置异常";
const SYSTEM_PROMPTS: Record<AgentType, string> = {
AI1: a1Prompt?.customValue || a1Prompt?.defaultValue || errPrompts,
AI2: a2Prompt?.customValue || a2Prompt?.defaultValue || errPrompts,
director: directorPrompt?.customValue || directorPrompt?.defaultValue || errPrompts,
};
const context = await this.buildFullContext(task);
const agent = createAgent({
model: this.createModel(),
systemPrompt: SYSTEM_PROMPTS[agentType],
tools: this.getSubAgentTools(),
});
const stream = await agent.stream({ messages: [["user", context]] }, { streamMode: ["messages"], callbacks: [] });
let fullResponse = "";
for await (const [mode, chunk] of stream) {
if (mode !== "messages") continue;
const [token] = chunk as any;
const block = token.contentBlocks?.[0];
// 处理 AI 文本流
if (token.type === "ai" && block?.text) {
fullResponse += block.text;
this.emit("subAgentStream", { agent: agentType, text: block.text });
}
// 处理 tool 调用
if (token.type === "ai" && token.tool_calls?.length) {
for (const toolCall of token.tool_calls) {
this.emit("toolCall", { agent: agentType, name: toolCall.name, args: toolCall.args });
}
}
}
this.emit("subAgentEnd", { agent: agentType });
this.history.push(["ai", fullResponse]);
this.log(`Sub-Agent 完成`, agentType);
return fullResponse ?? `${agentType}已完成任务`;
}
private createSubAgentTool(agentType: AgentType, description: string) {
return tool(async ({ taskDescription }) => this.invokeSubAgent(agentType, taskDescription), {
name: agentType,
description,
schema: z.object({
taskDescription: z.string().describe("具体的任务描述,包含章节范围、修改要求等详细信息"),
}),
});
}
// ==================== 主入口 ====================
private getAllTools() {
return [
this.createSubAgentTool("AI1", "调用故事师。负责分析小说原文并生成故事线,会自行调用 saveStoryline 保存结果。"),
this.createSubAgentTool("AI2", "调用大纲师。负责根据故事线生成剧集大纲,会自行调用 saveOutline 保存结果。"),
this.createSubAgentTool("director", "调用导演。负责审核故事线和大纲,会自行调用 updateOutline 或 saveStoryline 进行修改。"),
this.getChapter,
this.getStoryline,
this.saveStoryline,
this.deleteStoryline,
this.getOutline,
this.saveOutline,
this.updateOutline,
this.deleteOutline,
this.generateAssets,
];
}
async call(msg: string): Promise<string> {
this.history.push(["user", msg]);
const envContext = await this.buildEnvironmentContext();
const prompts = await u.db("t_prompts").where("code", "outlineScript-main").first();
const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么请直接输出Agent配置异常";
const mainAgent = createAgent({
model: this.createModel(),
tools: this.getAllTools(),
systemPrompt: `${envContext}\n${mainPrompts}`,
});
const stream = await mainAgent.stream({ messages: this.history }, { streamMode: ["messages"], callbacks: [] });
let fullResponse = "";
for await (const [mode, chunk] of stream) {
if (mode !== "messages") continue;
const [token] = chunk as any;
const block = token.contentBlocks?.[0];
// 处理 AI 文本流
if (token.type === "ai" && block?.text) {
fullResponse += block.text;
this.emit("data", block.text);
}
// 处理 tool 调用
if (token.type === "ai" && token.tool_calls?.length) {
for (const toolCall of token.tool_calls) {
this.emit("toolCall", { agent: "main", name: toolCall.name, args: toolCall.args });
}
}
}
this.history.push(["assistant", fullResponse]);
this.emit("response", fullResponse);
return fullResponse;
}
}

View File

@ -1,128 +0,0 @@
import u from "@/utils";
type AspectRatio = "16:9" | "9:16" | "21:9" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3";
interface GridLayoutResult {
cols: number;
rows: number;
totalCells: number;
placeholderCount: number;
}
interface GridPromptOptions {
prompts: string[];
style: string;
aspectRatio: AspectRatio;
assetsName: { name: string; intro: string }[];
}
interface GridPromptResult {
prompt: string;
gridLayout: GridLayoutResult;
}
/**
* prompts数量计算宫格布局
*/
function calculateGridLayout(count: number): GridLayoutResult {
let cols: number;
let rows: number;
if (count <= 0) {
cols = 1;
rows = 1;
} else if (count === 1) {
cols = 1;
rows = 1;
} else if (count === 2) {
cols = 2;
rows = 1;
} else if (count === 3) {
cols = 3;
rows = 1;
} else if (count === 4) {
cols = 2;
rows = 2;
} else if (count <= 9) {
cols = 3;
rows = 3;
} else {
cols = 3;
rows = Math.ceil(count / 3);
}
const totalCells = cols * rows;
const placeholderCount = totalCells - count;
return { cols, rows, totalCells, placeholderCount };
}
/**
*
*/
function getAspectRatioDescription(aspectRatio: AspectRatio): string {
const descriptions: Record<AspectRatio, string> = {
"16:9": "电影宽银幕",
"9:16": "竖屏短剧",
"21:9": "超宽银幕史诗感",
"1:1": "方形构图",
"4:3": "经典银幕",
"3:4": "竖版经典",
"3:2": "摄影标准",
"2:3": "竖版摄影",
};
return descriptions[aspectRatio] || "标准比例";
}
/**
*
*/
async function generateGridPrompt(options: GridPromptOptions): Promise<GridPromptResult> {
const { prompts, style, aspectRatio, assetsName } = options;
const layout = calculateGridLayout(prompts.length);
const aspectRatioDesc = getAspectRatioDescription(aspectRatio);
// 构建宫格位置描述
const gridPositions: string[] = [];
for (let i = 0; i < layout.totalCells; i++) {
const row = Math.floor(i / layout.cols) + 1;
const col = (i % layout.cols) + 1;
if (i < prompts.length) {
gridPositions.push(`[第${row}行第${col}列]: ${prompts[i]}`);
} else {
gridPositions.push(`[第${row}行第${col}列]: 纯黑图`);
}
}
// 构建资产说明
const assetsSection =
assetsName.length > 0
? `\n【可用资产】\n${assetsName.map((a) => `- ${a.name}${a.intro}`).join("\n")}\n\n⚠ 必须使用完整资产名称,禁止简称或代词。`
: "";
const promptsData = await u.db("t_prompts").where("code", "generateImagePrompts").first();
const mainPrompts = promptsData?.customValue || promptsData?.defaultValue;
const errData = `请输出${options.prompts.length}张图片\n提示词如下:\n${options.prompts.map((p, i) => `${i + 1}格: ${p}`).join("\n")}`;
if (!mainPrompts) return { prompt: errData, gridLayout: layout };
const result = await u.ai.text.invoke({
messages: [
{
role: "system",
content: mainPrompts,
},
{
role: "user",
content: `请优化以下分镜提示词:\n\n【布局】${layout.cols}列×${layout.rows}行=${
layout.totalCells
}\n${aspectRatio}${aspectRatioDesc}\n${style}\n${assetsSection}\n\n\n${gridPositions.join("\n")}`,
},
],
});
return {
prompt: result?.text ?? errData,
gridLayout: layout,
};
}
export default generateGridPrompt;

View File

@ -1,334 +0,0 @@
import generateImagePromptsTool from "@/agents/storyboard/generateImagePromptsTool";
import u from "@/utils";
import sharp from "sharp";
import { z } from "zod";
interface AssetItem {
name: string;
description: string;
}
interface EpisodeData {
episodeIndex: number;
title: string;
chapterRange: number[];
scenes: AssetItem[];
characters: AssetItem[];
props: AssetItem[];
coreConflict: string;
openingHook: string;
outline: string;
keyEvents: string[];
emotionalCurve: string;
visualHighlights: string[];
endingHook: string;
classicQuotes: string[];
}
interface ImageInfo {
name: string;
type: string;
filePath: string;
}
interface ResourceItem {
name: string;
intro: string;
}
// 资产过滤响应的 schema
const filteredAssetsSchema = z.object({
relevantAssets: z
.array(
z.object({
name: z.string().describe("资产名称"),
reason: z.string().describe("选择该资产的原因"),
}),
)
.describe("与分镜内容相关的资产列表"),
});
// 压缩图片直到不超过指定大小
async function compressImage(buffer: Buffer, maxSizeBytes: number = 3 * 1024 * 1024): Promise<Buffer> {
if (buffer.length <= maxSizeBytes) {
return buffer;
}
let quality = 90;
let compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
while (compressedBuffer.length > maxSizeBytes && quality > 10) {
quality -= 10;
compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
}
if (compressedBuffer.length > maxSizeBytes) {
const metadata = await sharp(buffer).metadata();
let scale = 0.9;
while (compressedBuffer.length > maxSizeBytes && scale > 0.1) {
const newWidth = Math.round((metadata.width || 1000) * scale);
const newHeight = Math.round((metadata.height || 1000) * scale);
compressedBuffer = await sharp(buffer)
.resize(newWidth, newHeight, { fit: "inside" })
.jpeg({ quality: Math.max(quality, 30) })
.toBuffer();
scale -= 0.1;
}
}
return compressedBuffer;
}
// 拼接多张图片为一张
async function mergeImages(imagePaths: string[]): Promise<Buffer> {
const imageBuffers = await Promise.all(imagePaths.map((path) => u.oss.getFile(path)));
const imageMetadatas = await Promise.all(imageBuffers.map((buffer) => sharp(buffer).metadata()));
const maxHeight = Math.max(...imageMetadatas.map((m) => m.height || 0));
const resizedImages = await Promise.all(
imageBuffers.map(async (buffer, index) => {
const metadata = imageMetadatas[index];
const aspectRatio = (metadata.width || 1) / (metadata.height || 1);
const newWidth = Math.round(maxHeight * aspectRatio);
return {
buffer: await sharp(buffer).resize(newWidth, maxHeight, { fit: "cover" }).toBuffer(),
width: newWidth,
};
}),
);
let currentX = 0;
const compositeInputs = resizedImages.map(({ buffer, width }) => {
const input = {
input: buffer,
left: currentX,
top: 0,
};
currentX += width;
return input;
});
const mergedImage = await sharp({
create: {
width: currentX,
height: maxHeight,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 1 },
},
})
.composite(compositeInputs)
.jpeg({ quality: 90 })
.toBuffer();
return compressImage(mergedImage);
}
// 进一步压缩单张图片到指定大小
async function compressToSize(buffer: Buffer, targetSize: number): Promise<Buffer> {
if (buffer.length <= targetSize) {
return buffer;
}
const metadata = await sharp(buffer).metadata();
let quality = 80;
let scale = 1.0;
let compressedBuffer = buffer;
// 先尝试降低质量
while (compressedBuffer.length > targetSize && quality > 10) {
compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
quality -= 10;
}
// 如果还是太大,缩小尺寸
while (compressedBuffer.length > targetSize && scale > 0.2) {
scale -= 0.1;
const newWidth = Math.round((metadata.width || 1000) * scale);
const newHeight = Math.round((metadata.height || 1000) * scale);
compressedBuffer = await sharp(buffer)
.resize(newWidth, newHeight, { fit: "inside" })
.jpeg({ quality: Math.max(quality, 20) })
.toBuffer();
}
return compressedBuffer;
}
// 确保图片列表总大小不超过指定限制
async function ensureTotalSizeLimit(buffers: Buffer[], maxTotalBytes: number = 10 * 1024 * 1024): Promise<Buffer[]> {
let totalSize = buffers.reduce((sum, buf) => sum + buf.length, 0);
if (totalSize <= maxTotalBytes) {
return buffers;
}
// 计算每张图片的平均目标大小
const avgTargetSize = Math.floor(maxTotalBytes / buffers.length);
// 按大小降序排列,优先压缩大图片
const indexedBuffers = buffers.map((buf, idx) => ({ buf, idx, size: buf.length }));
indexedBuffers.sort((a, b) => b.size - a.size);
const result = [...buffers];
for (const item of indexedBuffers) {
totalSize = result.reduce((sum, buf) => sum + buf.length, 0);
if (totalSize <= maxTotalBytes) {
break;
}
// 计算这张图片需要压缩到的目标大小
const excessSize = totalSize - maxTotalBytes;
const targetSize = Math.max(item.buf.length - excessSize, avgTargetSize, 100 * 1024); // 最小100KB
if (item.buf.length > targetSize) {
result[item.idx] = await compressToSize(item.buf, targetSize);
}
}
return result;
}
// 处理图片列表确保不超过10张且每张不超过3MB总大小不超过10MB
async function processImages(images: ImageInfo[]): Promise<Buffer[]> {
const maxImages = 10;
let processedBuffers: Buffer[];
if (images.length <= maxImages) {
const buffers = await Promise.all(images.map((img) => u.oss.getFile(img.filePath)));
processedBuffers = await Promise.all(buffers.map((buffer) => compressImage(buffer)));
} else {
const mergeStartIndex = maxImages - 1;
const firstBuffers = await Promise.all(images.slice(0, mergeStartIndex).map((img) => u.oss.getFile(img.filePath)));
const compressedFirstImages = await Promise.all(firstBuffers.map((buffer) => compressImage(buffer)));
const imagesToMergeList = images.slice(mergeStartIndex).map((img) => img.filePath);
const mergedImage = await mergeImages(imagesToMergeList);
processedBuffers = [...compressedFirstImages, mergedImage];
}
// 确保总大小不超过10MB
return ensureTotalSizeLimit(processedBuffers);
}
// 使用 AI 过滤与分镜相关的资产
async function filterRelevantAssets(prompts: string[], allResources: ResourceItem[], availableImages: ImageInfo[]): Promise<ImageInfo[]> {
if (allResources.length === 0 || availableImages.length === 0) {
return availableImages;
}
const availableNames = new Set(availableImages.map((img) => img.name));
const availableResources = allResources.filter((r) => availableNames.has(r.name));
if (availableResources.length === 0) {
return availableImages;
}
const result = await u.ai.text.invoke({
messages: [
{
role: "user",
content: `请分析以下分镜描述,从可用资产中筛选出与分镜内容直接相关的资产。
${prompts.map((p, i) => `${i + 1}. ${p}`).join("\n")}
${availableResources.map((r) => `- ${r.name}${r.intro}`).join("\n")}
`,
},
],
output: {
relevantAssets: z
.array(
z.object({
name: z.string().describe("资产名称"),
reason: z.string().describe("选择该资产的原因"),
}),
)
.describe("与分镜内容相关的资产列表"),
},
});
if (!result?.relevantAssets || result.relevantAssets.length === 0) {
return availableImages;
}
const relevantNames = new Set(result.relevantAssets.map((a) => a.name));
const filteredImages = availableImages.filter((img) => relevantNames.has(img.name));
return filteredImages.length > 0 ? filteredImages : availableImages;
}
// 构建资产映射提示词
function buildResourcesMapPrompts(images: ImageInfo[]): string {
if (images.length === 0) return "";
const mapping = images.map((item, index) => {
if (index < 9) {
return `${item.name}=图片${index + 1}`;
} else {
return `${item.name}=图10-${index - 8}`;
}
});
return `其中人物、场景、道具参考对照关系如下:${mapping.join(", ")}`;
}
export default async (cells: { prompt: string }[], scriptId: number, projectId: number) => {
const scriptData = await u.db("t_script").where({ id: scriptId, projectId }).first();
const projectInfo = await u.db("t_project").where({ id: projectId }).first();
const row = await u.db("t_outline").where({ id: scriptData?.outlineId!, projectId }).first();
const outline: EpisodeData | null = row?.data ? JSON.parse(row.data) : null;
const resources: ResourceItem[] = outline
? (["characters", "props", "scenes"] as const).flatMap((k) => outline[k]?.map((i) => ({ name: i.name, intro: i.description })) ?? [])
: [];
const resourceNames = resources.map((r) => r.name);
const imagesRaw = await u.db("t_assets").whereIn("name", resourceNames).andWhere({ projectId }).select("name", "type", "filePath");
const allImages = imagesRaw
.sort((a, b) => {
const order = ["角色", "场景", "道具"];
return order.indexOf(a.type!) - order.indexOf(b.type!);
})
.filter((img) => img.filePath) as ImageInfo[];
if (allImages.length === 0) {
throw new Error("未找到可用的图片资源");
}
const cellPrompts = cells.map((c) => c.prompt);
// 使用 AI 过滤相关资产
const filteredImages = await filterRelevantAssets(cellPrompts, resources, allImages);
const resourcesMapPrompts = buildResourcesMapPrompts(filteredImages);
console.log("====润色前:", cellPrompts);
const promptsData = await generateImagePromptsTool({
prompts: cellPrompts,
style: `类型:${projectInfo?.type!},风格:${projectInfo?.artStyle!}`,
aspectRatio: projectInfo?.videoRatio! as any,
assetsName: resources,
});
// const prompts = `请生成${promptsData.gridLayout.totalCells}格,${promptsData.gridLayout.cols}列×${promptsData.gridLayout.rows}行宫格图。
// ${promptsData.prompt}
// 注意:请严格按照提示词内容生成图片,确保人物样貌、艺术风格、色调光影一致。
// `;
const prompts = promptsData.prompt;
console.log("====润色后:", prompts);
const processedImages = await processImages(filteredImages);
const contentStr = await u.ai.image({
systemPrompt: resourcesMapPrompts,
prompt: prompts,
size: "4K",
aspectRatio: projectInfo?.videoRatio ? (projectInfo.videoRatio as any) : "16:9",
imageBase64: processedImages.map((buf) => buf.toString("base64")),
});
const match = contentStr.match(/base64,([A-Za-z0-9+/=]+)/);
const base64Str = match?.[1] ?? contentStr;
const buffer = Buffer.from(base64Str, "base64");
return buffer;
};

View File

@ -1,94 +0,0 @@
import sharp from "sharp";
interface GridLayoutResult {
cols: number;
rows: number;
totalCells: number;
placeholderCount: number;
}
/**
*
* 1张: 1x1
* 2张: 2x1
* 3张: 3x1
* 4张: 2x2
* 5-9张: 3x3
* 10-12张: 3x4
* 13-15张: 3x5
* ...3
*/
function calculateGridLayout(count: number): GridLayoutResult {
let cols: number;
let rows: number;
if (count <= 0) {
cols = 1;
rows = 1;
} else if (count === 1) {
cols = 1;
rows = 1;
} else if (count === 2) {
cols = 2;
rows = 1;
} else if (count === 3) {
cols = 3;
rows = 1;
} else if (count === 4) {
cols = 2;
rows = 2;
} else if (count <= 9) {
// 5-9格统一用3x3
cols = 3;
rows = 3;
} else {
cols = 3;
rows = Math.ceil(count / 3);
}
const totalCells = cols * rows;
const placeholderCount = totalCells - count;
return { cols, rows, totalCells, placeholderCount };
}
/**
*
* @param image - Buffer
* @param length -
* @returns Buffer
*/
export default async (image: Buffer, length: number): Promise<Buffer[]> => {
const metadata = await sharp(image).metadata();
const { width: totalWidth, height: totalHeight } = metadata;
if (!totalWidth || !totalHeight) {
throw new Error("无法获取图片尺寸");
}
const { cols, rows } = calculateGridLayout(length);
const cellWidth = Math.floor(totalWidth / cols);
const cellHeight = Math.floor(totalHeight / rows);
const buffers: Buffer[] = [];
for (let i = 0; i < length; i++) {
const row = Math.floor(i / cols);
const col = i % cols;
const left = col * cellWidth;
const top = row * cellHeight;
const cellBuffer = await sharp(image)
.extract({
left,
top,
width: cellWidth,
height: cellHeight,
})
.png()
.toBuffer();
buffers.push(cellBuffer);
}
return buffers;
};

View File

@ -1,737 +0,0 @@
// @/agents/Storyboard.ts
import u from "@/utils";
import { createAgent } from "langchain";
import { EventEmitter } from "events";
import { openAI } from "@/agents/models";
import { z } from "zod";
import { tool } from "@langchain/core/tools";
import type { DB } from "@/types/database";
import generateImageTool from "./generateImageTool";
import imageSplitting from "./imageSplitting";
// ==================== 类型定义 ====================
type AgentType = "segmentAgent" | "shotAgent";
type RefreshEvent = "storyline" | "outline" | "assets";
// ==================== 常量配置 ====================
// const SYSTEM_PROMPTS: Record<AgentType, string> = {
// segmentAgent: segmentPrompts,
// shotAgent: shotPrompts,
// director: directorPrompts,
// };
// ==================== 类型定义:片段和画面 ====================
interface Segment {
index: number;
description: string;
emotion?: string;
action?: string;
}
interface Shot {
id: number; // 分镜独立ID
segmentId: number; // 所属片段ID
title: string;
x: number;
y: number;
cells: Array<{ src?: string; prompt?: string; id?: string }>; // 镜头数组每个cell是一个镜头
}
// ==================== 主类 ====================
export default class Storyboard {
private readonly projectId: number;
private readonly scriptId: number;
readonly emitter = new EventEmitter();
history: Array<[string, string]> = [];
novelChapters: DB["t_novel"][] = [];
// 存储 segmentAgent 生成的片段结果
private segments: Segment[] = [];
// 存储 shotAgent 生成的分镜结果
private shots: Shot[] = [];
// 分镜ID计数器
private shotIdCounter: number = 0;
// 存储正在生成分镜图的分镜ID
private generatingShots: Set<number> = new Set();
modelName = "gpt-4.1";
apiKey = "";
baseURL = "";
constructor(projectId: number, scriptId: number) {
this.projectId = projectId;
this.scriptId = scriptId;
}
// 更新shopts
public updatePreShots(segmentId: number, cellId: number, cell: { src?: string; prompt?: string; id?: string }) {
console.log("%c Line:76 🍤 segmentId", "background:#465975", segmentId);
console.log("%c Line:76 🍷 cellId", "background:#ffdd4d", cellId);
console.log("%c Line:76 🍢 cell", "background:#ffdd4d", cell);
const shotIndex = this.shots.findIndex((item) => item.segmentId === segmentId);
if (shotIndex === -1) {
return `分镜 ${segmentId} 不存在请检查分镜ID是否正确`;
}
const cellIndex = this.shots[shotIndex].cells.findIndex((item) => item.id === cellId.toString());
if (cellIndex === -1) {
return `镜头 ${cellId} 不存在请检查镜头ID是否正确`;
}
this.shots[shotIndex].cells[cellIndex] = { ...this.shots[shotIndex].cells[cellIndex], ...cell };
}
// ==================== 公共方法 ====================
get events() {
return this.emitter;
}
// ==================== 私有工具方法 ====================
private emit(event: string, data?: any) {
this.emitter.emit(event, data);
}
private refresh(type: RefreshEvent) {
this.emit("refresh", type);
}
private log(action: string, detail?: string) {
const msg = detail ? `${action}: ${detail}` : action;
console.log(`\n[${new Date().toLocaleTimeString()}] ${msg}\n`);
}
// ==================== 剧本相关操作 ====================
getScript = tool(
async () => {
this.log("获取剧本", `scriptId: ${this.scriptId}`);
const script = await u.db("t_script").where({ id: this.scriptId, projectId: this.projectId }).first();
if (!script) throw new Error("剧本不存在");
return `剧本集:${script.name}\n\n内容\n\`\`\`${script.content}\`\`\``;
},
{
name: "getScript",
description: "获取剧本内容",
schema: z.object({}),
verboseParsingErrors: true,
},
);
// ==================== 资产相关操作 ====================
/**
* segmentAgent shotAgent
*/
getAssets = tool(
async () => {
this.log("获取资产列表", `scriptId: ${this.scriptId}`);
const scriptData = await u.db("t_script").where({ id: this.scriptId, projectId: this.projectId }).first();
const row = await u.db("t_outline").where({ id: scriptData?.outlineId!, projectId: this.projectId }).first();
const outline: any | null = row?.data ? JSON.parse(row.data) : null;
if (!outline) {
return "暂无资产数据";
}
// 提取资源名称和描述与generateImageTool保持一致的字段名
const resources = outline
? (["characters", "props", "scenes"] as const).flatMap(
(k) => outline[k]?.map((i: any) => ({ name: i.name, description: i.description })) ?? [],
)
: [];
if (resources.length === 0) {
return "暂无资产数据";
}
// 分类提取资源并格式化
const characters = outline?.characters?.map((item: any) => `- ${item.name}${item.description ? `${item.description}` : ""}`) ?? [];
const props = outline?.props?.map((item: any) => `- ${item.name}${item.description ? `${item.description}` : ""}`) ?? [];
const scenes = outline?.scenes?.map((item: any) => `- ${item.name}${item.description ? `${item.description}` : ""}`) ?? [];
const sections = [
characters.length ? `【角色】\n${characters.join("\n")}` : "",
props.length ? `【道具】\n${props.join("\n")}` : "",
scenes.length ? `【场景】\n${scenes.join("\n")}` : "",
].filter(Boolean);
if (sections.length === 0) {
return "暂无资产数据";
}
return `<资产列表>
${sections.join("\n\n")}
</资产列表>
1. 使使
2.
3. `;
},
{
name: "getAssets",
description: "获取资产列表(角色、道具、场景),包含名称和详细介绍。生成片段和分镜时必须先调用此工具获取资产信息,确保名称一致性",
schema: z.object({}),
verboseParsingErrors: true,
},
);
// ==================== 片段和分镜工具 ====================
/**
* shotAgent
*/
getSegments = tool(
async () => {
this.log("获取片段数据", `${this.segments.length} 个片段`);
if (this.segments.length === 0) {
return "暂无片段数据,请先调用 segmentAgent 生成片段";
}
return JSON.stringify(this.segments, null, 2);
},
{
name: "getSegments",
description: "获取当前已生成的片段数据,用于生成分镜",
schema: z.object({}),
verboseParsingErrors: true,
},
);
/**
* / segmentAgent
*/
updateSegments = tool(
async ({ segments }: { segments: Segment[] }) => {
this.log("更新片段数据", `${segments.length} 个片段`);
this.segments = segments;
this.emit("segmentsUpdated", this.segments);
return `成功存储 ${segments.length} 个片段`;
},
{
name: "updateSegments",
description: "存储生成的片段数据segmentAgent 在生成片段后必须调用此工具保存结果",
schema: z.object({
segments: z
.array(
z.object({
index: z.number().describe("片段序号"),
description: z.string().describe("片段描述"),
emotion: z.string().optional().describe("情绪氛围"),
action: z.string().optional().describe("主要动作"),
}),
)
.describe("片段数组"),
}),
verboseParsingErrors: true,
},
);
/**
* shotAgent
*/
addShots = tool(
async ({ shots }: { shots: Array<{ segmentIndex: number; prompts: string[] }> }) => {
const added: { id: number; segmentIndex: number }[] = [];
const skipped: number[] = [];
for (const item of shots) {
const exists = this.shots.some((f) => f.segmentId === item.segmentIndex);
if (exists) {
skipped.push(item.segmentIndex);
continue;
}
// 分配独立的分镜ID
this.shotIdCounter++;
const shotId = this.shotIdCounter;
this.shots.push({
id: shotId,
segmentId: item.segmentIndex,
title: `分镜 ${shotId}`,
x: 0,
y: 0,
cells: item.prompts.map((prompt) => ({ id: u.uuid(), prompt })),
});
added.push({ id: shotId, segmentIndex: item.segmentIndex });
}
const addedInfo = added.map((a) => `分镜${a.id}(片段${a.segmentIndex})`).join(", ");
this.log("添加分镜", `新增: [${addedInfo}], 跳过片段: [${skipped.join(", ")}]`);
this.emit("shotsUpdated", this.shots);
if (skipped.length) {
return `已添加${addedInfo};片段 ${skipped.join(", ")} 已存在分镜被跳过。当前共 ${this.shots.length} 个分镜`;
}
return `已添加${addedInfo}。当前共 ${this.shots.length} 个分镜`;
},
{
name: "addShots",
description: "添加新的分镜。每个分镜有独立ID包含多个镜头每个镜头对应一个提示词。如果片段已存在分镜会跳过",
schema: z.object({
shots: z
.array(
z.object({
segmentIndex: z.number().describe("对应的片段序号"),
prompts: z.array(z.string()).describe("镜头提示词数组,每个提示词对应一个镜头(中文)"),
}),
)
.describe("要添加的分镜数组"),
}),
verboseParsingErrors: true,
},
);
/**
* shotAgent
* cells id src prompt
*/
updateShots = tool(
async ({ shotId, prompts }: { shotId: number; prompts: string[] }) => {
const existingIndex = this.shots.findIndex((item) => item.id === shotId);
if (existingIndex === -1) {
return `分镜 ${shotId} 不存在请检查分镜ID是否正确`;
}
const existingCells = this.shots[existingIndex].cells;
// 更新 cells保留原有的 id 和 src 字段
this.shots[existingIndex].cells = prompts.map((prompt, i) => {
const existingCell = existingCells[i];
if (existingCell) {
// 保留原有 cell 的 id 和 src只更新 prompt
return { ...existingCell, prompt };
} else {
// 新增的 cell
return { id: u.uuid(), prompt };
}
});
this.log("更新分镜", `分镜 ${shotId}`);
this.emit("shotsUpdated", this.shots);
return `已更新分镜 ${shotId}`;
},
{
name: "updateShots",
description: "更新指定分镜的镜头提示词。通过分镜ID指定要修改的分镜",
schema: z.object({
shotId: z.number().describe("要更新的分镜ID"),
prompts: z.array(z.string()).describe("新的镜头提示词数组,每个提示词对应一个镜头"),
}),
verboseParsingErrors: true,
},
);
/**
* shotAgent
*/
deleteShots = tool(
async ({ shotIds }: { shotIds: number[] }) => {
const deleted: number[] = [];
const notFound: number[] = [];
for (const shotId of shotIds) {
const idx = this.shots.findIndex((item) => item.id === shotId);
if (idx === -1) {
notFound.push(shotId);
} else {
this.shots.splice(idx, 1);
deleted.push(shotId);
}
}
this.log("删除分镜", `删除: [分镜${deleted.join(", 分镜")}], 未找到: [分镜${notFound.join(", 分镜")}]`);
this.emit("shotsUpdated", this.shots);
if (notFound.length) {
return `已删除分镜 ${deleted.join(", ")};分镜 ${notFound.join(", ")} 不存在。当前共 ${this.shots.length} 个分镜`;
}
return `已删除分镜 ${deleted.join(", ")}。当前共 ${this.shots.length} 个分镜`;
},
{
name: "deleteShots",
description: "删除指定的分镜。通过分镜ID指定要删除的分镜",
schema: z.object({
shotIds: z.array(z.number()).describe("要删除的分镜ID数组"),
}),
verboseParsingErrors: true,
},
);
/**
* 使 nanoBanana
*/
generateShotImage = tool(
async ({ shotIds }: { shotIds: number[] }) => {
const toGenerate: number[] = [];
const alreadyGenerating: number[] = [];
const notFound: number[] = [];
for (const shotId of shotIds) {
const shot = this.shots.find((f) => f.id === shotId);
if (!shot) {
notFound.push(shotId);
continue;
}
if (this.generatingShots.has(shotId)) {
alreadyGenerating.push(shotId);
continue;
}
toGenerate.push(shotId);
}
if (toGenerate.length === 0) {
if (notFound.length) {
return `分镜 ${notFound.join(", ")} 不存在请检查分镜ID是否正确`;
}
if (alreadyGenerating.length) {
return `分镜 ${alreadyGenerating.join(", ")} 正在生成中,请稍候`;
}
return "没有需要生成的分镜";
}
// 标记为正在生成
for (const id of toGenerate) {
this.generatingShots.add(id);
}
// 通知前端开始生成
this.emit("shotImageGenerateStart", { shotIds: toGenerate });
this.log("开始生成分镜图", `分镜: [${toGenerate.join(", ")}]`);
// 异步执行图片生成(不阻塞 Agent 流程)
this.executeShotImageGeneration(toGenerate).catch((err) => {
this.log("分镜图生成错误", err.message);
this.emit("shotImageGenerateError", { shotIds: toGenerate, error: err.message });
});
let result = `已开始为分镜 ${toGenerate.join(", ")} 生成分镜图,生成过程在后台进行`;
if (alreadyGenerating.length) {
result += `;分镜 ${alreadyGenerating.join(", ")} 正在生成中`;
}
if (notFound.length) {
result += `;分镜 ${notFound.join(", ")} 不存在`;
}
return result;
},
{
name: "generateShotImage",
description:
"为指定分镜生成分镜图。每个分镜会根据其所有提示词生成一张完整宫格图然后自动分割为单格图片。通过分镜ID指定不需要指定具体格子整个分镜是一个完整的生成单元",
schema: z.object({
shotIds: z.array(z.number()).describe("要生成分镜图的分镜ID数组"),
}),
verboseParsingErrors: true,
},
);
/**
*
*
*/
async executeShotImageGeneration(shotIds: number[]): Promise<void> {
await Promise.all(shotIds.map((shotId) => this.generateSingleShotImage(shotId)));
}
/**
*
*/
private async generateSingleShotImage(shotId: number): Promise<void> {
try {
const shot = this.shots.find((f) => f.id === shotId);
if (!shot) return;
// 提取所有镜头的有效提示词
const prompts: string[] = shot.cells.map((c) => c.prompt).filter((p): p is string => Boolean(p));
if (prompts.length === 0) {
this.log("跳过分镜图生成", `分镜 ${shotId} 没有有效的镜头提示词`);
this.generatingShots.delete(shotId);
return;
}
// 通知前端正在生成该分镜
this.emit("shotImageGenerateProgress", { shotId, status: "generating", message: "正在调用 AI 生成宫格图片" });
// 根据所有镜头提示词生成宫格图片
const gridImage = await generateImageTool(
prompts.map((p) => ({ prompt: p })),
this.scriptId,
this.projectId,
);
// 通知前端正在分割图片
this.emit("shotImageGenerateProgress", { shotId, status: "splitting", message: "正在分割宫格图片为单张镜头图" });
// 分割宫格图片为单张镜头图片
const imageBuffers = await imageSplitting(gridImage, prompts.length);
// 通知前端正在保存图片
this.emit("shotImageGenerateProgress", { shotId, status: "saving", message: `正在保存 ${imageBuffers.length} 张镜头图片` });
// 保存分割后的镜头图片到 OSS并获取文件路径
const timestamp = Date.now();
const imagePaths: string[] = [];
for (let i = 0; i < imageBuffers.length; i++) {
const fileName = `${this.projectId}/chat/${this.scriptId}/storyboard/shot_${shotId}_take_${i}_${timestamp}.png`;
await u.oss.writeFile(fileName, imageBuffers[i]);
const imageUrl = await u.oss.getFileUrl(fileName);
imagePaths.push(imageUrl);
// 每保存一张镜头图片通知进度
this.emit("shotImageGenerateProgress", {
shotId,
status: "saving",
message: `已保存 ${i + 1}/${imageBuffers.length} 张镜头图片`,
progress: Math.round(((i + 1) / imageBuffers.length) * 100),
});
}
// 更新每个镜头的 src 字段
shot.cells = shot.cells.map((cell, i) => ({
id: u.uuid(),
...cell,
src: imagePaths[i] || cell.src,
}));
// 生成完成后更新状态
this.generatingShots.delete(shotId);
this.emit("shotImageGenerateComplete", { shotId, shot, imagePaths });
this.emit("shotsUpdated", this.shots);
this.log("分镜图生成完成", `分镜 ${shotId},共 ${imagePaths.length} 张镜头图片`);
} catch (err: any) {
this.generatingShots.delete(shotId);
this.emit("shotImageGenerateError", { shotId, error: err.message });
this.log("分镜图生成失败", `分镜 ${shotId}: ${err.message}`);
}
}
// ==================== 公共访问器 ====================
/**
*
*/
getSegmentsData(): Segment[] {
return this.segments;
}
/**
*
*/
getShotsData(): Shot[] {
return this.shots;
}
// ==================== 上下文构建 ====================
private async buildEnvironmentContext(): Promise<string> {
const projectInfo = await u.db("t_project").where({ id: this.projectId }).first();
const row = await u.db("t_outline").where({ id: this.scriptId, projectId: this.projectId }).first();
const outline: any | null = row?.data ? JSON.parse(row.data) : null;
// 分类提取资源名称
const characters = outline?.characters?.map((i: any) => i.name) ?? [];
const props = outline?.props?.map((i: any) => i.name) ?? [];
const scenes = outline?.scenes?.map((i: any) => i.name) ?? [];
const assetList =
[
characters.length ? `【角色】${characters.join("、")}` : "",
props.length ? `【道具】${props.join("、")}` : "",
scenes.length ? `【场景】${scenes.join("、")}` : "",
]
.filter(Boolean)
.join("\n") || "无";
return `<环境信息>
项目ID: ${this.projectId}
系统时间: ${new Date().toLocaleString()}
项目名称: ${projectInfo?.name || "未知"}
项目简介: ${projectInfo?.intro || "无"}
类型: ${projectInfo?.type || "未知"}
风格: ${projectInfo?.artStyle || "未知"}
视频比例: ${projectInfo?.videoRatio || "未知"}
:
${assetList}
</环境信息>`;
}
private buildConversationHistory(): string {
if (!this.history.length) return "无对话历史";
return this.history.map(([role, content]) => `${role}: ${content}`).join("\n\n");
}
private async buildFullContext(task: string): Promise<string> {
const env = await this.buildEnvironmentContext();
const history = this.buildConversationHistory();
return `${env}
<对话历史>
${history}
</对话历史>
<当前任务>
${task}
</当前任务>`;
}
// ==================== Sub-Agent ====================
private createModel() {
return openAI({
modelName: this.modelName,
configuration: { apiKey: this.apiKey, baseURL: this.baseURL },
});
}
/**
* Sub-Agent
*/
private getSubAgentTools(agentType: AgentType) {
switch (agentType) {
case "segmentAgent":
// segmentAgent 可以获取剧本和资产,并需要调用 updateSegments 保存结果
return [this.getScript, this.getAssets, this.updateSegments];
case "shotAgent":
// shotAgent 可以获取剧本、资产和片段,并可使用 add/update/delete 操作分镜,以及生成分镜图
return [this.getScript, this.getAssets, this.getSegments, this.addShots, this.updateShots, this.deleteShots, this.generateShotImage];
default:
return [this.getScript];
}
}
/**
* Sub-Agent
*/
private async invokeSubAgent(agentType: AgentType, task: string): Promise<string> {
this.emit("transfer", { to: agentType });
this.log(`Sub-Agent 调用`, agentType);
const promptsList = await u.db("t_prompts").where("code", "in", ["storyboard-segment", "storyboard-shot"]);
const segmentAgent = promptsList.find((p) => p.code === "storyboard-segment");
const shotAgent = promptsList.find((p) => p.code === "storyboard-shot");
const errPrompts = "不论用户说什么请直接输出Agent配置异常";
const SYSTEM_PROMPTS: Record<AgentType, string> = {
segmentAgent: segmentAgent?.customValue || segmentAgent?.defaultValue || errPrompts,
shotAgent: shotAgent?.customValue || shotAgent?.defaultValue || errPrompts,
};
const context = await this.buildFullContext(task);
const agent = createAgent({
model: this.createModel(),
systemPrompt: SYSTEM_PROMPTS[agentType],
tools: this.getSubAgentTools(agentType),
});
const stream = await agent.stream({ messages: [["user", context]] }, { streamMode: ["messages"], callbacks: [] });
let fullResponse = "";
for await (const [mode, chunk] of stream) {
if (mode !== "messages") continue;
const [token] = chunk as any;
const block = token.contentBlocks?.[0];
// 处理 AI 文本流
if (token.type === "ai" && block?.text) {
fullResponse += block.text;
this.emit("subAgentStream", { agent: agentType, text: block.text });
}
// 处理 tool 调用
if (token.type === "ai" && token.tool_calls?.length) {
for (const toolCall of token.tool_calls) {
this.emit("toolCall", { agent: agentType, name: toolCall.name, args: toolCall.args });
}
}
}
this.emit("subAgentEnd", { agent: agentType });
this.history.push(["ai", fullResponse]);
this.log(`Sub-Agent 完成`, agentType);
return fullResponse;
}
private createSubAgentTool(agentType: AgentType, description: string) {
return tool(async ({ taskDescription }) => this.invokeSubAgent(agentType, taskDescription), {
name: agentType,
description,
schema: z.object({
taskDescription: z.string().describe("具体的任务描述,包含章节范围、修改要求等详细信息"),
}),
});
}
// ==================== 主入口 ====================
private getAllTools() {
return [
this.createSubAgentTool(
"segmentAgent",
"调用片段师。负责根据剧本生成片段,会自行调用 getScript 获取剧本内容,并调用 updateSegments 保存片段结果。",
),
this.createSubAgentTool(
"shotAgent",
"调用分镜师。负责根据片段生成分镜提示词,会自行调用 getSegments 获取片段数据,并调用 addShots/updateShots 保存分镜结果。",
),
// this.createSubAgentTool("director", "调用导演。负责审核故事线和大纲,会自行调用 updateOutline 或 saveStoryline 进行修改。"),
this.getScript,
this.getSegments,
this.generateShotImage,
...this.getSubAgentTools("segmentAgent"),
...this.getSubAgentTools("shotAgent"),
];
}
async call(msg: string): Promise<string> {
console.log("模型名称:", this.modelName);
this.history.push(["user", msg]);
const envContext = await this.buildEnvironmentContext();
const prompts = await u.db("t_prompts").where("code", "storyboard-main").first();
const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么请直接输出Agent配置异常";
const mainAgent = createAgent({
model: this.createModel(),
tools: this.getAllTools(),
systemPrompt: `${envContext}\n${mainPrompts}`,
});
const stream = await mainAgent.stream({ messages: this.history }, { streamMode: ["messages"], callbacks: [] });
let fullResponse = "";
for await (const [mode, chunk] of stream) {
if (mode !== "messages") continue;
const [token] = chunk as any;
const block = token.contentBlocks?.[0];
// 处理 AI 文本流
if (token.type === "ai" && block?.text) {
fullResponse += block.text;
this.emit("data", block.text);
}
// 处理 tool 调用
if (token.type === "ai" && token.tool_calls?.length) {
for (const toolCall of token.tool_calls) {
this.emit("toolCall", { agent: "main", name: toolCall.name, args: toolCall.args });
}
}
}
this.history.push(["assistant", fullResponse]);
this.emit("response", fullResponse);
return fullResponse;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "toonflow-app",
"version": "1.0.7-patch3",
"version": "1.0.7",
"description": "Toonflow 是一款 AI 短剧漫剧工具,能够利用 AI 技术将小说自动转化为剧本,并结合 AI 生成的图片和视频,实现高效的短剧创作。",
"author": "HBAI-Ltd <ltlctools@outlook.com>",
"homepage": "https://github.com/HBAI-Ltd/Toonflow-app#readme",
@ -34,13 +34,15 @@
"license": "bun run scripts/license.ts"
},
"dependencies": {
"@ag-ui/core": "^0.0.47",
"@ai-sdk/anthropic": "^3.0.35",
"@ai-sdk/deepseek": "^2.0.17",
"@ai-sdk/devtools": "^0.0.11",
"@ai-sdk/google": "^3.0.43",
"@ai-sdk/google": "^3.0.20",
"@ai-sdk/openai": "^3.0.25",
"@ai-sdk/openai-compatible": "^2.0.27",
"@ai-sdk/xai": "^3.0.47",
"@huggingface/transformers": "^3.8.1",
"@rmp135/sql-ts": "^2.2.0",
"ai": "^6.0.67",
"axios": "^1.13.2",
@ -61,8 +63,11 @@
"qwen-ai-provider-v5": "^2.1.0",
"serialize-error": "^13.0.1",
"sharp": "^0.34.5",
"socket.io": "^4.8.3",
"sqlite3": "^5.1.7",
"sucrase": "^3.35.1",
"uuid": "^13.0.0",
"vm2": "^3.10.5",
"zhipu-ai-provider": "^0.2.2",
"zod": "^4.3.5"
},

File diff suppressed because one or more lines are too long

View File

@ -1,736 +0,0 @@
// @/agents/outlineScript.ts
import u from "@/utils";
import { EventEmitter } from "events";
import { tool, ModelMessage } from "ai";
import { z } from "zod";
import type { DB } from "@/types/database";
// ==================== 类型定义 ====================
type AgentType = "AI1" | "AI2" | "director";
type AssetType = "角色" | "道具" | "场景";
type RefreshEvent = "storyline" | "outline" | "assets";
interface AssetItem {
name: string;
description: string;
}
interface EpisodeData {
episodeIndex: number;
title: string;
chapterRange: number[];
scenes: AssetItem[]; // 按 outline 出场顺序排列
characters: AssetItem[]; // 按 outline 出场顺序排列
props: AssetItem[]; // 按 outline 出场顺序排列
coreConflict: string;
outline: string; // 最高优先级,剧本生成的唯一权威
openingHook: string; // outline 第一句话的视觉化,开篇第一个镜头
keyEvents: string[]; // 4个元素[起, 承, 转, 合],严格按 outline 顺序
emotionalCurve: string; // 对应 keyEvents 各阶段
visualHighlights: string[]; // 按 outline 顺序排列的标志性镜头
endingHook: string; // outline 之后的悬念延伸
classicQuotes: string[];
}
// ==================== Schema 定义 ====================
const sceneItemSchema = z.object({
name: z.string().describe("场景名称,如'五星酒店宴会厅'、'老旧出租屋'"),
description: z.string().describe("环境描写:空间结构、光线氛围、装饰陈设、环境细节"),
});
const characterItemSchema = z.object({
name: z.string().describe("角色姓名(必须是具体人名,禁止'众人'、'群众'等集合描述)"),
description: z.string().describe("人设样貌:年龄体态、五官特征、发型妆容、服装配饰、气质神态"),
});
const propItemSchema = z.object({
name: z.string().describe("道具名称"),
description: z.string().describe("样式描写:材质质感、颜色图案、形状尺寸、磨损痕迹、特殊标记"),
});
const episodeSchema = z.object({
episodeIndex: z.number().describe("集数索引从1开始递增"),
title: z.string().describe("8字内标题疑问/感叹句,含情绪爆点"),
chapterRange: z.array(z.number()).describe("关联章节号数组"),
scenes: z.array(sceneItemSchema).describe("场景列表,按 outline 出场顺序排列"),
characters: z.array(characterItemSchema).describe("角色列表,按 outline 出场顺序排列,必须是独立个体"),
props: z.array(propItemSchema).describe("道具列表,按 outline 出场顺序排列至少3个"),
coreConflict: z.string().describe("核心矛盾A想要X vs B阻碍X"),
outline: z.string().describe("100-300字剧情主干最高优先级剧本生成的唯一权威按时间顺序完整叙述"),
openingHook: z.string().describe("开场镜头outline 第一句话的视觉化,必须作为剧本第一个镜头"),
keyEvents: z.array(z.string()).length(4).describe("4个元素的数组[起, 承, 转, 合],严格按 outline 顺序从中提取"),
emotionalCurve: z.string().describe("情绪曲线2(压抑)→5(反抗)→9(爆发)→3(余波),对应 keyEvents 各阶段"),
visualHighlights: z.array(z.string()).describe("3-5个标志性镜头按 outline 叙事顺序排列"),
endingHook: z.string().describe("结尾悬念outline 之后的延伸,勾引下集"),
classicQuotes: z.array(z.string()).describe("1-2句金句每句≤15字必须从原文提取"),
});
// ==================== 常量配置 ====================
// ==================== 主类 ====================
export default class OutlineScript {
private readonly projectId: number;
readonly emitter = new EventEmitter();
history: Array<ModelMessage> = [];
novelChapters: DB["t_novel"][] = [];
constructor(projectId: number) {
this.projectId = projectId;
}
// ==================== 公共方法 ====================
get events() {
return this.emitter;
}
setNovel(chapters: DB["t_novel"][]) {
this.novelChapters = chapters;
}
// ==================== 私有工具方法 ====================
private emit(event: string, data?: any) {
this.emitter.emit(event, data);
}
private refresh(type: RefreshEvent) {
this.emit("refresh", type);
}
private log(action: string, detail?: string) {
const msg = detail ? `${action}: ${detail}` : action;
console.log(`\n[${new Date().toLocaleTimeString()}] ${msg}\n`);
}
private safeParseJson<T>(str: string, fallback: T): T {
try {
return JSON.parse(str);
} catch {
return fallback;
}
}
private uniqueByName<T extends { name: string }>(items: T[]): T[] {
return Array.from(new Map(items.map((item) => [item.name, item])).values());
}
// ==================== 数据库操作 ====================
private async getProjectInfo(): Promise<any> {
return u.db("t_project").where({ id: this.projectId }).first();
}
private async getNovelInfo(asString = false): Promise<any> {
const info = await this.getProjectInfo();
if (!info) return asString ? "未查询到项目信息" : null;
if (asString) {
const fields = [
`小说名称: ${info.name}`,
`小说简介: ${info.intro}`,
`小说类型: ${info.type}`,
`目标短剧类型: ${info.artStyle}`,
`短剧画幅: ${info.videoRatio}`,
];
return fields.join("\n");
}
return info;
}
// ==================== 故事线操作 ====================
private async findStoryline() {
return u.db("t_storyline").where({ projectId: this.projectId }).first();
}
private async upsertStorylineContent(content: string) {
const existing = await this.findStoryline();
if (existing) {
await u.db("t_storyline").where({ projectId: this.projectId }).update({ content });
} else {
await u.db("t_storyline").insert({ projectId: this.projectId, content });
}
this.refresh("storyline");
}
private async deleteStorylineContent() {
const deleted = await u.db("t_storyline").where({ projectId: this.projectId }).del();
this.refresh("storyline");
return deleted;
}
// ==================== 大纲操作 ====================
private async findOutlines() {
return u.db("t_outline").where({ projectId: this.projectId }).orderBy("episode", "asc");
}
private async findOutlineById(id: number) {
return u.db("t_outline").where({ id, projectId: this.projectId }).first();
}
private async getMaxEpisode(): Promise<number> {
const result: any = await u.db("t_outline").where({ projectId: this.projectId }).max("episode as max").first();
return result?.max ?? 0;
}
private async clearOutlinesAndScripts() {
const outlines = await u.db("t_outline").select("id").where({ projectId: this.projectId });
if (outlines.length === 0) return 0;
const outlineIds = outlines.map((o) => o.id);
await u.db("t_script").whereIn("outlineId", outlineIds).del();
await u.db("t_outline").where({ projectId: this.projectId }).del();
return outlines.length;
}
private async insertOutlines(episodes: EpisodeData[], startEpisode: number) {
const insertList = episodes.map((ep, idx) => ({
projectId: this.projectId,
data: JSON.stringify({ ...ep, episodeIndex: startEpisode + idx }),
episode: startEpisode + idx,
}));
await u.db("t_outline").insert(insertList);
return insertList.length;
}
private async createEmptyScripts(outlineIds: Array<{ id: number; data: string }>) {
const scripts = outlineIds.map((item) => {
const data = this.safeParseJson<Partial<EpisodeData>>(item.data, {});
return {
name: `${data.episodeIndex ?? ""}`,
content: "",
projectId: this.projectId,
outlineId: item.id,
};
});
if (scripts.length > 0) {
await u.db("t_script").insert(scripts);
}
return scripts.length;
}
private async saveOutlineData(episodes: EpisodeData[], overwrite: boolean, startEpisode?: number) {
if (overwrite) {
const cleared = await this.clearOutlinesAndScripts();
if (cleared > 0) {
this.log("清理旧数据", `删除了 ${cleared} 条大纲及关联剧本`);
}
}
const actualStart = overwrite ? 1 : (startEpisode ?? (await this.getMaxEpisode()) + 1);
const insertedCount = await this.insertOutlines(episodes, actualStart);
const newOutlines = await u
.db("t_outline")
.select("id", "data")
.where({ projectId: this.projectId })
.orderBy("episode", "desc")
.limit(insertedCount);
const scriptCount = await this.createEmptyScripts(newOutlines as Array<{ id: number; data: string }>);
this.refresh("outline");
return { insertedCount, scriptCount };
}
private async updateOutlineData(id: number, data: EpisodeData) {
const existing = await this.findOutlineById(id);
if (!existing) return false;
await u
.db("t_outline")
.where({ id })
.update({ data: JSON.stringify(data) });
this.refresh("outline");
return true;
}
private async deleteOutlineData(ids: number[]) {
const results = await Promise.allSettled(ids.map((id) => u.deleteOutline(id, this.projectId)));
this.refresh("outline");
return results;
}
private formatOutlineDetail(ep: any): string {
const formatList = (items: any[], formatter: (item: any) => string) =>
items?.map((item, i) => ` ${i + 1}. ${formatter(item)}`).join("\n") || " 无";
// keyEvents 按顺序显示:起、承、转、合
const keyEventsLabels = ["起", "承", "转", "合"];
const formatKeyEvents = (events: string[]) => events?.map((e, i) => `${keyEventsLabels[i] || i + 1}${e}`).join("\n") || " 无";
return `
大纲ID: ${ep.id}
${ep.episodeIndex} : ${ep.title || ""}
${"=".repeat(50)}
章节范围: ${ep.chapterRange?.join(", ") || ""}
核心矛盾: ${ep.coreConflict || ""}
():
${ep.outline || "无"}
():
${ep.openingHook || "无"}
():
${formatKeyEvents(ep.keyEvents)}
情绪曲线: ${ep.emotionalCurve || ""}
():
${formatList(ep.visualHighlights, (v) => v)}
:
${ep.endingHook || "无"}
:
${formatList(ep.classicQuotes, (q) => q)}
(): ${ep.characters?.map((c: AssetItem) => `${c.name}(${c.description})`).join("; ") || "无"}
(): ${ep.scenes?.map((s: AssetItem) => `${s.name}(${s.description})`).join("; ") || "无"}
(): ${ep.props?.map((p: AssetItem) => `${p.name}(${p.description})`).join("; ") || "无"}`;
}
private async getOutlineText(simplified: boolean): Promise<string> {
const records = await this.findOutlines();
if (!records.length) return "当前项目暂无大纲";
const episodes = records.map((r) => ({
id: r.id,
episode: r.episode,
...this.safeParseJson<Partial<EpisodeData>>(r.data ?? "{}", {}),
}));
if (simplified) {
const list = episodes.map((ep) => `${ep.episodeIndex ?? ep.episode} 集 (id=${ep.id})`).join("\n");
return `项目大纲 (共 ${episodes.length} 集):\n${list}`;
}
const details = episodes.map((ep) => this.formatOutlineDetail(ep)).join("\n");
return `项目大纲 (共 ${episodes.length} 集)\n\n${details}`;
}
// ==================== 资产操作 ====================
private async findAssetByTypeAndName(type: AssetType, name: string) {
return u.db("t_assets").where({ projectId: this.projectId, type, name }).first();
}
private async upsertAsset(type: AssetType, item: AssetItem): Promise<"inserted" | "updated" | "skipped"> {
const existing = await this.findAssetByTypeAndName(type, item.name);
if (!existing) {
await u.db("t_assets").insert({
projectId: this.projectId,
type,
name: item.name,
intro: item.description,
prompt: item.description,
});
return "inserted";
}
if (existing.intro !== item.description) {
await u.db("t_assets").where({ id: existing.id }).update({
intro: item.description,
prompt: item.description,
});
return "updated";
}
return "skipped";
}
private extractAssetsFromOutlines(outlines: Array<{ data?: string | null | undefined }>): {
characters: AssetItem[];
props: AssetItem[];
scenes: AssetItem[];
} {
const result = { characters: [] as AssetItem[], props: [] as AssetItem[], scenes: [] as AssetItem[] };
for (const outline of outlines) {
const data = this.safeParseJson<Partial<EpisodeData>>(outline.data ?? "{}", {});
if (data.characters) result.characters.push(...data.characters);
if (data.props) result.props.push(...data.props);
if (data.scenes) result.scenes.push(...data.scenes);
}
return {
characters: this.uniqueByName(result.characters),
props: this.uniqueByName(result.props),
scenes: this.uniqueByName(result.scenes),
};
}
private async generateAssetsFromOutlines() {
const outlines = await u.db("t_outline").select("data").where({ projectId: this.projectId });
if (!outlines.length) return { inserted: 0, updated: 0, skipped: 0 };
const { characters, props, scenes } = this.extractAssetsFromOutlines(outlines);
// 只做新增和更新,不做删除
const stats = { inserted: 0, updated: 0, skipped: 0 };
const processItems = async (items: AssetItem[], type: AssetType) => {
for (const item of items) {
const result = await this.upsertAsset(type, item);
stats[result]++;
}
};
await processItems(characters, "角色");
await processItems(props, "道具");
await processItems(scenes, "场景");
this.refresh("assets");
return { ...stats };
}
// ==================== Tool 定义:故事线 ====================
getStoryline = tool({
title: "getStoryline",
description: "Get the weather in a location",
inputSchema: z.object({}),
execute: async () => {
this.log("获取故事线");
const storyline = await this.findStoryline();
return storyline?.content ?? "当前项目暂无故事线";
},
});
saveStoryline = tool({
title: "saveStoryline",
description: "保存或更新当前项目的故事线,会覆盖已有内容",
inputSchema: z.object({
content: z.string().describe("故事线完整内容"),
}),
execute: async ({ content }) => {
this.log("保存故事线");
await this.upsertStorylineContent(content);
return "故事线保存成功";
},
});
deleteStoryline = tool({
title: "deleteStoryline",
description: "删除当前项目的故事线",
inputSchema: z.object({}),
execute: async () => {
this.log("删除故事线");
const deleted = await this.deleteStorylineContent();
return deleted > 0 ? "故事线删除成功" : "当前项目没有故事线";
},
});
// ==================== Tool 定义:大纲 ====================
getOutline = tool({
title: "getOutline",
description: "获取项目大纲。simplified=true返回简化列表false返回完整内容",
inputSchema: z.object({
simplified: z.boolean().default(false).describe("是否返回简化版本"),
}),
execute: async ({ simplified }) => {
this.log("获取大纲", `简化模式: ${simplified}`);
return this.getOutlineText(simplified);
},
});
saveOutline = tool({
title: "saveOutline",
description: "保存大纲数据。overwrite=true会清空现有大纲后写入false则追加到末尾",
inputSchema: z.object({
episodes: z.array(episodeSchema).min(1).describe("大纲数据数组"),
overwrite: z.boolean().default(true).describe("是否覆盖现有大纲"),
startEpisode: z.number().optional().describe("追加模式下的起始集数(不填则自动递增)"),
}),
execute: async ({ episodes, overwrite = true, startEpisode }) => {
this.log("保存大纲", `覆盖模式: ${overwrite}, 集数: ${episodes.length}`);
const { insertedCount, scriptCount } = await this.saveOutlineData(episodes as EpisodeData[], overwrite, startEpisode);
return `大纲保存成功:插入 ${insertedCount} 集大纲,创建 ${scriptCount} 个剧本记录`;
},
});
updateOutline = tool({
title: "updateOutline",
description: "更新指定ID的单集大纲内容",
inputSchema: z.object({
id: z.number().describe("大纲ID"),
data: episodeSchema.describe("更新后的大纲数据"),
}),
execute: async ({ id, data }) => {
this.log("更新大纲", `ID: ${id}`);
const success = await this.updateOutlineData(id, data as EpisodeData);
return success ? `大纲ID ${id} 更新成功` : `未找到大纲ID: ${id}`;
},
});
deleteOutline = tool({
title: "deleteOutline",
description: "根据大纲ID删除指定大纲及关联数据",
inputSchema: z.object({
ids: z.array(z.number()).min(1).describe("要删除的大纲ID数组"),
}),
execute: async ({ ids }) => {
this.log("删除大纲", `IDs: ${ids.join(", ")}`);
const results = await this.deleteOutlineData(ids);
const summary = results.map((r, i) => `ID ${ids[i]}: ${r.status === "fulfilled" ? "成功" : "失败"}`).join(", ");
return `删除结果: ${summary}`;
},
});
// ==================== Tool 定义:章节 ====================
getChapter = tool({
title: "getChapter",
description: "根据章节编号获取小说章节的完整原文内容,支持批量获取",
inputSchema: z.object({
chapterNumbers: z.array(z.number()).min(1).describe("章节编号数组"),
}),
execute: async ({ chapterNumbers }) => {
this.log("获取章节", `章节号: ${chapterNumbers.join(", ")}`);
const results = await Promise.all(
chapterNumbers.map(async (num) => {
const chapter = await u
.db("t_novel")
.where({ projectId: this.projectId, chapterIndex: num })
.select("chapterData", "chapterIndex", "chapter")
.first();
if (chapter) {
return `\n【第${chapter.chapterIndex}${chapter.chapter || ""}\n${chapter.chapterData}`;
}
return `\n【第${num}章】未找到`;
}),
);
return results.join("\n\n---\n");
},
});
// ==================== Tool 定义:资产 ====================
generateAssets = tool({
title: "generateAssets",
description: "从当前项目的所有大纲中提取并生成角色、道具、场景资产,自动去重并清理冗余",
inputSchema: z.object({}),
execute: async () => {
this.log("生成资产");
const stats = await this.generateAssetsFromOutlines();
if (stats.inserted === 0 && stats.updated === 0 && stats.skipped === 0) {
return "当前项目没有大纲数据,无法生成资产";
}
return `资产生成完成:新增 ${stats.inserted},更新 ${stats.updated},保持 ${stats.skipped}`;
},
});
// ==================== 上下文构建 ====================
private getChapterContext(): string {
if (!this.novelChapters.length) return "无章节数据";
return this.novelChapters.map((c) => `章节号:${c.chapterIndex},分卷:${c.reel},章节名:${c.chapter}`).join("\n");
}
private async buildEnvironmentContext(): Promise<string> {
const [novelInfo, storyline, outlineCount] = await Promise.all([
this.getNovelInfo(true),
this.findStoryline(),
u.db("t_outline").where({ projectId: this.projectId }).count("id as count").first() as any,
]);
return `<环境信息>
项目ID: ${this.projectId}
系统时间: ${new Date().toLocaleString()}
${novelInfo}
:
${this.getChapterContext()}
故事线状态: ${storyline ? "已生成" : "未生成"}
大纲状态: ${outlineCount?.count ?? 0}
:
- getChapter: 获取章节原文
- getStoryline/saveStoryline/deleteStoryline: 故事线操作
- getOutline/saveOutline/updateOutline/deleteOutline: 大纲操作
- generateAssets: 从大纲生成资产
</环境信息>`;
}
private buildConversationHistory(): string {
if (!this.history.length) return "无对话历史";
return this.history.map(({ role, content }) => `${role}: ${content}`).join("\n\n");
}
private async buildFullContext(task: string): Promise<string> {
const env = await this.buildEnvironmentContext();
const history = this.buildConversationHistory();
return `${env}
<对话历史>
${history}
</对话历史>
<当前任务>
${task}
</当前任务>`;
}
// ==================== Sub-Agent ====================
private getSubAgentTools() {
return {
getChapter: this.getChapter,
getStoryline: this.getStoryline,
saveStoryline: this.saveStoryline,
getOutline: this.getOutline,
saveOutline: this.saveOutline,
updateOutline: this.updateOutline,
};
}
/**
* Sub-Agent
*/
private async invokeSubAgent(agentType: AgentType, task: string): Promise<string> {
this.emit("transfer", { to: agentType });
this.log(`Sub-Agent 调用`, agentType);
const promptsList = await u.db("t_prompts").where("code", "in", ["outlineScript-a1", "outlineScript-a2", "outlineScript-director"]);
const promptConfig = await u.getPromptAi("outlineScriptAgent");
const errPrompts = "不论用户说什么请直接输出Agent配置异常";
const getAiPromptConfig = (code: string) => {
const item = promptsList.find((p) => p.code === code);
return item?.customValue || item?.defaultValue || errPrompts;
};
const a1Prompt = getAiPromptConfig("outlineScript-a1");
const a2Prompt = getAiPromptConfig("outlineScript-a2");
const directorPrompt = getAiPromptConfig("outlineScript-director");
const SYSTEM_PROMPTS = {
AI1: a1Prompt,
AI2: a2Prompt,
director: directorPrompt,
};
const context = await this.buildFullContext(task);
const { fullStream } = await u.ai.text.stream(
{
system: SYSTEM_PROMPTS[agentType],
tools: this.getSubAgentTools(),
messages: [{ role: "user", content: context }],
maxStep: 100,
},
promptConfig,
);
let fullResponse = "";
for await (const item of fullStream) {
if (item.type == "tool-call") {
this.emit("toolCall", { agent: "main", name: item.title, args: null });
}
if (item.type == "text-delta") {
fullResponse += item.text;
this.emit("subAgentStream", { agent: agentType, text: item.text });
}
}
this.emit("subAgentEnd", { agent: agentType });
this.history.push({
role: "assistant",
content: fullResponse,
});
this.log(`Sub-Agent 完成`, agentType);
return fullResponse ?? `${agentType}已完成任务`;
}
private createSubAgentTool(agentType: AgentType, description: string) {
return tool({
title: agentType,
description,
inputSchema: z.object({
taskDescription: z.string().describe("具体的任务描述,包含章节范围、修改要求等详细信息"),
}),
execute: async ({ taskDescription }) => this.invokeSubAgent(agentType, taskDescription),
});
}
// ==================== 主入口 ====================
private getAllTools() {
return {
AI1: this.createSubAgentTool("AI1", "调用故事师。负责分析小说原文并生成故事线,会自行调用 saveStoryline 保存结果。"),
AI2: this.createSubAgentTool("AI2", "调用大纲师。负责根据故事线生成剧集大纲,会自行调用 saveOutline 保存结果。"),
director: this.createSubAgentTool("director", "调用导演。负责审核故事线和大纲,会自行调用 updateOutline 或 saveStoryline 进行修改。"),
getChapter: this.getChapter,
getStoryline: this.getStoryline,
saveStoryline: this.saveStoryline,
deleteStoryline: this.deleteStoryline,
getOutline: this.getOutline,
saveOutline: this.saveOutline,
updateOutline: this.updateOutline,
deleteOutline: this.deleteOutline,
generateAssets: this.generateAssets,
};
}
async call(msg: string): Promise<string> {
this.history.push({
role: "user",
content: msg,
});
const envContext = await this.buildEnvironmentContext();
const prompts = await u.db("t_prompts").where("code", "outlineScript-main").first();
const promptConfig = await u.getPromptAi("outlineScriptAgent");
const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么请直接输出Agent配置异常";
const { fullStream } = await u.ai.text.stream(
{
system: `${envContext}\n${mainPrompts}`,
tools: this.getAllTools(),
messages: this.history,
maxStep: 100,
},
promptConfig,
);
let fullResponse = "";
for await (const item of fullStream) {
if (item.type == "tool-call") {
this.emit("toolCall", { agent: "main", name: item.title, args: null });
}
if (item.type == "text-delta") {
fullResponse += item.text;
this.emit("data", item.text);
}
}
this.history.push({
role: "assistant",
content: fullResponse,
});
this.emit("response", fullResponse);
return fullResponse;
}
}

View File

@ -1,146 +0,0 @@
import u from "@/utils";
type AspectRatio = "16:9" | "9:16" | "21:9" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3";
interface GridLayoutResult {
cols: number;
rows: number;
totalCells: number;
placeholderCount: number;
}
interface GridPromptOptions {
prompts: string[];
style: string;
aspectRatio: AspectRatio;
assetsName: { name: string; intro: string }[];
}
interface GridPromptResult {
prompt: string;
gridLayout: GridLayoutResult;
}
/**
* prompts数量计算宫格布局
*/
function calculateGridLayout(count: number): GridLayoutResult {
let cols: number;
let rows: number;
if (count <= 0) {
cols = 1;
rows = 1;
} else if (count === 1) {
cols = 1;
rows = 1;
} else if (count === 2) {
cols = 2;
rows = 1;
} else if (count === 3) {
cols = 3;
rows = 1;
} else if (count === 4) {
cols = 2;
rows = 2;
} else if (count <= 9) {
cols = 3;
rows = 3;
} else {
cols = 3;
rows = Math.ceil(count / 3);
}
const totalCells = cols * rows;
const placeholderCount = totalCells - count;
return { cols, rows, totalCells, placeholderCount };
}
/**
*
*/
function getAspectRatioDescription(aspectRatio: AspectRatio): string {
const descriptions: Record<AspectRatio, string> = {
"16:9": "电影宽银幕",
"9:16": "竖屏短剧",
"21:9": "超宽银幕史诗感",
"1:1": "方形构图",
"4:3": "经典银幕",
"3:4": "竖版经典",
"3:2": "摄影标准",
"2:3": "竖版摄影",
};
return descriptions[aspectRatio] || "标准比例";
}
/**
*
*/
async function generateGridPrompt(options: GridPromptOptions): Promise<GridPromptResult> {
const { prompts, style, aspectRatio, assetsName } = options;
const layout = calculateGridLayout(prompts.length);
const aspectRatioDesc = getAspectRatioDescription(aspectRatio);
// 构建宫格位置描述
const gridPositions: string[] = [];
for (let i = 0; i < layout.totalCells; i++) {
const row = Math.floor(i / layout.cols) + 1;
const col = (i % layout.cols) + 1;
if (i < prompts.length) {
gridPositions.push(`[第${row}行第${col}列]: ${prompts[i]}`);
} else {
gridPositions.push(`[第${row}行第${col}列]: 纯黑图`);
}
}
// 构建资产说明
const assetsSection =
assetsName.length > 0
? `\n【可用资产】\n${assetsName.map((a) => `- ${a.name}${a.intro}`).join("\n")}\n\n⚠ 必须使用完整资产名称,禁止简称或代词。`
: "";
const promptsData = await u.db("t_prompts").where("code", "generateImagePrompts").first();
const promptAiConfig = await u.getPromptAi("storyboardAgent");
const mainPrompts = promptsData?.customValue || promptsData?.defaultValue;
const errData = `请输出${options.prompts.length}张图片\n提示词如下:\n${options.prompts.map((p, i) => `${i + 1}格: ${p}`).join("\n")}`;
if (!mainPrompts) return { prompt: errData, gridLayout: layout };
const result = await u.ai.text.invoke(
{
messages: [
{
role: "system",
content: mainPrompts,
},
{
role: "user",
content: `请优化以下分镜提示词:\n\n【布局】${layout.cols}列×${layout.rows}行=${
layout.totalCells
}\n${aspectRatio}${aspectRatioDesc}\n${style}\n${assetsSection}\n\n\n${gridPositions.join("\n")}`,
},
],
},
promptAiConfig,
);
// const result = await chatModel!.invoke({
// messages: [
// {
// role: "system",
// content: mainPrompts,
// },
// {
// role: "user",
// content: `请优化以下分镜提示词:\n\n【布局】${layout.cols}列×${layout.rows}行=${
// layout.totalCells
// }格\n【比例】${aspectRatio}${aspectRatioDesc}\n【风格】${style}\n${assetsSection}\n\n【原始内容】\n${gridPositions.join("\n")}`,
// },
// ],
// });
return {
prompt: result.text ?? errData,
gridLayout: layout,
};
}
export default generateGridPrompt;

View File

@ -1,337 +0,0 @@
import generateImagePromptsTool from "@/agents/storyboard/generateImagePromptsTool";
import u from "@/utils";
import sharp from "sharp";
import { z } from "zod";
interface AssetItem {
name: string;
description: string;
}
interface EpisodeData {
episodeIndex: number;
title: string;
chapterRange: number[];
scenes: AssetItem[];
characters: AssetItem[];
props: AssetItem[];
coreConflict: string;
openingHook: string;
outline: string;
keyEvents: string[];
emotionalCurve: string;
visualHighlights: string[];
endingHook: string;
classicQuotes: string[];
}
interface ImageInfo {
name: string;
type: string;
filePath: string;
}
interface ResourceItem {
name: string;
intro: string;
}
// 压缩图片直到不超过指定大小
async function compressImage(buffer: Buffer, maxSizeBytes: number = 3 * 1024 * 1024): Promise<Buffer> {
if (buffer.length <= maxSizeBytes) {
return buffer;
}
let quality = 90;
let compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
while (compressedBuffer.length > maxSizeBytes && quality > 10) {
quality -= 10;
compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
}
if (compressedBuffer.length > maxSizeBytes) {
const metadata = await sharp(buffer).metadata();
let scale = 0.9;
while (compressedBuffer.length > maxSizeBytes && scale > 0.1) {
const newWidth = Math.round((metadata.width || 1000) * scale);
const newHeight = Math.round((metadata.height || 1000) * scale);
compressedBuffer = await sharp(buffer)
.resize(newWidth, newHeight, { fit: "inside" })
.jpeg({ quality: Math.max(quality, 30) })
.toBuffer();
scale -= 0.1;
}
}
return compressedBuffer;
}
// 拼接多张图片为一张
async function mergeImages(imagePaths: string[]): Promise<Buffer> {
const imageBuffers = await Promise.all(imagePaths.map((path) => u.oss.getFile(path)));
const imageMetadatas = await Promise.all(imageBuffers.map((buffer) => sharp(buffer).metadata()));
const maxHeight = Math.max(...imageMetadatas.map((m) => m.height || 0));
const resizedImages = await Promise.all(
imageBuffers.map(async (buffer, index) => {
const metadata = imageMetadatas[index];
const aspectRatio = (metadata.width || 1) / (metadata.height || 1);
const newWidth = Math.round(maxHeight * aspectRatio);
return {
buffer: await sharp(buffer).resize(newWidth, maxHeight, { fit: "cover" }).toBuffer(),
width: newWidth,
};
}),
);
let currentX = 0;
const compositeInputs = resizedImages.map(({ buffer, width }) => {
const input = {
input: buffer,
left: currentX,
top: 0,
};
currentX += width;
return input;
});
const mergedImage = await sharp({
create: {
width: currentX,
height: maxHeight,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 1 },
},
})
.composite(compositeInputs)
.jpeg({ quality: 90 })
.toBuffer();
return compressImage(mergedImage);
}
// 进一步压缩单张图片到指定大小
async function compressToSize(buffer: Buffer, targetSize: number): Promise<Buffer> {
if (buffer.length <= targetSize) {
return buffer;
}
const metadata = await sharp(buffer).metadata();
let quality = 80;
let scale = 1.0;
let compressedBuffer = buffer;
// 先尝试降低质量
while (compressedBuffer.length > targetSize && quality > 10) {
compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
quality -= 10;
}
// 如果还是太大,缩小尺寸
while (compressedBuffer.length > targetSize && scale > 0.2) {
scale -= 0.1;
const newWidth = Math.round((metadata.width || 1000) * scale);
const newHeight = Math.round((metadata.height || 1000) * scale);
compressedBuffer = await sharp(buffer)
.resize(newWidth, newHeight, { fit: "inside" })
.jpeg({ quality: Math.max(quality, 20) })
.toBuffer();
}
return compressedBuffer;
}
// 确保图片列表总大小不超过指定限制
async function ensureTotalSizeLimit(buffers: Buffer[], maxTotalBytes: number = 10 * 1024 * 1024): Promise<Buffer[]> {
let totalSize = buffers.reduce((sum, buf) => sum + buf.length, 0);
if (totalSize <= maxTotalBytes) {
return buffers;
}
// 计算每张图片的平均目标大小
const avgTargetSize = Math.floor(maxTotalBytes / buffers.length);
// 按大小降序排列,优先压缩大图片
const indexedBuffers = buffers.map((buf, idx) => ({ buf, idx, size: buf.length }));
indexedBuffers.sort((a, b) => b.size - a.size);
const result = [...buffers];
for (const item of indexedBuffers) {
totalSize = result.reduce((sum, buf) => sum + buf.length, 0);
if (totalSize <= maxTotalBytes) {
break;
}
// 计算这张图片需要压缩到的目标大小
const excessSize = totalSize - maxTotalBytes;
const targetSize = Math.max(item.buf.length - excessSize, avgTargetSize, 100 * 1024); // 最小100KB
if (item.buf.length > targetSize) {
result[item.idx] = await compressToSize(item.buf, targetSize);
}
}
return result;
}
// 处理图片列表确保不超过10张且每张不超过3MB总大小不超过10MB
async function processImages(images: ImageInfo[]): Promise<Buffer[]> {
const maxImages = 10;
let processedBuffers: Buffer[];
if (images.length <= maxImages) {
const buffers = await Promise.all(images.map((img) => u.oss.getFile(img.filePath)));
processedBuffers = await Promise.all(buffers.map((buffer) => compressImage(buffer)));
} else {
const mergeStartIndex = maxImages - 1;
const firstBuffers = await Promise.all(images.slice(0, mergeStartIndex).map((img) => u.oss.getFile(img.filePath)));
const compressedFirstImages = await Promise.all(firstBuffers.map((buffer) => compressImage(buffer)));
const imagesToMergeList = images.slice(mergeStartIndex).map((img) => img.filePath);
const mergedImage = await mergeImages(imagesToMergeList);
processedBuffers = [...compressedFirstImages, mergedImage];
}
// 确保总大小不超过10MB
return ensureTotalSizeLimit(processedBuffers);
}
// 使用 AI 过滤与分镜相关的资产
async function filterRelevantAssets(prompts: string[], allResources: ResourceItem[], availableImages: ImageInfo[]): Promise<ImageInfo[]> {
if (allResources.length === 0 || availableImages.length === 0) {
return availableImages;
}
const availableNames = new Set(availableImages.map((img) => img.name));
const availableResources = allResources.filter((r) => availableNames.has(r.name));
if (availableResources.length === 0) {
return availableImages;
}
const apiConfig = await u.getPromptAi("storyboardAgent");
const { relevantAssets } = await u.ai.text.invoke(
{
messages: [
{
role: "user",
content: `请分析以下分镜描述,从可用资产中筛选出与分镜内容直接相关的资产。
${prompts.map((p, i) => `${i + 1}. ${p}`).join("\n")}
${availableResources.map((r) => `- ${r.name}${r.intro}`).join("\n")}
`,
},
],
output: {
relevantAssets: z
.array(
z.object({
name: z.string().describe("资产名称"),
reason: z.string().describe("选择该资产的原因"),
}),
)
.describe("与分镜内容相关的资产列表"),
},
},
apiConfig,
);
if (!relevantAssets || relevantAssets.length === 0) {
return availableImages;
}
const relevantNames = new Set(relevantAssets.map((a) => a.name));
const filteredImages = availableImages.filter((img) => relevantNames.has(img.name));
return filteredImages.length > 0 ? filteredImages : availableImages;
}
// 构建资产映射提示词
function buildResourcesMapPrompts(images: ImageInfo[]): string {
if (images.length === 0) return "";
const mapping = images.map((item, index) => {
if (index < 9) {
return `${item.name}=图片${index + 1}`;
} else {
return `${item.name}=图10-${index - 8}`;
}
});
return `其中人物、场景、道具参考对照关系如下:${mapping.join(", ")}`;
}
export default async (cells: { prompt: string }[], scriptId: number, projectId: number) => {
const scriptData = await u.db("t_script").where({ id: scriptId, projectId }).first();
const projectInfo = await u.db("t_project").where({ id: projectId }).first();
const row = await u.db("t_outline").where({ id: scriptData?.outlineId!, projectId }).first();
const outline: EpisodeData | null = row?.data ? JSON.parse(row.data) : null;
const resources: ResourceItem[] = outline
? (["characters", "props", "scenes"] as const).flatMap((k) => outline[k]?.map((i) => ({ name: i.name, intro: i.description })) ?? [])
: [];
const resourceNames = resources.map((r) => r.name);
const imagesRaw = await u.db("t_assets").whereIn("name", resourceNames).andWhere({ projectId }).select("name", "type", "filePath");
const allImages = imagesRaw
.sort((a, b) => {
const order = ["角色", "场景", "道具"];
return order.indexOf(a.type!) - order.indexOf(b.type!);
})
.filter((img) => img.filePath) as ImageInfo[];
if (allImages.length === 0) {
throw new Error("未找到可用的图片资源");
}
const cellPrompts = cells.map((c) => c.prompt);
// 使用 AI 过滤相关资产
const filteredImages = await filterRelevantAssets(cellPrompts, resources, allImages);
const resourcesMapPrompts = buildResourcesMapPrompts(filteredImages);
const promptsData = await generateImagePromptsTool({
prompts: cellPrompts,
style: `类型:${projectInfo?.type!},风格:${projectInfo?.artStyle!}`,
aspectRatio: projectInfo?.videoRatio! as any,
assetsName: resources,
});
// const prompts = `请生成${promptsData.gridLayout.totalCells}格,${promptsData.gridLayout.cols}列×${promptsData.gridLayout.rows}行宫格图。
// ${promptsData.prompt}
// 注意:请严格按照提示词内容生成图片,确保人物样貌、艺术风格、色调光影一致。
// `;
const prompts = promptsData.prompt;
const processedImages = await processImages(filteredImages);
const apiConfig = await u.getPromptAi("storyboardImage");
const contentStr = await u.ai.image(
{
systemPrompt: resourcesMapPrompts,
prompt: prompts,
size: "4K",
aspectRatio: projectInfo?.videoRatio ? (projectInfo.videoRatio as any) : "16:9",
imageBase64: processedImages.map((buf) => buf.toString("base64")),
taskClass: "分镜图生成",
name: `分镜图-${outline?.title || "未知剧集"}`,
describe: prompts,
projectId,
},
apiConfig,
);
const match = contentStr.match(/base64,([A-Za-z0-9+/=]+)/);
const base64Str = match?.[1] ?? contentStr;
const buffer = Buffer.from(base64Str, "base64");
return buffer;
};

View File

@ -1,94 +0,0 @@
import sharp from "sharp";
interface GridLayoutResult {
cols: number;
rows: number;
totalCells: number;
placeholderCount: number;
}
/**
*
* 1张: 1x1
* 2张: 2x1
* 3张: 3x1
* 4张: 2x2
* 5-9张: 3x3
* 10-12张: 3x4
* 13-15张: 3x5
* ...3
*/
function calculateGridLayout(count: number): GridLayoutResult {
let cols: number;
let rows: number;
if (count <= 0) {
cols = 1;
rows = 1;
} else if (count === 1) {
cols = 1;
rows = 1;
} else if (count === 2) {
cols = 2;
rows = 1;
} else if (count === 3) {
cols = 3;
rows = 1;
} else if (count === 4) {
cols = 2;
rows = 2;
} else if (count <= 9) {
// 5-9格统一用3x3
cols = 3;
rows = 3;
} else {
cols = 3;
rows = Math.ceil(count / 3);
}
const totalCells = cols * rows;
const placeholderCount = totalCells - count;
return { cols, rows, totalCells, placeholderCount };
}
/**
*
* @param image - Buffer
* @param length -
* @returns Buffer
*/
export default async (image: Buffer, length: number): Promise<Buffer[]> => {
const metadata = await sharp(image).metadata();
const { width: totalWidth, height: totalHeight } = metadata;
if (!totalWidth || !totalHeight) {
throw new Error("无法获取图片尺寸");
}
const { cols, rows } = calculateGridLayout(length);
const cellWidth = Math.floor(totalWidth / cols);
const cellHeight = Math.floor(totalHeight / rows);
const buffers: Buffer[] = [];
for (let i = 0; i < length; i++) {
const row = Math.floor(i / cols);
const col = i % cols;
const left = col * cellWidth;
const top = row * cellHeight;
const cellBuffer = await sharp(image)
.extract({
left,
top,
width: cellWidth,
height: cellHeight,
})
.png()
.toBuffer();
buffers.push(cellBuffer);
}
return buffers;
};

View File

@ -1,734 +0,0 @@
// @/agents/Storyboard.ts
import u from "@/utils";
import { tool, ModelMessage, Tool } from "ai";
import { EventEmitter } from "events";
import { z } from "zod";
import type { DB } from "@/types/database";
import generateImageTool from "./generateImageTool";
import imageSplitting from "./imageSplitting";
import path from "path";
import sharp from "sharp";
// ==================== 类型定义 ====================
type AgentType = "segmentAgent" | "shotAgent";
type RefreshEvent = "storyline" | "outline" | "assets";
// ==================== 常量配置 ====================
// const SYSTEM_PROMPTS: Record<AgentType, string> = {
// segmentAgent: segmentPrompts,
// shotAgent: shotPrompts,
// director: directorPrompts,
// };
// ==================== 类型定义:片段和画面 ====================
interface Segment {
index: number;
description: string;
emotion?: string;
action?: string;
}
interface Shot {
id: number; // 分镜独立ID
segmentId: number; // 所属片段ID
title: string;
x: number;
y: number;
cells: Array<{ src?: string; prompt?: string; id?: string }>; // 镜头数组每个cell是一个镜头
fragmentContent: string;
assetsTags: AssetsType[];
}
interface AssetsType {
type: "role" | "props" | "scene";
text: string;
}
// ==================== 主类 ====================
export default class Storyboard {
private readonly projectId: number;
private readonly scriptId: number;
readonly emitter = new EventEmitter();
history: ModelMessage[] = [];
novelChapters: DB["t_novel"][] = [];
// 存储 segmentAgent 生成的片段结果
private segments: Segment[] = [];
// 存储 shotAgent 生成的分镜结果
private shots: Shot[] = [];
// 分镜ID计数器
private shotIdCounter: number = 0;
// 存储正在生成分镜图的分镜ID
private generatingShots: Set<number> = new Set();
constructor(projectId: number, scriptId: number) {
this.projectId = projectId;
this.scriptId = scriptId;
}
// 更新shopts
public updatePreShots(segmentId: number, cellId: number, cell: { src?: string; prompt?: string; id?: string }) {
const shotIndex = this.shots.findIndex((item) => item.segmentId === segmentId);
if (shotIndex === -1) {
return `分镜 ${segmentId} 不存在请检查分镜ID是否正确`;
}
const cellIndex = this.shots[shotIndex].cells.findIndex((item) => item.id === cellId.toString());
if (cellIndex === -1) {
return `镜头 ${cellId} 不存在请检查镜头ID是否正确`;
}
this.shots[shotIndex].cells[cellIndex] = { ...this.shots[shotIndex].cells[cellIndex], ...cell };
}
// ==================== 公共方法 ====================
get events() {
return this.emitter;
}
// ==================== 私有工具方法 ====================
private emit(event: string, data?: any) {
this.emitter.emit(event, data);
}
private refresh(type: RefreshEvent) {
this.emit("refresh", type);
}
private log(action: string, detail?: string) {
const msg = detail ? `${action}: ${detail}` : action;
}
// ==================== 剧本相关操作 ====================
getScript = tool({
title: "getScript",
description: "用于获取剧本内容",
inputSchema: z.object({}),
execute: async () => {
this.log("获取剧本", `scriptId: ${this.scriptId}`);
const script = await u.db("t_script").where({ id: this.scriptId, projectId: this.projectId }).first();
if (!script) throw new Error("剧本不存在");
return `剧本集:${script.name}\n\n内容\n\`\`\`${script.content}\`\`\``;
},
});
// ==================== 资产相关操作 ====================
/**
* segmentAgent shotAgent
*/
getAssets = tool({
title: "getAssets",
description: "获取资产列表(角色、道具、场景),包含名称和详细介绍。生成片段和分镜时必须先调用此工具获取资产信息,确保名称一致性",
inputSchema: z.object({}),
execute: async () => {
this.log("获取资产列表", `scriptId: ${this.scriptId}`);
const scriptData = await u.db("t_script").where({ id: this.scriptId, projectId: this.projectId }).first();
const row = await u.db("t_outline").where({ id: scriptData?.outlineId!, projectId: this.projectId }).first();
const outline: any | null = row?.data ? JSON.parse(row.data) : null;
if (!outline) {
return "暂无资产数据";
}
// 提取资源名称和描述与generateImageTool保持一致的字段名
const resources = outline
? (["characters", "props", "scenes"] as const).flatMap(
(k) => outline[k]?.map((i: any) => ({ name: i.name, description: i.description })) ?? [],
)
: [];
if (resources.length === 0) {
return "暂无资产数据";
}
// 分类提取资源并格式化
const characters = outline?.characters?.map((item: any) => `- ${item.name}${item.description ? `${item.description}` : ""}`) ?? [];
const props = outline?.props?.map((item: any) => `- ${item.name}${item.description ? `${item.description}` : ""}`) ?? [];
const scenes = outline?.scenes?.map((item: any) => `- ${item.name}${item.description ? `${item.description}` : ""}`) ?? [];
const sections = [
characters.length ? `【角色】\n${characters.join("\n")}` : "",
props.length ? `【道具】\n${props.join("\n")}` : "",
scenes.length ? `【场景】\n${scenes.join("\n")}` : "",
].filter(Boolean);
if (sections.length === 0) {
return "暂无资产数据";
}
return `<资产列表>
${sections.join("\n\n")}
</资产列表>
1. 使使
2.
3. `;
},
});
// ==================== 片段和分镜工具 ====================
/**
* shotAgent
*/
getSegments = tool({
title: "getSegments",
description: "获取当前已生成的片段数据,用于生成分镜",
inputSchema: z.object({}),
execute: async () => {
this.log("获取片段数据", `${this.segments.length} 个片段`);
if (this.segments.length === 0) {
return "暂无片段数据,请先调用 segmentAgent 生成片段";
}
return JSON.stringify(this.segments, null, 2);
},
});
/**
* / segmentAgent
*/
updateSegments = tool({
title: "updateSegments",
description: "存储生成的片段数据segmentAgent 在生成片段后必须调用此工具保存结果",
inputSchema: z.object({
segments: z
.array(
z.object({
index: z.number().describe("片段序号"),
description: z.string().describe("片段描述"),
emotion: z.string().optional().describe("情绪氛围"),
action: z.string().optional().describe("主要动作"),
}),
)
.describe("片段数组"),
}),
execute: async ({ segments }: { segments: Segment[] }) => {
this.log("更新片段数据", `${segments.length} 个片段`);
this.segments = segments;
this.emit("segmentsUpdated", this.segments);
return `成功存储 ${segments.length} 个片段`;
},
});
/**
* shotAgent
*/
addShots = tool({
title: "addShots",
description: "添加新的分镜。每个分镜有独立ID包含多个镜头每个镜头对应一个提示词。如果片段已存在分镜会跳过",
inputSchema: z.object({
shots: z
.array(
z.object({
segmentIndex: z.number().describe("对应的片段序号"),
prompts: z.array(z.string()).describe("镜头提示词数组,每个提示词对应一个镜头(中文)"),
assetsTags: z.array(
z.object({
type: z.enum(["role", "props", "scene"]).describe("资源类型"),
text: z.string().describe("资源名称"),
}),
),
}),
)
.describe("要添加的分镜数组"),
}),
execute: async ({ shots }: { shots: Array<{ segmentIndex: number; prompts: string[]; assetsTags: AssetsType[] }> }) => {
const added: { id: number; segmentIndex: number }[] = [];
const skipped: number[] = [];
for (const item of shots) {
const exists = this.shots.some((f) => f.segmentId === item.segmentIndex);
if (exists) {
skipped.push(item.segmentIndex);
continue;
}
// 分配独立的分镜ID
this.shotIdCounter++;
const shotId = this.shotIdCounter;
this.shots.push({
id: shotId,
segmentId: item.segmentIndex,
title: `分镜 ${shotId}`,
x: 0,
y: 0,
cells: item.prompts.map((prompt) => ({ id: u.uuid(), prompt })),
fragmentContent: this.segments[item.segmentIndex - 1]?.description,
assetsTags: item.assetsTags,
});
added.push({ id: shotId, segmentIndex: item.segmentIndex });
}
const addedInfo = added.map((a) => `分镜${a.id}(片段${a.segmentIndex})`).join(", ");
this.log("添加分镜", `新增: [${addedInfo}], 跳过片段: [${skipped.join(", ")}]`);
this.emit("shotsUpdated", this.shots);
if (skipped.length) {
return `已添加${addedInfo};片段 ${skipped.join(", ")} 已存在分镜被跳过。当前共 ${this.shots.length} 个分镜`;
}
return `已添加${addedInfo}。当前共 ${this.shots.length} 个分镜`;
},
});
/**
* shotAgent
* cells id src prompt
*/
updateShots = tool({
title: "updateShots",
description: "更新指定分镜的镜头提示词。通过分镜ID指定要修改的分镜",
inputSchema: z.object({
shotId: z.number().describe("要更新的分镜ID"),
prompts: z.array(z.string()).describe("新的镜头提示词数组,每个提示词对应一个镜头"),
}),
execute: async ({ shotId, prompts }: { shotId: number; prompts: string[] }) => {
const existingIndex = this.shots.findIndex((item) => item.id === shotId);
if (existingIndex === -1) {
return `分镜 ${shotId} 不存在请检查分镜ID是否正确`;
}
const existingCells = this.shots[existingIndex].cells;
// 更新 cells保留原有的 id 和 src 字段
this.shots[existingIndex].cells = prompts.map((prompt, i) => {
const existingCell = existingCells[i];
if (existingCell) {
// 保留原有 cell 的 id 和 src只更新 prompt
return { ...existingCell, prompt };
} else {
// 新增的 cell
return { id: u.uuid(), prompt };
}
});
this.log("更新分镜", `分镜 ${shotId}`);
this.emit("shotsUpdated", this.shots);
return `已更新分镜 ${shotId}`;
},
});
/**
* shotAgent
*/
deleteShots = tool({
title: "deleteShots",
description: "删除指定的分镜。通过分镜ID指定要删除的分镜",
inputSchema: z.object({
shotIds: z.array(z.number()).describe("要删除的分镜ID数组"),
}),
execute: async ({ shotIds }: { shotIds: number[] }) => {
const deleted: number[] = [];
const notFound: number[] = [];
for (const shotId of shotIds) {
const idx = this.shots.findIndex((item) => item.id === shotId);
if (idx === -1) {
notFound.push(shotId);
} else {
this.shots.splice(idx, 1);
deleted.push(shotId);
}
}
this.log("删除分镜", `删除: [分镜${deleted.join(", 分镜")}], 未找到: [分镜${notFound.join(", 分镜")}]`);
this.emit("shotsUpdated", this.shots);
if (notFound.length) {
return `已删除分镜 ${deleted.join(", ")};分镜 ${notFound.join(", ")} 不存在。当前共 ${this.shots.length} 个分镜`;
}
return `已删除分镜 ${deleted.join(", ")}。当前共 ${this.shots.length} 个分镜`;
},
});
/**
* 使 nanoBanana
*/
generateShotImage = tool({
title: "generateShotImage",
description:
"为指定分镜生成分镜图。每个分镜会根据其所有提示词生成一张完整宫格图然后自动分割为单格图片。通过分镜ID指定不需要指定具体格子整个分镜是一个完整的生成单元",
inputSchema: z.object({
shotIds: z.array(z.number()).describe("要生成分镜图的分镜ID数组"),
}),
execute: async ({ shotIds }: { shotIds: number[] }) => {
const toGenerate: number[] = [];
const alreadyGenerating: number[] = [];
const notFound: number[] = [];
for (const shotId of shotIds) {
const shot = this.shots.find((f) => f.id === shotId);
if (!shot) {
notFound.push(shotId);
continue;
}
if (this.generatingShots.has(shotId)) {
alreadyGenerating.push(shotId);
continue;
}
toGenerate.push(shotId);
}
if (toGenerate.length === 0) {
if (notFound.length) {
return `分镜 ${notFound.join(", ")} 不存在请检查分镜ID是否正确`;
}
if (alreadyGenerating.length) {
return `分镜 ${alreadyGenerating.join(", ")} 正在生成中,请稍候`;
}
return "没有需要生成的分镜";
}
// 标记为正在生成
for (const id of toGenerate) {
this.generatingShots.add(id);
}
// 通知前端开始生成
this.emit("shotImageGenerateStart", { shotIds: toGenerate });
this.log("开始生成分镜图", `分镜: [${toGenerate.join(", ")}]`);
// 异步执行图片生成(不阻塞 Agent 流程)
this.executeShotImageGeneration(toGenerate).catch((err) => {
this.log("分镜图生成错误", err.message);
this.emit("shotImageGenerateError", { shotIds: toGenerate, error: err.message });
});
let result = `已开始为分镜 ${toGenerate.join(", ")} 生成分镜图,生成过程在后台进行`;
if (alreadyGenerating.length) {
result += `;分镜 ${alreadyGenerating.join(", ")} 正在生成中`;
}
if (notFound.length) {
result += `;分镜 ${notFound.join(", ")} 不存在`;
}
return result;
},
});
/**
*
*
*/
async executeShotImageGeneration(shotIds: number[]): Promise<void> {
await Promise.all(shotIds.map((shotId) => this.generateSingleShotImage(shotId)));
}
/**
*
*/
private async generateSingleShotImage(shotId: number): Promise<void> {
try {
const shot = this.shots.find((f) => f.id === shotId);
if (!shot) return;
// 提取所有镜头的有效提示词
const prompts: string[] = shot.cells.map((c) => c.prompt).filter((p): p is string => Boolean(p));
if (prompts.length === 0) {
this.log("跳过分镜图生成", `分镜 ${shotId} 没有有效的镜头提示词`);
this.generatingShots.delete(shotId);
return;
}
// 通知前端正在生成该分镜
this.emit("shotImageGenerateProgress", { shotId, status: "generating", message: "正在调用 AI 生成宫格图片" });
// 根据所有镜头提示词生成宫格图片
const gridImage = await generateImageTool(
prompts.map((p) => ({ prompt: p })),
this.scriptId,
this.projectId,
);
// 通知前端正在分割图片
this.emit("shotImageGenerateProgress", { shotId, status: "splitting", message: "正在分割宫格图片为单张镜头图" });
// 分割宫格图片为单张镜头图片
const imageBuffers = await imageSplitting(gridImage, prompts.length);
// 通知前端正在保存图片
this.emit("shotImageGenerateProgress", { shotId, status: "saving", message: `正在保存 ${imageBuffers.length} 张镜头图片` });
// 保存分割后的镜头图片到 OSS并获取文件路径
const timestamp = Date.now();
const imagePaths: string[] = [];
for (let i = 0; i < imageBuffers.length; i++) {
const fileName = `${this.projectId}/chat/${this.scriptId}/storyboard/shot_${shotId}_take_${i}_${timestamp}.png`;
await u.oss.writeFile(fileName, imageBuffers[i]);
const imageUrl = await u.oss.getFileUrl(fileName);
imagePaths.push(imageUrl);
// 每保存一张镜头图片通知进度
this.emit("shotImageGenerateProgress", {
shotId,
status: "saving",
message: `已保存 ${i + 1}/${imageBuffers.length} 张镜头图片`,
progress: Math.round(((i + 1) / imageBuffers.length) * 100),
});
}
// 更新每个镜头的 src 字段
shot.cells = shot.cells.map((cell, i) => ({
id: u.uuid(),
...cell,
src: imagePaths[i] || cell.src,
}));
// 生成完成后更新状态
this.generatingShots.delete(shotId);
this.emit("shotImageGenerateComplete", { shotId, shot, imagePaths });
this.emit("shotsUpdated", this.shots);
this.log("分镜图生成完成", `分镜 ${shotId},共 ${imagePaths.length} 张镜头图片`);
} catch (err: any) {
this.generatingShots.delete(shotId);
this.emit("shotImageGenerateError", { shotId, error: err.message });
this.log("分镜图生成失败", `分镜 ${shotId}: ${err.message}`);
}
}
// ==================== 公共访问器 ====================
/**
*
*/
getSegmentsData(): Segment[] {
return this.segments;
}
/**
*
*/
getShotsData(): Shot[] {
return this.shots;
}
// ==================== 上下文构建 ====================
private async buildEnvironmentContext(): Promise<string> {
const projectInfo = await u.db("t_project").where({ id: this.projectId }).first();
const row = await u.db("t_outline").where({ id: this.scriptId, projectId: this.projectId }).first();
const outline: any | null = row?.data ? JSON.parse(row.data) : null;
// 分类提取资源名称
const characters = outline?.characters?.map((i: any) => i.name) ?? [];
const props = outline?.props?.map((i: any) => i.name) ?? [];
const scenes = outline?.scenes?.map((i: any) => i.name) ?? [];
const assetList =
[
characters.length ? `【角色】${characters.join("、")}` : "",
props.length ? `【道具】${props.join("、")}` : "",
scenes.length ? `【场景】${scenes.join("、")}` : "",
]
.filter(Boolean)
.join("\n") || "无";
return `<环境信息>
项目ID: ${this.projectId}
系统时间: ${new Date().toLocaleString()}
项目名称: ${projectInfo?.name || "未知"}
项目简介: ${projectInfo?.intro || "无"}
类型: ${projectInfo?.type || "未知"}
风格: ${projectInfo?.artStyle || "未知"}
视频比例: ${projectInfo?.videoRatio || "未知"}
:
${assetList}
</环境信息>`;
}
private buildConversationHistory(): string {
if (!this.history.length) return "无对话历史";
return this.history.map(({ role, content }) => `${role}: ${content}`).join("\n\n");
}
private async buildFullContext(task: string): Promise<string> {
const env = await this.buildEnvironmentContext();
const history = this.buildConversationHistory();
return `${env}
<对话历史>
${history}
</对话历史>
<当前任务>
${task}
</当前任务>`;
}
// ==================== Sub-Agent ====================
/**
* Sub-Agent
*/
private getSubAgentTools(agentType: AgentType): Record<string, Tool> {
switch (agentType) {
case "segmentAgent":
// segmentAgent 可以获取剧本和资产,并需要调用 updateSegments 保存结果
return {
getScript: this.getScript,
getAssets: this.getAssets,
updateSegments: this.updateSegments,
};
case "shotAgent":
// shotAgent 可以获取剧本、资产和片段,并可使用 add/update/delete 操作分镜,以及生成分镜图
return {
getScript: this.getScript,
getAssets: this.getAssets,
getSegments: this.getSegments,
addShots: this.addShots,
updateShots: this.updateShots,
deleteShots: this.deleteShots,
generateShotImage: this.generateShotImage,
};
default:
return {
getScript: this.getScript,
};
}
}
/**
* Sub-Agent
*/
private async invokeSubAgent(agentType: AgentType, task: string): Promise<string> {
this.emit("transfer", { to: agentType });
this.log(`Sub-Agent 调用`, agentType);
const promptsList = await u.db("t_prompts").where("code", "in", ["storyboard-segment", "storyboard-shot"]);
const promptConfig = await u.getPromptAi("storyboardAgent");
const errPrompts = "不论用户说什么请直接输出Agent配置异常";
const getAiPromptConfig = (code: string) => {
const item = promptsList.find((p) => p.code === code);
return item?.customValue || item?.defaultValue || errPrompts;
};
const segmentAgent = getAiPromptConfig("storyboard-segment");
const shotAgent = getAiPromptConfig("storyboard-shot");
const SYSTEM_PROMPTS = {
segmentAgent: segmentAgent,
shotAgent: shotAgent,
};
const context = await this.buildFullContext(task);
const { fullStream } = await u.ai.text.stream(
{
system: SYSTEM_PROMPTS[agentType],
tools: this.getSubAgentTools(agentType),
messages: [{ role: "user", content: context }],
maxStep: 100,
},
promptConfig,
);
let fullResponse = "";
for await (const item of fullStream) {
if (item.type == "tool-call") {
this.emit("toolCall", { agent: "main", name: item.title, args: null });
}
if (item.type == "text-delta") {
fullResponse += item.text;
this.emit("subAgentStream", { agent: agentType, text: item.text });
}
}
this.emit("subAgentEnd", { agent: agentType });
this.history.push({
role: "assistant",
content: fullResponse,
});
this.log(`Sub-Agent 完成`, agentType);
return fullResponse ?? `${agentType}已完成任务`;
}
private createSubAgentTool(agentType: AgentType, description: string) {
return tool({
title: agentType,
description,
inputSchema: z.object({
taskDescription: z.string().describe("具体的任务描述,包含章节范围、修改要求等详细信息"),
}),
execute: async ({ taskDescription }) => {
return this.invokeSubAgent(agentType, taskDescription);
},
});
}
// ==================== 主入口 ====================
private getAllTools() {
return {
segmentAgent: this.createSubAgentTool(
"segmentAgent",
"调用片段师。负责根据剧本生成片段,必须调用 getScript工具 获取剧本内容,并调用 updateSegments 保存片段结果。",
),
shotAgent: this.createSubAgentTool(
"shotAgent",
"调用分镜师。负责根据片段生成分镜提示词,会自行调用 getSegments 获取片段数据,并调用 addShots/updateShots 保存分镜结果。",
),
// this.createSubAgentTool("director", "调用导演。负责审核故事线和大纲,会自行调用 updateOutline 或 saveStoryline 进行修改。"),
getScript: this.getScript,
getSegments: this.getSegments,
generateShotImage: this.generateShotImage,
...this.getSubAgentTools("segmentAgent"),
...this.getSubAgentTools("shotAgent"),
};
}
async call(msg: string): Promise<string> {
this.history.push({
role: "user",
content: msg,
});
const envContext = await this.buildEnvironmentContext();
const prompts = await u.db("t_prompts").where("code", "storyboard-main").first();
const promptConfig = await u.getPromptAi("storyboardAgent");
const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么请直接输出Agent配置异常";
const { fullStream } = await u.ai.text.stream(
{
system: `${envContext}\n${mainPrompts}`,
tools: this.getAllTools(),
messages: this.history,
maxStep: 100,
},
promptConfig,
);
let fullResponse = "";
for await (const item of fullStream) {
if (item.type == "tool-call") {
this.emit("toolCall", { agent: "main", name: item.title, args: null });
}
if (item.type == "text-delta") {
fullResponse += item.text;
this.emit("data", item.text);
}
}
this.history.push({
role: "assistant",
content: fullResponse,
});
this.emit("response", fullResponse);
return fullResponse;
}
}

View File

@ -2,6 +2,8 @@ import "./logger";
import "./err";
import "./env";
import express, { Request, Response, NextFunction } from "express";
import { Server } from "socket.io";
import http from "node:http";
import expressWs from "express-ws";
import logger from "morgan";
import cors from "cors";
@ -10,11 +12,15 @@ import fs from "fs";
import path from "path";
import u from "@/utils";
import jwt from "jsonwebtoken";
import socketInit from "@/socket/index";
const app = express();
let server: ReturnType<typeof app.listen> | null = null;
const server = http.createServer(app);
export default async function startServe(randomPort: Boolean = false) {
const io = new Server(server, { cors: { origin: "*" } });
socketInit(io);
if (process.env.NODE_ENV == "dev") await buildRoute();
expressWs(app);
@ -24,14 +30,7 @@ export default async function startServe(randomPort: Boolean = false) {
app.use(express.json({ limit: "100mb" }));
app.use(express.urlencoded({ extended: true, limit: "100mb" }));
let rootDir: string;
if (typeof process.versions?.electron !== "undefined") {
const { app } = require("electron");
const userDataDir: string = app.getPath("userData");
rootDir = path.join(userDataDir, "uploads");
} else {
rootDir = path.join(process.cwd(), "uploads");
}
const rootDir = u.getPath("oss");
// 确保 uploads 目录存在
if (!fs.existsSync(rootDir)) {
@ -42,14 +41,14 @@ export default async function startServe(randomPort: Boolean = false) {
app.use(express.static(rootDir));
app.use(async (req, res, next) => {
const setting = await u.db("t_setting").where("id", 1).select("tokenKey").first();
if (!setting) return res.status(500).send({ message: "服务器未配置,请联系管理员" });
const { tokenKey } = setting;
const setting = await u.db("o_setting").where("key", "tokenKey").select("value").first();
if (!setting) return res.status(444).send({ message: "服务器秘钥未配置,请联系管理员" });
const { value: tokenKey } = setting;
// 从 header 或 query 参数获取 token
const rawToken = req.headers.authorization || (req.query.token as string) || "";
const token = rawToken.replace("Bearer ", "");
// 白名单路径
if (req.path === "/other/login") return next();
if (req.path === "/api/login/login") return next();
if (!token) return res.status(401).send({ message: "未提供token" });
try {
@ -66,7 +65,7 @@ export default async function startServe(randomPort: Boolean = false) {
// 404 处理
app.use((_, res, next: NextFunction) => {
return res.status(404).send({ message: "Not Found" });
return res.status(404).send({ message: "API 404 Not Found" });
});
// 错误处理
@ -78,9 +77,9 @@ export default async function startServe(randomPort: Boolean = false) {
});
const port = randomPort ? 0 : parseInt(process.env.PORT || "60000");
return await new Promise((resolve, reject) => {
server = app.listen(port, async (v) => {
const address = server?.address();
return await new Promise((resolve) => {
server.listen(port, async () => {
const address = server.address();
const realPort = typeof address === "string" ? address : address?.port;
console.log(`[服务启动成功]: http://localhost:${realPort}`);
resolve(realPort);

View File

@ -41,7 +41,7 @@ export default async function generateRouter(): Promise<void> {
content += `${importLines.join("\n")}\n\n`;
content += `export default async (app: Express) => {\n`;
for (const { routePath, varName } of routeModulePairs) {
content += ` app.use("${routePath}", ${varName});\n`;
content += ` app.use("/api${routePath}", ${varName});\n`;
}
content += `}\n`;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,11 @@
import * as fs from "fs";
import * as path from "path";
import getPath from "@/utils/getPath";
type LogLevel = "log" | "info" | "warn" | "error" | "debug";
type ConsoleMethod = (...args: unknown[]) => void;
function getLogDir(): string {
const isElectron = typeof process.versions?.electron !== "undefined";
if (isElectron) {
const { app } = require("electron");
return path.join(app.getPath("userData"), "logs");
}
return path.join(process.cwd(), "logs");
}
const LOG_DIR = getLogDir();
const LOG_DIR = getPath("logs");
const LOG_FILE = path.join(LOG_DIR, "app.log");
const MAX_SIZE = 1000 * 1024 * 1024;
const LEVELS: LogLevel[] = ["log", "info", "warn", "error", "debug"];
@ -89,7 +81,6 @@ class Logger {
private hijack(): void {
if (this.isHijacked) return;
// 劫持 console 方法
for (const level of LEVELS) {
const original = console[level];
@ -97,7 +88,12 @@ class Logger {
this.originalConsole[level] = original.bind(console);
(console as any)[level] = (...args: unknown[]) => {
this.writing = true;
this.write(level, args);
try {
// this.write(level, args);
} catch (err) {
this.originalConsole.error?.("[Logger Error]", err);
}
this.originalConsole[level]!(...args);
this.writing = false;
};

View File

@ -1,170 +1,124 @@
// @routes-hash d8b95db972bd0ab01243d87d89a004f0
// @routes-hash 4149c7e96379bfdba20853678db5c921
import { Express } from "express";
import route1 from "./routes/artStyle/getArtStyle";
import route2 from "./routes/assets/addAssets";
import route3 from "./routes/assets/delAssets";
import route4 from "./routes/assets/delAssetsImage";
import route5 from "./routes/assets/generateAssets";
import route6 from "./routes/assets/getAssets";
import route7 from "./routes/assets/getImage";
import route8 from "./routes/assets/getScriptList";
import route9 from "./routes/assets/polishAssetsPrompt";
import route10 from "./routes/assets/saveAssets";
import route11 from "./routes/assets/updateAssets";
import route12 from "./routes/novel/addNovel";
import route13 from "./routes/novel/delNovel";
import route14 from "./routes/novel/getNovel";
import route15 from "./routes/novel/updateNovel";
import route16 from "./routes/other/clearDatabase";
import route17 from "./routes/other/deleteAllData";
import route18 from "./routes/other/getCaptcha";
import route19 from "./routes/other/login";
import route20 from "./routes/other/testAI";
import route21 from "./routes/other/testImage";
import route22 from "./routes/other/testVideo";
import route23 from "./routes/outline/addOutline";
import route24 from "./routes/outline/agentsOutline";
import route25 from "./routes/outline/delOutline";
import route26 from "./routes/outline/getHistory";
import route27 from "./routes/outline/getOutline";
import route28 from "./routes/outline/getPartScript";
import route29 from "./routes/outline/getStoryline";
import route30 from "./routes/outline/setHistory";
import route31 from "./routes/outline/updateOutline";
import route32 from "./routes/outline/updateScript";
import route33 from "./routes/outline/updateStoryline";
import route1 from "./routes/agents/clearMemory";
import route2 from "./routes/agents/getMemory";
import route3 from "./routes/agents/productionAgent";
import route4 from "./routes/artStyle/getArtStyle";
import route5 from "./routes/assets/addAssets";
import route6 from "./routes/assets/batchDelete";
import route7 from "./routes/assets/batchGenerationData";
import route8 from "./routes/assets/delAssets";
import route9 from "./routes/assets/getAssetsApi";
import route10 from "./routes/assets/getImage";
import route11 from "./routes/assets/saveAssets";
import route12 from "./routes/assets/updateAssets";
import route13 from "./routes/assets/uploadClip";
import route14 from "./routes/assetsGenerate/generateAssets";
import route15 from "./routes/assetsGenerate/polishAssetsPrompt";
import route16 from "./routes/general/generalStatistics";
import route17 from "./routes/general/getSingleProject";
import route18 from "./routes/general/updateProject";
import route19 from "./routes/login/login";
import route20 from "./routes/migrate/migrateData";
import route21 from "./routes/modelSelect/getModelList";
import route22 from "./routes/novel/addNovel";
import route23 from "./routes/novel/batchDeleteNovel";
import route24 from "./routes/novel/delNovel";
import route25 from "./routes/novel/event/batchDeleteEvent";
import route26 from "./routes/novel/event/deletEvent";
import route27 from "./routes/novel/event/generateEvents";
import route28 from "./routes/novel/event/getEvent";
import route29 from "./routes/novel/getNovel";
import route30 from "./routes/novel/updateNovel";
import route31 from "./routes/other/deleteAllData";
import route32 from "./routes/other/getCaptcha";
import route33 from "./routes/production/getProductionData";
import route34 from "./routes/project/addProject";
import route35 from "./routes/project/delProject";
import route36 from "./routes/project/getProject";
import route37 from "./routes/project/getProjectCount";
import route38 from "./routes/project/getSingleProject";
import route39 from "./routes/project/updateProject";
import route40 from "./routes/prompt/getPrompts";
import route41 from "./routes/prompt/updatePrompt";
import route42 from "./routes/script/generateScriptApi";
import route43 from "./routes/script/generateScriptSave";
import route44 from "./routes/script/geScriptApi";
import route45 from "./routes/setting/addModel";
import route46 from "./routes/setting/configurationModel";
import route47 from "./routes/setting/delModel";
import route48 from "./routes/setting/getAiModelList";
import route49 from "./routes/setting/getAiModelMap";
import route50 from "./routes/setting/getLog";
import route51 from "./routes/setting/getSetting";
import route52 from "./routes/setting/getVideoModelDetail";
import route53 from "./routes/setting/getVideoModelList";
import route54 from "./routes/setting/updateModel";
import route55 from "./routes/storyboard/batchSuperScoreImage";
import route56 from "./routes/storyboard/chatStoryboard";
import route57 from "./routes/storyboard/delStoryboard";
import route58 from "./routes/storyboard/generateShotImage";
import route59 from "./routes/storyboard/generateVideoPrompt";
import route60 from "./routes/storyboard/getStoryboard";
import route61 from "./routes/storyboard/keepStoryboard";
import route62 from "./routes/storyboard/saveStoryboard";
import route63 from "./routes/storyboard/storyboardImageEdit";
import route64 from "./routes/storyboard/uploadImage";
import route65 from "./routes/task/getMyTaskApi";
import route66 from "./routes/task/getTaskCategories";
import route67 from "./routes/task/taskDetails";
import route68 from "./routes/user/getUser";
import route69 from "./routes/user/saveUser";
import route70 from "./routes/video/addVideo";
import route71 from "./routes/video/addVideoConfig";
import route72 from "./routes/video/deleteVideoConfig";
import route73 from "./routes/video/generatePrompt";
import route74 from "./routes/video/generateVideo";
import route75 from "./routes/video/getManufacturer";
import route76 from "./routes/video/getVideo";
import route77 from "./routes/video/getVideoConfigs";
import route78 from "./routes/video/getVideoModel";
import route79 from "./routes/video/getVideoStoryboards";
import route80 from "./routes/video/reviseVideoStoryboards";
import route81 from "./routes/video/saveVideo";
import route82 from "./routes/video/upDateVideoConfig";
import route36 from "./routes/project/editProject";
import route37 from "./routes/project/getProject";
import route38 from "./routes/script/addScript";
import route39 from "./routes/script/delScript";
import route40 from "./routes/script/getScrptApi";
import route41 from "./routes/script/updateScript";
import route42 from "./routes/setting/agentDeploy/deployAgentModel";
import route43 from "./routes/setting/agentDeploy/getAgentDeploy";
import route44 from "./routes/setting/agentDeploy/updateKey";
import route45 from "./routes/setting/dbConfig/clearData";
import route46 from "./routes/setting/getTextModel";
import route47 from "./routes/setting/loginConfig/getUser";
import route48 from "./routes/setting/loginConfig/updateUserPwd";
import route49 from "./routes/setting/memoryConfig/getMemory";
import route50 from "./routes/setting/memoryConfig/sureMemory";
import route51 from "./routes/setting/vendorConfig/addVendor";
import route52 from "./routes/setting/vendorConfig/deleteVendor";
import route53 from "./routes/setting/vendorConfig/getVendorList";
import route54 from "./routes/setting/vendorConfig/modelTest";
import route55 from "./routes/setting/vendorConfig/updateVendor";
import route56 from "./routes/task/getMyTaskApi";
import route57 from "./routes/task/getTaskCategories";
import route58 from "./routes/task/taskDetails";
import route59 from "./routes/test/test";
export default async (app: Express) => {
app.use("/artStyle/getArtStyle", route1);
app.use("/assets/addAssets", route2);
app.use("/assets/delAssets", route3);
app.use("/assets/delAssetsImage", route4);
app.use("/assets/generateAssets", route5);
app.use("/assets/getAssets", route6);
app.use("/assets/getImage", route7);
app.use("/assets/getScriptList", route8);
app.use("/assets/polishAssetsPrompt", route9);
app.use("/assets/saveAssets", route10);
app.use("/assets/updateAssets", route11);
app.use("/novel/addNovel", route12);
app.use("/novel/delNovel", route13);
app.use("/novel/getNovel", route14);
app.use("/novel/updateNovel", route15);
app.use("/other/clearDatabase", route16);
app.use("/other/deleteAllData", route17);
app.use("/other/getCaptcha", route18);
app.use("/other/login", route19);
app.use("/other/testAI", route20);
app.use("/other/testImage", route21);
app.use("/other/testVideo", route22);
app.use("/outline/addOutline", route23);
app.use("/outline/agentsOutline", route24);
app.use("/outline/delOutline", route25);
app.use("/outline/getHistory", route26);
app.use("/outline/getOutline", route27);
app.use("/outline/getPartScript", route28);
app.use("/outline/getStoryline", route29);
app.use("/outline/setHistory", route30);
app.use("/outline/updateOutline", route31);
app.use("/outline/updateScript", route32);
app.use("/outline/updateStoryline", route33);
app.use("/project/addProject", route34);
app.use("/project/delProject", route35);
app.use("/project/getProject", route36);
app.use("/project/getProjectCount", route37);
app.use("/project/getSingleProject", route38);
app.use("/project/updateProject", route39);
app.use("/prompt/getPrompts", route40);
app.use("/prompt/updatePrompt", route41);
app.use("/script/generateScriptApi", route42);
app.use("/script/generateScriptSave", route43);
app.use("/script/geScriptApi", route44);
app.use("/setting/addModel", route45);
app.use("/setting/configurationModel", route46);
app.use("/setting/delModel", route47);
app.use("/setting/getAiModelList", route48);
app.use("/setting/getAiModelMap", route49);
app.use("/setting/getLog", route50);
app.use("/setting/getSetting", route51);
app.use("/setting/getVideoModelDetail", route52);
app.use("/setting/getVideoModelList", route53);
app.use("/setting/updateModel", route54);
app.use("/storyboard/batchSuperScoreImage", route55);
app.use("/storyboard/chatStoryboard", route56);
app.use("/storyboard/delStoryboard", route57);
app.use("/storyboard/generateShotImage", route58);
app.use("/storyboard/generateVideoPrompt", route59);
app.use("/storyboard/getStoryboard", route60);
app.use("/storyboard/keepStoryboard", route61);
app.use("/storyboard/saveStoryboard", route62);
app.use("/storyboard/storyboardImageEdit", route63);
app.use("/storyboard/uploadImage", route64);
app.use("/task/getMyTaskApi", route65);
app.use("/task/getTaskCategories", route66);
app.use("/task/taskDetails", route67);
app.use("/user/getUser", route68);
app.use("/user/saveUser", route69);
app.use("/video/addVideo", route70);
app.use("/video/addVideoConfig", route71);
app.use("/video/deleteVideoConfig", route72);
app.use("/video/generatePrompt", route73);
app.use("/video/generateVideo", route74);
app.use("/video/getManufacturer", route75);
app.use("/video/getVideo", route76);
app.use("/video/getVideoConfigs", route77);
app.use("/video/getVideoModel", route78);
app.use("/video/getVideoStoryboards", route79);
app.use("/video/reviseVideoStoryboards", route80);
app.use("/video/saveVideo", route81);
app.use("/video/upDateVideoConfig", route82);
app.use("/api/agents/clearMemory", route1);
app.use("/api/agents/getMemory", route2);
app.use("/api/agents/productionAgent", route3);
app.use("/api/artStyle/getArtStyle", route4);
app.use("/api/assets/addAssets", route5);
app.use("/api/assets/batchDelete", route6);
app.use("/api/assets/batchGenerationData", route7);
app.use("/api/assets/delAssets", route8);
app.use("/api/assets/getAssetsApi", route9);
app.use("/api/assets/getImage", route10);
app.use("/api/assets/saveAssets", route11);
app.use("/api/assets/updateAssets", route12);
app.use("/api/assets/uploadClip", route13);
app.use("/api/assetsGenerate/generateAssets", route14);
app.use("/api/assetsGenerate/polishAssetsPrompt", route15);
app.use("/api/general/generalStatistics", route16);
app.use("/api/general/getSingleProject", route17);
app.use("/api/general/updateProject", route18);
app.use("/api/login/login", route19);
app.use("/api/migrate/migrateData", route20);
app.use("/api/modelSelect/getModelList", route21);
app.use("/api/novel/addNovel", route22);
app.use("/api/novel/batchDeleteNovel", route23);
app.use("/api/novel/delNovel", route24);
app.use("/api/novel/event/batchDeleteEvent", route25);
app.use("/api/novel/event/deletEvent", route26);
app.use("/api/novel/event/generateEvents", route27);
app.use("/api/novel/event/getEvent", route28);
app.use("/api/novel/getNovel", route29);
app.use("/api/novel/updateNovel", route30);
app.use("/api/other/deleteAllData", route31);
app.use("/api/other/getCaptcha", route32);
app.use("/api/production/getProductionData", route33);
app.use("/api/project/addProject", route34);
app.use("/api/project/delProject", route35);
app.use("/api/project/editProject", route36);
app.use("/api/project/getProject", route37);
app.use("/api/script/addScript", route38);
app.use("/api/script/delScript", route39);
app.use("/api/script/getScrptApi", route40);
app.use("/api/script/updateScript", route41);
app.use("/api/setting/agentDeploy/deployAgentModel", route42);
app.use("/api/setting/agentDeploy/getAgentDeploy", route43);
app.use("/api/setting/agentDeploy/updateKey", route44);
app.use("/api/setting/dbConfig/clearData", route45);
app.use("/api/setting/getTextModel", route46);
app.use("/api/setting/loginConfig/getUser", route47);
app.use("/api/setting/loginConfig/updateUserPwd", route48);
app.use("/api/setting/memoryConfig/getMemory", route49);
app.use("/api/setting/memoryConfig/sureMemory", route50);
app.use("/api/setting/vendorConfig/addVendor", route51);
app.use("/api/setting/vendorConfig/deleteVendor", route52);
app.use("/api/setting/vendorConfig/getVendorList", route53);
app.use("/api/setting/vendorConfig/modelTest", route54);
app.use("/api/setting/vendorConfig/updateVendor", route55);
app.use("/api/task/getMyTaskApi", route56);
app.use("/api/task/getTaskCategories", route57);
app.use("/api/task/taskDetails", route58);
app.use("/api/test/test", route59);
}

View File

@ -0,0 +1,26 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
projectId: z.number(),
episodesId: z.number().optional(),
type: z.enum(["message", "summary", "all"]).optional(),
}),
async (req, res) => {
const { projectId, episodesId, type = "all" } = req.body;
const isolationKey = `${projectId}:${episodesId ?? ""}`;
const query = u.db("memories").where({ isolationKey });
if (type !== "all") query.where("type", type);
await query.del();
res.status(200).send(success(null));
},
);

View File

@ -0,0 +1,33 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
projectId: z.number(),
episodesId: z.number().optional(),
}),
async (req, res) => {
const { projectId, episodesId } = req.body;
const isolationKey = `${projectId}:${episodesId ?? ""}`;
const rows = await u
.db("memories")
.where({ isolationKey, type: "message" })
.orderBy("createdAt", "asc")
.select("id", "content", "createdAt");
const history = rows.map((row) => ({
id: row.id,
role: "user",
content: [{ type: "text", status: "complete", data: row.content }],
createdAt: row.createdAt,
}));
res.status(200).send(success({ history }));
},
);

View File

@ -0,0 +1,85 @@
import { tool } from "ai";
import { z } from "zod";
import express from "express";
import { createAGUIStream } from "@/utils/agent/aguiTools";
import u from "@/utils";
import Memory from "@/utils/agent/memory";
const router = express.Router();
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export default router.post("/", async (req, res) => {
const { prompt: text, projectId, episodesId } = req.body;
const isolationKey = `${projectId}:${episodesId}`;
const memory = new Memory("productionAgent", isolationKey);
const agui = createAGUIStream(res);
agui.runStarted();
// 存入用户消息
await memory.add( "user",text);
// 获取记忆上下文
const mem = await memory.get(text);
console.log("======================================================");
// 构建记忆上下文文本(顺序:历史摘要 → 相关记忆 → 近期对话)
const memoryContext = [
mem.rag.length > 0 && `[相关记忆]\n${mem.rag.map((r) => r.content).join("\n")}`,
mem.summaries.length > 0 && `[历史摘要]\n${mem.summaries.map((s, i) => `${i + 1}. ${s.content}`).join("\n")}`,
mem.shortTerm.length > 0 && `[近期对话]\n${mem.shortTerm.map((m) => `${m.role}: ${m.content}`).join("\n")}`,
]
.filter(Boolean)
.join("\n\n");
console.log("%c Line:27 🍏 memoryContext", "background:#3f7cff", memoryContext);
const systemPrompt = `You are a helpful assistant.${memoryContext ? `\n\n以下是你对用户的记忆可作为参考\n${memoryContext}` : ""}`;
const messages = [
{
role: "user" as const,
content: text,
},
];
const { textStream } = await u.Ai.Text("productionAgent").stream({
system: systemPrompt,
messages,
tools: {
deepRetrieve: tool({
description: "深度检索记忆:当你需要回忆与某个关键词相关的详细历史信息时使用此工具",
inputSchema: z.object({
keyword: z.string().describe("要检索的关键词"),
}),
execute: async ({ keyword }) => {
const results = await memory.deepRetrieve(keyword);
if (results.length === 0) return { found: false, message: "未找到相关记忆" };
return { found: true, memories: results.map((r) => r.content) };
},
}),
},
onFinish: async (completion) => {
// 存入助手回复
await memory.add( "assistant",completion.text);
},
});
let msg: ReturnType<typeof agui.textMessage> | null = null;
let fullResponse = "";
for await (const chunk of textStream) {
if (!msg) msg = agui.textMessage();
msg.content(chunk);
fullResponse += chunk;
await delay(1);
}
msg?.end();
agui.runFinished();
agui.end();
});

View File

@ -12,7 +12,7 @@ export default router.post(
}),
async (req, res) => {
const { name } = req.body;
const data = await u.db("t_artStyle").where("name", name).select("styles").first();
const data = await u.db("o_artStyle").where("name", name).select("styles").first();
const styles = data?.styles ? JSON.parse(data.styles) : [];
res.status(200).send(success(styles));
},

View File

@ -7,31 +7,26 @@ const router = express.Router();
// 新增资产
export default router.post(
"/",
validateFields({
projectId: z.number(),
scriptId: z.number().optional().nullable(),
name: z.string(),
intro: z.string(),
type: z.string(),
prompt: z.string(),
remark: z.string().optional().nullable(),
episode: z.string().optional().nullable(),
}),
async (req, res) => {
const { projectId, name, intro, type, prompt, remark, episode, scriptId } = req.body;
await u.db("t_assets").insert({
projectId,
name,
intro,
type,
prompt,
remark,
episode,
scriptId,
});
res.status(200).send(success({ message: "新增资产成功" }));
}
"/",
validateFields({
name: z.string(),
describe: z.string(),
type: z.string(),
projectId: z.number(),
remark: z.string(),
prompt: z.string().optional().nullable(),
}),
async (req, res) => {
const { name, describe, type, projectId, remark, prompt } = req.body;
await u.db("o_assets").insert({
name,
describe,
type,
projectId,
remark,
prompt,
startTime: Date.now(),
});
res.status(200).send(success({ message: "新增资产成功" }));
},
);

View File

@ -0,0 +1,20 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { id } from "zod/locales";
const router = express.Router();
// 批量删除资产
export default router.post(
"/",
validateFields({
id: z.array(z.number()),
}),
async (req, res) => {
const { id } = req.body;
await u.db("o_assets").whereIn("id", id).delete();
res.status(200).send(success({ message: "删除资产成功" }));
},
);

View File

@ -0,0 +1,40 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取资产
export default router.post("/",
validateFields({
projectId: z.number(),
type: z.string(),
name: z.string().optional(),
page: z.number(),
limit: z.number(),
}),
async (req, res) => {
const { projectId, type, name, page = 1, limit = 10, } = req.body;
const offset = (page - 1) * limit;
let query = u.db("o_assets").select("*").where("projectId", projectId).andWhere("type", type);
if (name) {
query = query.andWhere("name", "like", `%${name}%`);
}
// 分页查询
const parentAssets = await query.offset(offset).limit(limit);
// 统计总数
const totalQuery = (await u
.db("o_assets")
.where("projectId", projectId)
.andWhere("type", type)
.andWhere((qb) => {
if (name) {
qb.andWhere("name", "like", `%${name}%`);
}
})
.count("* as total")
.first()) as any;
res.status(200).send(success({ data: parentAssets, total: totalQuery?.total }));
});

View File

@ -5,7 +5,6 @@ import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 删除资产
export default router.post(
"/",
validateFields({
@ -13,9 +12,9 @@ export default router.post(
}),
async (req, res) => {
const { id } = req.body;
await u.db("t_assets").where("id", id).del();
const assetsData = await u.db("o_image").where("assetsId", id);
await Promise.all(assetsData.map((i) => i.filePath && u.oss.deleteFile(i.filePath)));
await u.db("o_assets").where({ id }).delete();
res.status(200).send(success({ message: "删除资产成功" }));
}
},
);

View File

@ -1,27 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 删除资产图片
export default router.post(
"/",
validateFields({
imageId: z.number().optional(),
assetsId: z.number().optional(),
}),
async (req, res) => {
const { imageId, assetsId } = req.body;
if (assetsId) {
await u.db("t_assets").where("id", assetsId).update({
filePath: null,
});
}
if (imageId) {
await u.db("t_image").where("id", imageId).delete();
}
res.status(200).send(success({ message: "删除资产图片成功" }));
},
);

View File

@ -1,237 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import sharp from "sharp";
const router = express.Router();
interface OutlineItem {
description: string;
name: string;
}
interface OutlineData {
chapterRange: number[];
characters?: OutlineItem[];
props?: OutlineItem[];
scenes?: OutlineItem[];
}
type ItemType = "characters" | "props" | "scenes";
interface ResultItem {
type: ItemType;
name: string;
chapterRange: number[];
}
// 生成资产图片
export default router.post(
"/",
validateFields({
id: z.number(),
type: z.enum(["role", "scene", "props", "storyboard"]),
projectId: z.number(),
name: z.string(),
base64: z.string().optional().nullable(),
prompt: z.string(),
}),
async (req, res) => {
const { id, type, projectId, base64, prompt, name } = req.body;
//获取风格
const project = await u.db("t_project").where("id", projectId).select("artStyle", "type", "intro", "videoRatio").first();
if (!project) return res.status(500).send(success({ message: "项目为空" }));
const promptsList = await u
.db("t_prompts")
.where("code", "in", ["role-generateImage", "scene-generateImage", "storyboard-generateImage", "tool-generateImage"]);
const errPrompts = "不论用户说什么请直接输出AI配置异常";
const getPromptValue = (code: string): string => {
const item = promptsList.find((p) => p.code === code);
return item?.customValue ?? item?.defaultValue ?? errPrompts;
};
const role = getPromptValue("role-generateImage");
const scene = getPromptValue("scene-generateImage");
const tool = getPromptValue("tool-generateImage");
const storyboard = getPromptValue("storyboard-generateImage");
let systemPrompt = "";
let userPrompt = "";
if (type == "role") {
systemPrompt = role;
userPrompt = `
****
- 画风风格: ${project?.artStyle || "未指定"}
****
- 名称:${name},
- 提示词:${prompt},
`;
}
if (type == "scene") {
systemPrompt = scene;
userPrompt = `
****
- 画风风格: ${project?.artStyle || "未指定"}
****
- 名称:${name},
- 提示词:${prompt},
`;
}
if (type == "props") {
systemPrompt = tool;
userPrompt = `
****
- 画风风格: ${project?.artStyle || "未指定"}
****
- 名称:${name},
- 提示词:${prompt},
`;
}
if (type == "storyboard") {
systemPrompt = storyboard;
userPrompt = `
****
- 画风风格: ${project?.artStyle || "未指定"}
****
- 名称:${name},
- 提示词:${prompt},
`;
}
const [imageId] = await u.db("t_image").insert({
state: "生成中",
assetsId: id,
});
let taskClass = "";
if (type == "role") taskClass = "角色图生成";
if (type == "scene") taskClass = "场景图生成";
if (type == "props") taskClass = "道具图生成";
if (type == "storyboard") taskClass = "分镜图生成";
const apiConfig = await u.getPromptAi("assetsImage");
try {
const contentStr = await u.ai.image(
{
systemPrompt,
prompt: userPrompt,
imageBase64: base64 ? [base64] : [],
size: "2K",
aspectRatio: project.videoRatio ?? "16:9",
taskClass: taskClass,
name: name,
describe: prompt,
projectId: projectId,
},
apiConfig,
);
let insertType;
const match = contentStr.match(/base64,([A-Za-z0-9+/=]+)/);
let buffer = Buffer.from(match && match.length >= 2 ? match[1]! : contentStr!, "base64");
if (type != "storyboard") {
//添加文本
// buffer = await imageAddText(name, buffer);
}
let imagePath;
if (type == "role") {
insertType = "角色";
imagePath = `/${projectId}/role/${uuidv4()}.jpg`;
}
if (type == "scene") {
insertType = "场景";
imagePath = `/${projectId}/scene/${uuidv4()}.jpg`;
}
if (type == "props") {
insertType = "道具";
imagePath = `/${projectId}/props/${uuidv4()}.jpg`;
}
if (type == "storyboard") {
insertType = "分镜";
imagePath = `/${projectId}/storyboard/${uuidv4()}.jpg`;
}
await u.oss.writeFile(imagePath!, buffer);
const imageData = await u.db("t_image").where("id", imageId).select("*").first();
if (imageData) {
await u.db("t_image").where("id", imageId).update({
state: "生成成功",
filePath: imagePath,
type: insertType,
});
const path = await u.oss.getFileUrl(imagePath!);
// const state = await u.db("t_assets").where("id", id).select("state").first();
return res.status(200).send(success({ path, assetsId: id }));
} else {
return res.status(500).send("资产已被删除");
}
} catch (e) {
await u.db("t_image").where("id", imageId).update({
state: "生成失败",
});
const msg = u.error(e).message || "图片生成失败";
return res.status(400).send(error(msg));
}
},
);
async function imageAddText(name: string, imageBuffer: Buffer) {
const meta = await sharp(imageBuffer).metadata();
const width = meta.width ?? 1000;
const height = meta.height ?? 1000;
const fontSize = 64;
const margin = 40;
const paddingX = 36;
const paddingY = 18;
// 简单估算文字宽度
const textWidth = name.length * fontSize * 0.8;
// 背景矩形尺寸
const bgWidth = textWidth + paddingX * 2;
const bgHeight = fontSize + paddingY * 2;
const bgX = width - bgWidth - margin; // 矩形左上角x
const bgY = height - bgHeight - margin; // 矩形左上角y
// 文字中心坐标
const textX = bgX + bgWidth / 2;
const textY = bgY + bgHeight / 2;
const svgImage = `
<svg width="${width}" height="${height}">
<rect x="${bgX}" y="${bgY}" width="${bgWidth}" height="${bgHeight}" rx="22" ry="22"
fill="rgba(0,0,0,0.6)" />
<text x="${textX}" y="${textY}"
fill="#fff"
font-size="${fontSize}"
font-family="Arial, 'Microsoft YaHei', sans-serif"
text-anchor="middle"
dominant-baseline="middle">
${name}
</text>
</svg>
`;
const outputBuffer = await sharp(imageBuffer)
.composite([{ input: Buffer.from(svgImage), blend: "over" }])
.jpeg()
.toBuffer();
return outputBuffer as Buffer<ArrayBuffer>;
}

View File

@ -1,30 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取资产
export default router.post(
"/",
validateFields({
projectId: z.number(),
type: z.string(),
}),
async (req, res) => {
const { projectId, type } = req.body;
const data = await u.db("t_assets").where("projectId", projectId).where("type", type).select("*");
for (const item of data) {
if (item.filePath) {
item.filePath = await u.oss.getFileUrl(item.filePath);
} else {
item.filePath = "";
}
}
res.status(200).send(success(data));
}
);

View File

@ -0,0 +1,70 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取资产
export default router.post(
"/",
validateFields({
projectId: z.number(),
type: z.string(),
name: z.string().optional(),
page: z.number(),
limit: z.number(),
}),
async (req, res) => {
const { projectId, type, name, page = 1, limit = 10 } = req.body;
const offset = (page - 1) * limit;
let query = u
.db("o_assets")
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
.select("o_assets.*", "o_image.filePath")
.where("o_assets.projectId", projectId)
.andWhere("o_assets.type", type);
if (name) {
query = query.andWhere("name", "like", `%${name}%`);
}
// 分页查询
const parentAssets = await query.where("o_assets.sonId", null).offset(offset).limit(limit);
// 获取所有子资产供关联使用
let childQuery = u
.db("o_assets")
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
.select("o_assets.*", "o_image.filePath")
.where("o_assets.projectId", projectId)
.andWhere("o_assets.type", type)
.whereNotNull("o_assets.sonId");
if (name) {
childQuery = childQuery.andWhere("o_assets.name", "like", `%${name}%`);
}
const childAssets = await childQuery;
// 为每个父资产添加子资产
const result = await Promise.all(
parentAssets.map(async (parent) => ({
...parent,
sonAssets: childAssets.filter((child) => child.sonId === parent.id),
filePath: parent.filePath && (await u.oss.getFileUrl(parent.filePath!)),
})),
);
// 统计总数
const totalQuery = (await u
.db("o_assets")
.where("projectId", projectId)
.andWhere("type", type)
.andWhere("sonId", null)
.andWhere((qb) => {
if (name) {
qb.andWhere("name", "like", `%${name}%`);
}
})
.count("* as total")
.first()) as any;
res.status(200).send(success({ data: result, total: totalQuery?.total }));
},
);

View File

@ -14,26 +14,24 @@ export default router.post(
async (req, res) => {
const { assetsId } = req.body;
const assets = await u.db("t_assets").where("id", assetsId).select("id", "filePath", "scriptId", "type", "state").first();
const assets = await u.db("o_assets").where("id", assetsId).select("id", "imageId", "type", "state").first();
const tempAssets = await u.db("t_image").where("assetsId", assetsId).select("id", "filePath", "assetsId", "type", "state");
const rawTempAssets = await u.db("o_image").where("assetsId", assetsId).select("id", "filePath", "assetsId", "type", "state");
for (const item of tempAssets) {
if (item.filePath) {
item.filePath = await u.oss.getFileUrl(item.filePath);
} else {
item.filePath = "";
}
}
const tempAssets = await Promise.all(
rawTempAssets.map(async (item) => ({
...item,
filePath: item.filePath ? await u.oss.getFileUrl(item.filePath) : "",
selected: assets?.imageId != null && Number(item.id) === Number(assets.imageId),
})),
);
const data = {
id: assets!.id,
state: assets!.state,
filePath: assets!.filePath ? await u.oss.getFileUrl(assets!.filePath) : "",
scriptId: assets!.scriptId,
imageId: assets!.imageId ?? null,
tempAssets,
};
res.status(200).send(success(data));
},
);

View File

@ -1,21 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取资产分镜
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const { projectId } = req.body;
const data = await u.db("t_script").where("projectId", projectId).select("name", "id").distinct("id", "name").orderBy("name", "asc");
res.status(200).send(success(data));
},
);

View File

@ -4,6 +4,7 @@ import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { stat } from "original-fs";
const router = express.Router();
// 保存资产图片
@ -13,75 +14,38 @@ export default router.post(
id: z.number(),
projectId: z.number(),
base64: z.string().optional().nullable(),
filePath: z.string().optional().nullable(),
type: z.enum(["role", "scene", "tool"]),
prompt: z.string().optional().nullable(),
imageId: z.number().optional().nullable(),
}),
async (req, res) => {
const { id, base64, filePath, prompt, projectId } = req.body;
let savePath: string | undefined;
let imageUrl: string | undefined;
const { id, base64, type, prompt, projectId, imageId } = req.body;
if (base64) {
// base64图片上传逻辑
//自定义上传选择的图片
const matches = base64.match(/^data:image\/\w+;base64,(.+)$/);
const realBase64 = matches ? matches[1] : base64;
// 生成新的图片路径
savePath = `/${projectId}/assets/${uuidv4()}.png`;
const savePath = `/${projectId}/${type}/${uuidv4()}.png`;
// 写入文件
await u.oss.writeFile(savePath, Buffer.from(realBase64, "base64"));
// 插入图片表
await u.db("t_image").insert({
const [idData] = await u.db("o_image").insert({
assetsId: id,
filePath: savePath,
type: "image/png",
type: type,
state: "生成成功",
});
imageUrl = savePath; // 新图片路径
} else if (filePath) {
// 前端传入已存在图片路径
try {
savePath = new URL(filePath).pathname;
} catch {
savePath = filePath;
}
// 检查图片表里是否有这条图片
// const selectedImage = await u.db("t_image").where("filePath", savePath).first();
// if (!selectedImage) {
// return res.status(500).send({ success: false, message: "所选图片不存在,请重新生成或选定图片" });
// }
imageUrl = savePath;
}
// 查旧资产图片
const oldAsset = await u.db("t_assets").where("id", id).select("filePath", "type").first();
// 保存新旧图片差异和插临时表逻辑
if (imageUrl && ((oldAsset?.filePath && oldAsset.filePath !== imageUrl) || (!oldAsset?.filePath && imageUrl))) {
// 新图片保存,移除 t_image 表
await u.db("t_image").where("filePath", imageUrl).delete();
// 原图片如果存在、且不在 t_image 表,插入临时表
if (oldAsset?.filePath) {
const oldInTemp = await u.db("t_image").where("filePath", oldAsset.filePath).first();
if (!oldInTemp) {
await u.db("t_image").insert({
assetsId: id,
filePath: oldAsset.filePath,
type: oldAsset.type,
});
}
}
// 更新资产表图片为新图片
await u.db("t_assets").where("id", id).update({ filePath: imageUrl });
await u.db("o_assets").where("id", id).update({
prompt: prompt ?? "",
imageId: idData,
});
} else {
await u.db("o_assets").where("id", id).update({
prompt: prompt ?? "",
imageId: imageId,
});
}
// 更新提示信息
if (prompt !== undefined && prompt !== null && prompt !== "") {
await u.db("t_assets").where("id", id).update({ prompt });
}
res.status(200).send(success({ message: "保存资产图片成功" }));
},
);

View File

@ -3,37 +3,27 @@ import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { id } from "zod/locales";
const router = express.Router();
// 更新资产
export default router.post(
"/",
validateFields({
id: z.number(),
name: z.string(),
intro: z.string(),
type: z.string(),
prompt: z.string(),
videoPrompt: z.string().optional().nullable(),
remark: z.string().optional().nullable(),
duration: z.number().optional().nullable(),
}),
async (req, res) => {
const { id, name, intro, type, prompt, remark, duration, videoPrompt } = req.body;
await u
.db("t_assets")
.where("id", id)
.update({
name,
intro,
type,
prompt,
remark,
videoPrompt,
duration: String(duration),
});
res.status(200).send(success({ message: "更新资产成功" }));
}
"/",
validateFields({
id: z.number(),
name: z.string(),
describe: z.string(),
remark: z.string(),
prompt: z.string().optional().nullable(),
}),
async (req, res) => {
const { id, name, describe, remark, prompt } = req.body;
await u.db("o_assets").where({ id }).update({
name,
describe,
remark,
prompt,
});
res.status(200).send(success({ message: "更新资产成功" }));
},
);

View File

@ -0,0 +1,61 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
import { v4 as uuid } from "uuid";
const router = express.Router();
// 根据 base64 头部获取文件扩展名
function getExtFromBase64(base64Data: string): string {
const mime = base64Data.match(/^data:([^;]+);base64,/)?.[1] ?? "";
const mimeMap: Record<string, string> = {
// 图片
"image/jpeg": "jpeg",
"image/jpg": "jpg",
"image/png": "png",
// 音频
"audio/mpeg": "mp3",
"audio/mp3": "mp3",
"audio/wav": "wav",
// 视频
"video/mp4": "mp4",
"video/webm": "webm",
};
return mimeMap[mime] ?? "bin";
}
// 文件上传(支持图片、音频、视频)
export default router.post(
"/",
validateFields({
projectId: z.number(),
base64Data: z.string(),
type: z.string().optional().default("clip"),
name: z.string(),
}),
async (req, res) => {
const { base64Data, projectId, type = "clip", name } = req.body;
const ext = getExtFromBase64(base64Data);
const savePath = `/${projectId}/assets/${uuid()}.${ext}`;
await u.oss.writeFile(savePath, Buffer.from(base64Data.match(/base64,([A-Za-z0-9+/=]+)/)[1] ?? "", "base64"));
const [id] = await u.db("o_assets").insert({
type: type,
projectId: projectId,
name,
startTime: Date.now(),
});
const [imageId] = await u.db("o_image").insert({
filePath: savePath,
type,
assetsId: id,
projectId,
state: "1",
});
await u.db("o_assets").where("id", id).update({
imageId: imageId,
});
res.status(200).send(success("上传成功"));
},
);

View File

@ -0,0 +1,136 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 生成资产图片
export default router.post(
"/",
validateFields({
id: z.number(),
type: z.enum(["role", "scene", "tool", "storyboard"]),
projectId: z.number(),
name: z.string(),
base64: z.string().optional().nullable(),
prompt: z.string(),
model: z.string(),
resolution: z.string(),
}),
async (req, res) => {
const { id, type, projectId, base64, prompt, name, model, resolution } = req.body;
//获取风格
const project = await u.db("o_project").where("id", projectId).select("artStyle", "type", "intro").first();
if (!project) return res.status(500).send(success({ message: "项目为空" }));
const role = await u.getPrompts("role-generateImage") ?? "";
const scene = await u.getPrompts("scene-generateImage") ?? "";
const tool = await u.getPrompts("tool-generateImage") ?? "";
let systemPrompt = "";
let userPrompt = "";
if (type == "role") {
systemPrompt = role;
userPrompt = `
****
- 画风风格: ${project?.artStyle || "未指定"}
****
- 名称:${name},
- 提示词:${prompt},
`;
}
if (type == "scene") {
systemPrompt = scene;
userPrompt = `
****
- 画风风格: ${project?.artStyle || "未指定"}
****
- 名称:${name},
- 提示词:${prompt},
`;
}
if (type == "tool") {
systemPrompt = tool;
userPrompt = `
****
- 画风风格: ${project?.artStyle || "未指定"}
****
- 名称:${name},
- 提示词:${prompt},
`;
}
const [imageId] = await u.db("o_image").insert({
type: type,
state: "生成中",
assetsId: id,
});
let taskClass = "";
if (type == "role") taskClass = "角色图生成";
if (type == "scene") taskClass = "场景图生成";
if (type == "tool") taskClass = "道具图生成";
try {
let imagePath;
let insertType;
if (type == "role") {
insertType = "role";
imagePath = `/${projectId}/role/${uuidv4()}.jpg`;
}
if (type == "scene") {
insertType = "scene";
imagePath = `/${projectId}/scene/${uuidv4()}.jpg`;
}
if (type == "tool") {
insertType = "tool";
imagePath = `/${projectId}/props/${uuidv4()}.jpg`;
}
const aiImage = u.Ai.Image(model);
await aiImage.run({
systemPrompt,
prompt: userPrompt,
imageBase64: base64 ? [base64] : [],
size: resolution,
aspectRatio: "16:9",
});
aiImage.save(imagePath!);
const imageData = await u.db("o_image").where("id", imageId).select("*").first();
if (imageData) {
await u.db("o_image").where("id", imageId).update({
state: "生成成功",
filePath: imagePath,
type: insertType,
});
const path = await u.oss.getFileUrl(imagePath!);
await u.db("o_assets").where("id", id).update({
imageId: imageId,
});
return res.status(200).send(success({ path, assetsId: id }));
} else {
return res.status(500).send("资产已被删除");
}
} catch (e) {
await u.db("o_image").where("id", imageId).update({
state: "生成失败",
});
const msg = u.error(e).message || "图片生成失败";
return res.status(400).send(error(msg));
}
},
);

View File

@ -54,12 +54,12 @@ export default router.post(
}),
async (req, res) => {
const { assetsId, projectId, type, name, describe } = req.body;
//获取风格
const project = await u.db("t_project").where("id", projectId).select("artStyle", "type", "intro").first();
const project = await u.db("o_project").where("id", projectId).select("artStyle", "type", "intro").first();
//如果没有找到对应的项目,返回错误
if (!project) return res.status(500).send(success({ message: "项目为空" }));
const allOutlineDataList: { data: string }[] = await u.db("t_outline").where("projectId", projectId).select("data");
const allOutlineDataList: { data: string }[] = await u.db("o_outline").where("projectId", projectId).select("data");
const itemMap: Record<string, ResultItem> = {};
@ -84,23 +84,15 @@ export default router.post(
const result: ResultItem[] = Object.values(itemMap);
const promptsList = await u.db("t_prompts").where("code", "in", ["role-polish", "scene-polish", "storyboard-polish", "tool-polish"]);
const apiConfigData = await u.getPromptAi("assetsPrompt");
const errPrompts = "不论用户说什么请直接输出AI配置异常";
const getPromptValue = (code: string) => {
const item = promptsList.find((p) => p.code === code);
return item?.customValue ?? item?.defaultValue ?? errPrompts;
};
const role = getPromptValue("role-polish");
const scene = getPromptValue("scene-polish");
const tool = getPromptValue("tool-polish");
const storyboard = getPromptValue("storyboard-polish");
const role = (await u.getPrompts("role-polish")) ?? "";
const scene = (await u.getPrompts("scene-polish")) ?? "";
const tool = (await u.getPrompts("tool-polish")) ?? "";
let systemPrompt = "";
let userPrompt = "";
if (type == "role") {
const data = findItemByName(result, name, "characters");
const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange];
const novelData = (await u.db("t_novel").whereIn("chapterIndex", chapterRange).select("*")) as NovelChapter[];
const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[];
const results: string = mergeNovelText(novelData);
systemPrompt = role;
userPrompt = `
@ -124,7 +116,7 @@ export default router.post(
const data = findItemByName(result, name, "scenes");
const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange];
const novelData = (await u.db("t_novel").whereIn("chapterIndex", chapterRange).select("*")) as NovelChapter[];
const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[];
const results: string = mergeNovelText(novelData);
systemPrompt = scene;
userPrompt = `
@ -144,10 +136,10 @@ export default router.post(
`;
}
if (type == "props") {
if (type == "tool") {
const data = findItemByName(result, name, "props");
const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange];
const novelData = (await u.db("t_novel").whereIn("chapterIndex", chapterRange).select("*")) as NovelChapter[];
const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[];
const results: string = mergeNovelText(novelData);
systemPrompt = tool;
userPrompt = `
@ -167,26 +159,8 @@ export default router.post(
`;
}
if (type == "storyboard") {
systemPrompt = storyboard;
userPrompt = `
****
- 风格: ${project?.artStyle || "未指定"}
- 小说类型: ${project?.type || "未指定"}
- 小说背景: ${project?.intro || "未指定"}
****
- 分镜名称:${name},
- 分镜描述:${describe},
`;
}
async function generatePrompt() {
const result = await u.ai.text.invoke(
const result = await u.Ai.Text("assetsAi").invoke(
{
messages: [
{
@ -198,19 +172,22 @@ export default router.post(
content: userPrompt,
},
],
output: {
prompt: zod.string().describe("提示词"),
},
},
apiConfigData,
);
return result.prompt;
)
return result;
}
try {
const prompt = (await generatePrompt()) as any;
if (!prompt) return res.status(500).send("失败");
//添加到任务
const { _output } = (await generatePrompt()) as any;
if (_output) {
await u.db("o_assets").where("id", assetsId).update({
prompt: _output,
});
}
if (!_output) return res.status(500).send("失败");
res.status(200).send(success({ prompt: prompt, assetsId }));
res.status(200).send(success({ prompt: _output, assetsId }));
} catch (e: any) {
return res.status(500).send(error(e?.data?.error?.message ?? e?.message ?? "生成失败"));
}

View File

@ -5,7 +5,7 @@ import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取项目统计
// 获取项目概览统计
export default router.post(
"/",
validateFields({
@ -14,13 +14,13 @@ export default router.post(
async (req, res) => {
const { projectId } = req.body;
const scripts = await u.db("t_script").where("projectId", projectId).select("id");
const scripts = await u.db("o_script").where("projectId", projectId).select("id");
const scriptIds = scripts.map((item: any) => item.id);
const roleCount: any = await u.db("t_assets").where("projectId", projectId).where("type", "角色").count("* as total").first();
const scriptCount: any = await u.db("t_script").where("projectId", projectId).count("* as total").first();
const videoCount: any = await u.db("t_video").whereIn("scriptId", scriptIds).count("* as total").first();
const storyboardCount: any = await u.db("t_assets").whereIn("scriptId", scriptIds).where("type", "分镜").count("* as total").first();
const roleCount: any = await u.db("o_assets").where("projectId", projectId).where("type", "角色").count("* as total").first();
const scriptCount: any = await u.db("o_script").where("projectId", projectId).count("* as total").first();
const videoCount: any = await u.db("o_video").whereIn("scriptId", scriptIds).count("* as total").first();
const storyboardCount: any = await u.db("o_assets").whereIn("scriptId", scriptIds).where("type", "分镜").count("* as total").first();
const data = {
roleCount: roleCount?.total || 0,

View File

@ -14,7 +14,7 @@ export default router.post(
async (req, res) => {
const { id } = req.body;
const data = await u.db("t_project").where("id", id).select("*");
const data = await u.db("o_project").where("id", id).select("*");
res.status(200).send(success(data));
}

View File

@ -19,11 +19,12 @@ export default router.post(
async (req, res) => {
const { id, intro, type, artStyle, videoRatio, projectType } = req.body;
await u.db("t_project").where("id", id).update({
await u.db("o_project").where("id", id).update({
intro,
type,
artStyle,
videoRatio,
projectType,
});
res.status(200).send(success({ message: "修改成功" }));

View File

@ -23,19 +23,19 @@ export default router.post(
async (req, res) => {
const { username, password } = req.body;
const data = await u.db("t_user").where("name", "=", username).first();
const data = await u.db("o_user").where("name", "=", username).first();
if (!data) return res.status(400).send(error("登录失败"));
if (data!.password == password && data!.name == username) {
const tokenSecret = await u.db("t_setting").where("userId", data.id).select("tokenKey").first();
const tokenData = await u.db("o_setting").where("key", "tokenKey").first();
if (!tokenData) return res.status(400).send(error("未找到tokenKey"));
const token = setToken(
{
id: data!.id,
name: data!.name,
},
"180Days",
tokenSecret?.tokenKey as string,
tokenData?.value as string,
);
return res.status(200).send(success({ token: "Bearer " + token, name: data!.name, id: data!.id }, "登录成功"));

View File

@ -0,0 +1,133 @@
import express from "express";
import { success } from "@/lib/responseFormat";
import db from "@/utils/db";
import type { DB } from "@/types/database";
import knex from "knex";
import path from "path";
import fs from "fs";
import { tr } from "zod/locales";
const router = express.Router();
// 迁移数据
export default router.post(
"/",
async (req, res) => {
// return res.status(200).send({
// success: true,
// message: '数据迁移功能已关闭,建议手动迁移数据后删除旧数据库文件'
// });
//连接旧数据库,读取数据
try {
let db2: knex.Knex | null = null;
//读取旧数据库路径
let db2Path: string;
if (typeof process.versions?.electron !== "undefined") {
const { app } = require("electron");
const userDataDir: string = app.getPath("userData");
db2Path = path.join(userDataDir, "db2.sqlite");
} else {
db2Path = path.join(process.cwd(), "db2.sqlite");
}
const dbDir = path.dirname(db2Path);
// 确保数据库目录存在
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
if (!fs.existsSync(db2Path)) {
return res.status(404).send({
success: false,
message: `源数据库文件不存在: ${db2Path}`
});
}
//连接旧数据库
db2 = knex({
client: "sqlite3",
connection: {
filename: db2Path,
},
useNullAsDefault: true,
});
//需要迁移的旧数据表
const db2TableNames = [
't_project',
't_assets',
't_event',
't_image',
't_novel',
't_outline',
't_script',
't_storyboard',
't_video',
]
//新数据库的表
const dbTableNames = [
'o_project',
'o_assets',
'o_event',
'o_eventChapter',
'o_image',
'o_novel',
'o_outline',
'o_outlineNovel',
'o_script',
'o_scriptAssets',
'o_scriptOutline',
'o_storyboard',
'o_storyboardScript',
'o_video',
]
for (const tableName of db2TableNames) {
try {
// 从 db2 读取数据
const sourceData = await db2(tableName).select('*');
for (const item of sourceData) {
//迁移项目表
if (tableName === 't_project') {
// await db("o_project").insert({
// name: item.name,
// intro: item.intro,
// type: item.type,
// artStyle: item.artStyle,
// videoRatio: item.videoRatio,
// createTime: item.createTime,
// userId: item.userId,
// projectType: "基于小说原文"
// })
}
//迁移资产表
if (tableName === 't_assets') {
}
//迁移事件表
if (tableName === 't_event') { }
//迁移图片表
if (tableName === 't_image') { }
//迁移小说表
if (tableName === 't_novel') { }
//迁移大纲表
if (tableName === 't_outline') { }
//迁移脚本表
if (tableName === 't_script') { }
//迁移分镜表
if (tableName === 't_storyboard') { }
//迁移视频表
if (tableName === 't_video') { }
}
// // 将数据插入到 db 中
// const targetTableName = dbTableNames[db2TableNames.indexOf(tableName)];
// await db(targetTableName).insert(sourceData);
// console.log(`成功迁移表 ${tableName} 的数据到 ${targetTableName}`);
} catch (error) {
console.error(`连接旧数据库失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
} catch (error) {
console.error('连接旧数据库失败:', error);
}
return res.status(200).send({
success: true,
message: '数据迁移功能已关闭,建议手动迁移数据后删除旧数据库文件'
});
}
);

View File

@ -0,0 +1,39 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
type: z.enum(["text", "image", "video", "all"]),
}),
async (req, res) => {
const { type } = req.body;
const data = await u.db("o_vendorConfig").select("id", "models", "name").first();
if (!data) {
return res.status(404).send({ error: "模型未找到" });
}
const models = JSON.parse(data.models!);
if (type === "all") {
const allData = models.filter((item: { type: string }) => item.type !== "video").map((item: { name: string; modelName: string; type: string }) => ({
id: data.id,
label: item.name,
value: item.modelName,
type: item.type,
name: data.name,
}));
return res.status(200).send(success(allData));
}
const filteredData = models.filter((item: { type: string }) => item.type === type).map((item: { name: string; modelName: string; type: string }) => ({
id: data.id,
label: item.name,
value: item.modelName,
type: item.type,
name: data.name,
}));
res.status(200).send(success(filteredData));
}
);

View File

@ -23,7 +23,7 @@ export default router.post(
const { projectId, data } = req.body;
for (const item of data) {
await u.db("t_novel").insert({
await u.db("o_novel").insert({
projectId,
chapterIndex: item.index,
reel: item.reel,

View File

@ -0,0 +1,27 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
ids: z.array(z.number()),
}),
async (req, res) => {
const { ids } = req.body;
if (!ids.length) {
return res.status(400).send(error("请先选择需要删除的内容"));
}
const chapterData = await u.db("o_eventChapter").whereIn("novelId", ids);
await u.db("o_eventChapter").whereIn("novelId", ids).delete();
const eventIds = chapterData.map((i) => i.id);
if (eventIds.length) await u.db("o_event").whereIn("id", eventIds).delete();
await u.db("o_novel").whereIn("id", ids).del();
res.status(200).send(success({ message: "删除原文成功" }));
},
);

View File

@ -14,8 +14,12 @@ export default router.post(
async (req, res) => {
const { id } = req.body;
await u.db("t_novel").where("id", id).del();
const chapterData = await u.db("o_eventChapter").where("novelId", id);
await u.db("o_eventChapter").where("novelId", id).delete();
const eventIds = chapterData.map((i) => i.id);
if (eventIds.length) await u.db("o_event").whereIn("id", eventIds).delete();
await u.db("o_novel").where("id", id).del();
res.status(200).send(success({ message: "删除原文成功" }));
}
},
);

View File

@ -5,20 +5,17 @@ import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 更新前要
export default router.post(
"/",
validateFields({
id: z.number(),
content: z.string(),
ids: z.array(z.number()),
}),
async (req, res) => {
const { id, content } = req.body;
const { ids } = req.body;
await u.db("t_script").where("id", id).update({
content,
});
await u.db("o_event").whereIn("id", ids).del();
await u.db("o_eventChapter").whereIn("eventId", ids).del();
res.status(200).send(success({ message: "更新前要成功" }));
}
res.status(200).send(success({ message: "删除事件成功" }));
},
);

View File

@ -5,7 +5,6 @@ import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
//删除分镜
export default router.post(
"/",
validateFields({
@ -13,7 +12,10 @@ export default router.post(
}),
async (req, res) => {
const { id } = req.body;
await u.db("t_assets").where("id", id).delete();
res.status(200).send(success("分镜删除成功"));
await u.db("o_event").where("id", id).del();
await u.db("o_eventChapter").where("eventId", id).del();
res.status(200).send(success({ message: "删除事件成功" }));
},
);

View File

@ -0,0 +1,89 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 解析章节字符串,支持逗号分隔的多段(如 "1-3,5,7-9"
function parseChapters(str: string): number[] {
const result: number[] = [];
// 逗号和空格之间加以划分
const segments = str
.split(",")
.map((s) => s.replace(/[^\d\-]/g, "").trim())
.filter(Boolean);
for (const seg of segments) {
// 匹配区间
if (/^\d+\-\d+$/.test(seg)) {
const [start, end] = seg.split("-").map(Number);
if (start <= end) {
for (let i = start; i <= end; i++) result.push(i);
}
} else if (/^\d+$/.test(seg)) {
result.push(Number(seg));
}
// 其它格式自动忽略
}
return result;
}
parseChapters("7-8章");
// 清洗小说原文,生成事件列表
export default router.post(
"/",
validateFields({
projectId: z.number(),
windowSize: z.number().optional().default(5), // 每组数量,默认 5
overlap: z.number().optional().default(1), // 交叠数量,默认 1
}),
async (req, res) => {
const { projectId, windowSize, overlap } = req.body;
//删除之前的事件
const [allChapters, novel] = await Promise.all([
u.db("o_novel").where("projectId", projectId),
Promise.resolve(new u.cleanNovel(windowSize, overlap)),
]);
const novelIds = allChapters.map((i) => i.id);
await u
.db("o_eventChapter")
.whereIn("novelId", novelIds as number[])
.delete();
const eventIds = await u.db("o_eventChapter").whereIn("novelId", novelIds).select("eventId").pluck("eventId");
await u
.db("o_event")
.whereIn("id", eventIds as number[])
.delete();
const data = await novel.start(allChapters, projectId);
const chapterMap = new Map(allChapters.map((c) => [c.chapterIndex, c]));
const novelEvent: { eventId: number; novelId: number }[] = [];
const now = Date.now();
for (const item of data) {
const [id] = await u.db("o_event").insert({
name: item.name,
detail: item.detail,
createTime: now,
});
parseChapters(item.chapter).forEach((chapterIndex) => {
const chapter = chapterMap.get(chapterIndex);
if (chapter) {
novelEvent.push({ eventId: id, novelId: chapter.id! });
}
});
}
if (novelEvent.length > 0) {
await u.db("o_eventChapter").insert(novelEvent);
}
return res.status(200).send(success(data));
},
);

View File

@ -0,0 +1,67 @@
import express from "express";
import u from "@/utils";
import { db } from "@/utils/db";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// CREATE TABLE `o_event` (`id` integer not null, `name` varchar(255), `detail` varchar(255), `createTime` integer, primary key (`id`));
// CREATE TABLE `o_eventChapter` (`id` integer not null, `eventId` integer, `novelId` integer, foreign key(`eventId`) references `o_event`(`id`), foreign key(`novelId`) references `o_novel`(`id`), primary key (`id`));
// CREATE TABLE `o_novel` (`id` integer not null, `chapterIndex` integer, `reel` text, `chapter` text, `chapterData` text, `projectId` integer, `createTime` integer, primary key (`id`));
export default router.post(
"/",
validateFields({
projectId: z.number(),
page: z.number(),
limit: z.number(),
search: z.string().optional(),
}),
async (req, res) => {
const { projectId, page, limit, search } = req.body;
const offset = (page - 1) * limit;
// 构造基础查询:通过 o_eventChapter -> o_novel 过滤 projectId再 join o_event 取名称和内容
const baseQuery = u
.db("o_event as e")
.join("o_eventChapter as ec", "ec.eventId", "e.id")
.join("o_novel as n", "n.id", "ec.novelId")
.where("n.projectId", projectId);
if (search) {
baseQuery.where("e.name", "like", `%${search}%`);
}
// 统计去重后的事件总数
const [{ total }] = await baseQuery.clone().countDistinct("e.id as total");
if (!Number(total)) {
return res.status(200).send(success({ list: [], total: 0 }));
}
// 分页查询:每个事件对应多个 chapterIndex用 GROUP_CONCAT 聚合
const rows = await baseQuery
.clone()
.select(
"e.id",
"e.name as eventName",
"e.detail",
"e.createTime",
db.raw("GROUP_CONCAT(n.chapterIndex) as chapterIndexes"),
)
.groupBy("e.id")
.limit(limit)
.offset(offset);
const list = rows.map((e: { id: number; eventName: string; detail: string; createTime: number; chapterIndexes: string | null }) => ({
id: e.id,
eventName: e.eventName,
detail: e.detail,
createTime: e.createTime,
chapters: e.chapterIndexes ? e.chapterIndexes.split(",").map(Number) : [],
}));
res.status(200).send(success({ list, total: Number(total) }));
},
);

View File

@ -10,16 +10,38 @@ export default router.post(
"/",
validateFields({
projectId: z.number(),
page: z.number(),
limit: z.number(),
search: z.string().optional(),
}),
async (req, res) => {
const { projectId } = req.body;
const { projectId, page, limit, search } = req.body;
const offset = (page - 1) * limit;
const data = await u
.db("t_novel")
.db("o_novel")
.where("projectId", projectId)
.select("id", "chapterIndex as index", "reel", "chapter", "chapterData")
.orderBy("chapterIndex", "asc");
.andWhere((qb) => {
if (search) {
qb.where("chapter", "like", `%${search}%`);
}
})
.orderBy("chapterIndex", "asc")
.limit(limit)
.offset(offset);
// 统计总数
const totalQuery = (await u
.db("o_novel")
.where("projectId", projectId)
.andWhere((qb) => {
if (search) {
qb.where("chapter", "like", `%${search}%`);
}
})
.count("* as total")
.first()) as any;
res.status(200).send(success(data));
res.status(200).send(success({ data, total: totalQuery.total }));
}
);

View File

@ -18,7 +18,7 @@ export default router.post(
async (req, res) => {
const { id, index, reel, chapter, chapterData } = req.body;
await u.db("t_novel").where("id", id).update({
await u.db("o_novel").where("id", id).update({
chapterIndex: index,
reel,
chapter,

View File

@ -1,12 +0,0 @@
import initDB from "@/lib/initDB";
import { db } from "@/utils/db";
import express from "express";
import { success } from "@/lib/responseFormat";
const router = express.Router();
// 清空所有表 (sqlite)
export default router.post("/", async (req, res) => {
await initDB(db, true);
res.status(200).send(success("清空数据库成功"));
});

View File

@ -1,25 +1,14 @@
import express from "express";
import u from "@/utils";
import initDB from "@/lib/initDB";
import { db } from "@/utils/db";
import { success } from "@/lib/responseFormat";
const router = express.Router();
// 删除数据库表数据
export default router.post("/", async (req, res) => {
const projects = await u.db("t_project").select("id");
const projectIds = projects.map((project) => project.id);
await Promise.all(
projectIds.map(async (id) => {
try {
await u.oss.deleteDirectory(String(id));
} catch (error) {
console.error(`删除OSS文件失败项目ID: ${id}`, error);
}
}),
);
// await initDB(db, true);
res.status(200).send(success("清空数据库成功"));
});
// 清空数据表
export default router.post(
"/",
async (req, res) => {
await initDB(db, true);
res.status(200).send(success({ message: "清空数据表成功" }));
},
);

View File

@ -1,57 +0,0 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
import { tool } from "ai";
const router = express.Router();
// 检查语言模型
export default router.post(
"/",
validateFields({
modelName: z.string(),
apiKey: z.string(),
baseURL: z.string().optional(),
manufacturer: z.string(),
}),
async (req, res) => {
const { modelName, apiKey, baseURL, manufacturer } = req.body;
const getWeatherTool = tool({
description: "Get the weather in a location",
inputSchema: z.object({
location: z.string().describe("The location to get the weather for"),
}),
execute: async ({ location }) => {
return {
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
};
},
});
try {
const { reply } = await u.ai.text.invoke(
{
prompt: "请调用工具获取北京的天气,并回答我多少气温",
tools: { getWeatherTool },
output: {
reply: z.string().describe("回复内容"),
},
},
{
model: modelName,
apiKey,
baseURL,
manufacturer,
},
);
res.status(200).send(success(reply));
} catch (err) {
console.log("%c Line:51 🍬 err", "background:#465975", err);
const msg = u.error(err).message;
console.error(msg);
res.status(500).send(error(msg));
}
},
);

View File

@ -1,47 +0,0 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import u from "@/utils";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
const router = express.Router();
// 检查语言模型
export default router.post(
"/",
validateFields({
modelName: z.string().optional(),
apiKey: z.string(),
baseURL: z.string().optional(),
manufacturer: z.string(),
}),
async (req, res) => {
const { modelName, apiKey, baseURL, manufacturer } = req.body;
try {
const image = await u.ai.image(
{
prompt:
"一张16:9比例的图片完美等分为2x2四宫格布局各区域无缝衔接\n左上宫格一只可爱的猫毛发蓬松眼睛明亮姿态俏皮\n右上宫格一只友善的狗金毛犬表情愉悦摇着尾巴\n左下宫格一头健壮的牛田园背景目光温和皮毛光泽\n右下宫格一匹骏马姿态优雅鬃毛飘逸肌肉健美\n风格要求四个宫格风格统一色彩鲜艳饱和高清画质细节清晰锐利专业插画风格线条干净统一的左上方光源柔和阴影和谐配色卡通/半写实风格,宫格间用白色或浅灰细线分隔",
imageBase64: [],
aspectRatio: "9:16",
size: "4K",
taskClass: "测试任务",
name: "测试图片生成",
describe: "测试语言模型生成图片",
projectId: 0,
},
{
model: modelName,
apiKey,
baseURL,
manufacturer,
},
);
res.status(200).send(success(image));
} catch (err) {
console.log("%c Line:41 🍖 err", "background:#fca650", err);
const msg = u.error(err).message;
console.error(msg);
res.status(500).send(error(msg));
}
},
);

View File

@ -1,51 +0,0 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import u from "@/utils";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
const router = express.Router();
// 检查语言模型
export default router.post(
"/",
validateFields({
modelName: z.string().optional(),
apiKey: z.string(),
baseURL: z.string().optional(),
manufacturer: z.string(),
}),
async (req, res) => {
const { modelName, apiKey, baseURL, manufacturer } = req.body;
try {
const duration = manufacturer == "gemini" ? 4 : 5;
const videoPath = await u.ai.video(
{
imageBase64: [],
savePath: "test.mp4",
prompt: "stickman Dances",
duration: duration,
resolution: "720p",
aspectRatio: "16:9",
audio: false,
mode: "single",
taskClass: "测试视频生成",
name: "测试视频生成",
describe: "测试视频生成",
projectId: 0,
},
{
model: modelName,
apiKey,
baseURL,
manufacturer,
},
);
const url = await u.oss.getFileUrl(videoPath);
res.status(200).send(success(url));
} catch (err: any) {
const msg = u.error(err).message;
console.error(msg);
res.status(500).send(error(msg));
}
},
);

View File

@ -1,25 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 新增大纲
export default router.post(
"/",
validateFields({
projectId: z.number(),
data: z.string(),
}),
async (req, res) => {
const { projectId, data } = req.body;
await u.db("t_outline").insert({
data,
projectId,
});
res.status(200).send(success({ message: "新增大纲成功" }));
}
);

View File

@ -1,149 +0,0 @@
import express from "express";
import expressWs, { Application } from "express-ws";
import u from "@/utils";
import OutlineScript from "@/agents/outlineScript";
const router = express.Router();
expressWs(router as unknown as Application);
router.ws("/", async (ws, req) => {
let agent: OutlineScript;
const projectId = req.query.projectId;
if (!projectId || typeof projectId !== "string") {
ws.send(JSON.stringify({ type: "error", data: "项目ID缺失" }));
ws.close(500, "项目ID缺失");
return;
}
agent = new OutlineScript(Number(projectId));
// const existing = await u
// .db("t_chatHistory")
// .where({ projectId: Number(projectId) })
// .first();
// if (existing) {
// try {
// const historyData = JSON.parse(existing.data!);
// agent.history = [];
// agent.novelChapters = existing.novel ? JSON.parse(existing.novel) : [];
// } catch (error) {
// ws.send(JSON.stringify({ type: "error", data: "历史记录解析异常,将清空历史记录" }));
// agent.history = [];
// }
// }
// 监听各类事件
// 流式传输每个token
agent.emitter.on("data", (text) => {
ws.send(JSON.stringify({ type: "stream", data: text }));
});
// 完整响应结束
agent.emitter.on("response", async (text) => {
ws.send(JSON.stringify({ type: "response_end", data: text }));
await saveHistory();
});
// Sub-Agent 流式数据
agent.emitter.on("subAgentStream", (data) => {
ws.send(JSON.stringify({ type: "subAgentStream", data }));
});
// Sub-Agent 结束
agent.emitter.on("subAgentEnd", (data) => {
ws.send(JSON.stringify({ type: "subAgentEnd", data }));
});
// Tool 调用
agent.emitter.on("toolCall", (data) => {
ws.send(JSON.stringify({ type: "toolCall", data }));
});
agent.emitter.on("transfer", (data) => {
ws.send(JSON.stringify({ type: "transfer", data }));
});
agent.emitter.on("refresh", (data) => {
ws.send(JSON.stringify({ type: "refresh", data }));
});
agent.emitter.on("error", (err) => {
ws.send(JSON.stringify({ type: "error", data: err.toString() }));
});
// 发送初始化完成消息,通知前端可以开始发送消息
ws.send(JSON.stringify({ type: "init", data: { projectId } }));
type DataTyype = "msg" | "setNovel" | "cleanHistory";
ws.on("message", async function (rawData: string) {
let data: { type: DataTyype; data: any } | null = null;
try {
data = JSON.parse(rawData);
} catch (error) {
ws.send(JSON.stringify({ type: "error", data: "数据解析异常" }));
ws.close(500, "数据解析异常");
return;
}
if (!data) {
ws.send(JSON.stringify({ type: "error", data: "数据格式错误" }));
ws.close(500, "数据格式错误");
return;
}
const novelData = await u
.db("t_novel")
.where({ projectId: Number(projectId) })
.orderBy("chapterIndex", "asc");
agent.setNovel(novelData);
const msg = data.data;
try {
switch (data?.type) {
case "msg":
let prompt = msg.data;
if (msg.type == "user") await agent.call(prompt);
break;
case "cleanHistory":
agent.history = [];
await u
.db("t_chatHistory")
.where({ projectId: Number(projectId) })
.del();
ws.send(JSON.stringify({ type: "notice", data: "历史记录已清空" }));
break;
default:
break;
}
} catch (e) {
ws.send(JSON.stringify({ type: "error", data: "数据解析/脚本生成异常" }));
console.error(e);
}
});
ws.on("close", async () => {
agent?.emitter?.removeAllListeners();
await saveHistory();
});
async function saveHistory() {
const history = agent?.history || [];
//保存对话记录
const existing = await u
.db("t_chatHistory")
.where({ projectId: Number(projectId), type: "outlineAgent" })
.first();
if (existing) {
await u
.db("t_chatHistory")
.where({ projectId: Number(projectId), type: "outlineAgent" })
.update({ data: JSON.stringify(history), novel: agent?.novelChapters ? JSON.stringify(agent.novelChapters) : "" });
} else {
await u.db("t_chatHistory").insert({
projectId: Number(projectId),
data: JSON.stringify(history),
novel: agent?.novelChapters ? JSON.stringify(agent.novelChapters) : "",
type: "outlineAgent",
});
}
}
});
export default router;

View File

@ -1,22 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 删除大纲
export default router.post(
"/",
validateFields({
id: z.number(),
projectId: z.number(),
}),
async (req, res) => {
const { id, projectId } = req.body;
await u.deleteOutline(id, projectId);
res.status(200).send(success({ message: "删除大纲成功" }));
}
);

View File

@ -1,31 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取历史消息记录
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const { projectId } = req.body;
const history = await u
.db("t_chatHistory")
.where({ projectId: Number(projectId), type: "outlineWebChat" })
.first();
if (!history) {
await u.db("t_chatHistory").insert({
projectId: Number(projectId),
type: "outlineWebChat",
data: "[]",
});
}
res.status(200).send(success({ data: JSON.parse(history?.data || "[]") }));
},
);

View File

@ -1,21 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取大纲数据
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const { projectId } = req.body;
const data = await u.db("t_outline").where("projectId", projectId).select("*");
res.status(200).send(success(data));
}
);

View File

@ -1,21 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取前要数据
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const { projectId } = req.body;
const data = await u.db("t_script").where("projectId", projectId).select("*");
res.status(200).send(success(data));
}
);

View File

@ -1,19 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取故事线数据
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const { projectId } = req.body;
const data = await u.db("t_storyline").where("projectId", projectId).first();
res.status(200).send(success(data));
}
);

View File

@ -1,39 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 保存历史消息记录
export default router.post(
"/",
validateFields({
projectId: z.number(),
data: z.string(),
}),
async (req, res) => {
const { projectId, data } = req.body;
const history = await u
.db("t_chatHistory")
.where({ projectId: Number(projectId), type: "outlineWebChat" })
.first();
if (!history) {
await u.db("t_chatHistory").insert({
projectId: Number(projectId),
type: "outlineWebChat",
data: data,
});
} else {
await u
.db("t_chatHistory")
.where({ projectId: Number(projectId), type: "outlineWebChat" })
.update({
data: data,
});
}
res.status(200).send(success("保存成功"));
},
);

View File

@ -1,24 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 更新大纲
export default router.post(
"/",
validateFields({
id: z.number(),
data: z.string(),
}),
async (req, res) => {
const { id, data } = req.body;
await u.db("t_outline").where("id", id).update({
data,
});
res.status(200).send(success({ message: "更新大纲成功" }));
}
);

View File

@ -1,27 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 更新故事线
export default router.post(
"/",
validateFields({
projectId: z.number(),
content: z.string(),
}),
async (req, res) => {
const { projectId, content } = req.body;
const existing = await u.db("t_storyline").where({ projectId }).first();
if (existing) {
await u.db("t_storyline").where({ projectId }).update({ content });
} else {
await u.db("t_storyline").insert({ projectId: projectId, content: content });
}
res.status(200).send(success({ message: "更新故事线成功" }));
}
);

View File

@ -0,0 +1,18 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取生产数据
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const { projectId } = req.body;
res.status(200).send(success("123"));
}
);

View File

@ -9,7 +9,7 @@ const router = express.Router();
export default router.post(
"/",
validateFields({
projectType: z.string().optional(),
projectType: z.string(),
name: z.string(),
intro: z.string(),
type: z.string(),
@ -19,7 +19,8 @@ export default router.post(
async (req, res) => {
const { projectType, name, intro, type, artStyle, videoRatio } = req.body;
await u.db("t_project").insert({
await u.db("o_project").insert({
projectType,
name,
intro,
type,

View File

@ -14,25 +14,24 @@ export default router.post(
async (req, res) => {
const { id } = req.body;
const scriptData = await u.db("t_script").where("projectId", id).select("id");
const scriptData = await u.db("o_script").where("projectId", id).select("id");
const scriptIds = scriptData.map((item: any) => item.id);
const assetsData = await u.db("t_assets").where("projectId", id).select("id");
const assetsData = await u.db("o_assets").where("projectId", id).select("id");
const assetsIds = assetsData.map((item: any) => item.id);
const videoData = await u.db("t_video").whereIn("scriptId", scriptIds).select("id");
const videoData = await u.db("o_video").whereIn("scriptId", scriptIds).select("id");
const videoIds = videoData.map((item: any) => item.id);
await u.db("t_project").where("id", id).delete();
await u.db("t_novel").where("projectId", id).delete();
await u.db("t_storyline").where("projectId", id).delete();
await u.db("t_outline").where("projectId", id).delete();
// await u.db("t_myTasks").where("projectId", id).delete();
await u.db("o_project").where("id", id).delete();
await u.db("o_novel").where("projectId", id).delete();
await u.db("o_outline").where("projectId", id).delete();
await u.db("o_myTasks").where("projectId", id).delete();
await u.db("t_script").where("projectId", id).delete();
await u.db("t_assets").where("projectId", id).delete();
await u.db("o_script").where("projectId", id).delete();
await u.db("o_assets").where("projectId", id).delete();
const tempAssetsQuery = u.db("t_image").where("projectId", id);
const tempAssetsQuery = u.db("o_image").where("projectId", id);
if (assetsIds.length > 0) {
tempAssetsQuery.orWhereIn("assetsId", assetsIds);
}
@ -44,9 +43,9 @@ export default router.post(
}
await tempAssetsQuery.delete();
await u.db("t_video").whereIn("scriptId", scriptIds).delete();
await u.db("o_video").whereIn("scriptId", scriptIds).delete();
await u.db("t_chatHistory").where("projectId", id).delete();
await u.db("o_chatHistory").where("projectId", id).delete();
try {
await u.oss.deleteDirectory(`${id}/`);

View File

@ -0,0 +1,32 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 新增项目
export default router.post(
"/",
validateFields({
id: z.number(),
name: z.string(),
intro: z.string(),
type: z.string(),
artStyle: z.string(),
videoRatio: z.string(),
}),
async (req, res) => {
const { id, name, intro, type, artStyle, videoRatio } = req.body;
await u.db("o_project").where("id", id).update({
name,
intro,
type,
artStyle,
videoRatio,
});
res.status(200).send(success({ message: "新增项目成功" }));
},
);

View File

@ -1,12 +1,10 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取项目
export default router.post("/", async (req, res) => {
const data = await u.db("t_project").select("*");
const data = await u.db("o_project").select("*");
res.status(200).send(success(data));
});

View File

@ -1,13 +0,0 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
import { v4 as uuid } from "uuid";
const router = express.Router();
// 获取提示词
export default router.get("/", async (req, res) => {
const data = await u.db("t_prompts");
res.status(200).send(success(data));
});

View File

@ -1,28 +0,0 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
const router = express.Router();
// 更新提示词
export default router.post(
"/",
validateFields({
id: z.number(),
customValue: z.string(),
code: z.string(),
}),
async (req, res) => {
const { id, customValue, code } = req.body;
await u
.db("t_prompts")
.update({
customValue: customValue,
})
.where("id", id);
res.status(200).send(success({ message: "更新提示词成功" }));
},
);

View File

@ -0,0 +1,26 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 新增剧本
export default router.post(
"/",
validateFields({
name: z.string(),
content: z.string(),
projectId: z.number(),
}),
async (req, res) => {
const { name, content, projectId } = req.body;
await u.db("o_script").insert({
name,
content,
projectId,
createTime: Date.now(),
});
res.status(200).send(success({ message: "添加剧本成功" }));
},
);

View File

@ -0,0 +1,19 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 删除剧本
export default router.post(
"/",
validateFields({
id: z.number(),
}),
async (req, res) => {
const { id } = req.body;
await u.db("o_script").where({ id }).delete();
res.status(200).send(success({ message: "删除剧本成功" }));
},
);

View File

@ -1,72 +0,0 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { z } from "zod";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
interface Asset {
id: number;
type: string; // "角色" 或其他
name: string;
filePath: string;
}
interface ScriptRow {
id: number;
name: string;
content: string;
outlineId: number;
projectId: number;
data: string;
}
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const { projectId } = req.body;
//查询剧本和大纲数据
const rows: ScriptRow[] = await u
.db("t_outline")
.leftJoin("t_script", "t_outline.id", "t_script.outlineId")
.where("t_outline.projectId", projectId)
.select("t_script.id", "t_script.name", "t_script.content", "t_script.outlineId", "t_script.projectId", "t_outline.data");
// 查询所有的资产
const assets: Asset[] = await u
.db("t_assets")
.where("projectId", projectId)
.andWhere("type", "<>", "分镜")
.select("id", "type", "name", "filePath", "intro", "prompt");
const data = rows.map((item) => {
const parseData = JSON.parse(item.data);
const charData = parseData.characters.map((i: Asset) => i.name);
const propsData = parseData.props.map((i: Asset) => i.name);
const sceneData = parseData.scenes.map((i: Asset) => i.name);
return {
...item,
element: [
...assets.filter((i) => i.type == "道具" && propsData.includes(i.name)),
...assets.filter((i) => i.type == "角色" && charData.includes(i.name)),
...assets.filter((i) => i.type == "场景" && sceneData.includes(i.name)),
],
};
});
await Promise.all(
data.map(async (script) => {
await Promise.all(
script.element.map(async (el) => {
el.filePath = el.filePath ? await u.oss.getFileUrl(el.filePath) : "";
})
);
})
);
res.status(200).send(success(data));
}
);

View File

@ -1,60 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { generateScript } from "@/utils/generateScript";
const router = express.Router();
interface NovelChapter {
id: number;
reel: string;
chapter: string;
chapterData: string;
projectId: number;
}
function mergeNovelText(novelData: NovelChapter[]): string {
if (!Array.isArray(novelData)) return "";
return novelData
.map((chap) => {
return `${chap.chapter.trim()}\n\n${chap.chapterData.trim().replace(/\r?\n/g, "\n")}\n`;
})
.join("\n");
}
// 生成剧本
export default router.post(
"/",
validateFields({
outlineId: z.number(),
scriptId: z.number(),
}),
async (req, res) => {
const { outlineId, scriptId } = req.body;
const outlineData = await u.db("t_outline").where("id", outlineId).select("*").first();
if (!outlineData) return res.status(500).send(success({ message: "大纲为空" }));
const parameter = JSON.parse(outlineData.data!);
const novelData = (await u
.db("t_novel")
.whereIn("chapterIndex", parameter.chapterRange)
.where("projectId", outlineData.projectId)
.select("*")) as NovelChapter[];
if (novelData.length == 0) return res.status(500).send(success({ message: "原文为空" }));
const result: string = mergeNovelText(novelData);
try {
const data = await generateScript(parameter ?? "", result ?? "");
if (!data) return res.status(500).send({ message: "生成剧本失败" });
await u.db("t_script").where("id", scriptId).update({
content: data,
});
res.status(200).send(success({ message: "生成剧本成功" }));
} catch (e) {
const errMsg = u.error(e).message || "生成剧本失败";
res.status(500).send(error(errMsg));
}
},
);

View File

@ -1,26 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { generateScript } from "@/utils/generateScript";
const router = express.Router();
// 生成剧本
export default router.post(
"/",
validateFields({
outlineId: z.number(),
scriptId: z.number(),
content: z.string(),
}),
async (req, res) => {
const { outlineId, scriptId, content } = req.body;
await u.db("t_script").where("id", scriptId).update({
content: content,
});
res.status(200).send(success({ message: "保存成功" }));
},
);

View File

@ -0,0 +1,23 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
projectId: z.number(),
name: z.string().optional(),
}),
async (req, res) => {
const { projectId, name } = req.body;
let query = u.db("o_script").where("projectId", projectId).select("*");
if (name) {
query = query.andWhere("name", "like", `%${name}%`);
}
const data = await query;
res.status(200).send(success(data));
},
);

View File

@ -0,0 +1,24 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 编辑剧本
export default router.post(
"/",
validateFields({
id: z.number(),
name: z.string(),
content: z.string(),
}),
async (req, res) => {
const { id, name, content } = req.body;
await u.db("o_script").where({ id }).update({
name,
content,
});
res.status(200).send(success({ message: "编辑剧本成功" }));
},
);

View File

@ -1,33 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
type: z.enum(["text", "video", "image"]),
model: z.string(),
baseUrl: z.string(),
apiKey: z.string(),
modelType: z.string(),
manufacturer: z.string(),
}),
async (req, res) => {
const { type, model, baseUrl, apiKey, manufacturer, modelType } = req.body;
await u.db("t_config").insert({
type,
model,
baseUrl,
apiKey,
manufacturer,
modelType,
createTime: Date.now(),
userId: 1,
});
res.status(200).send(success("新增成功"));
},
);

View File

@ -0,0 +1,19 @@
import express from "express";
import { success } from "@/lib/responseFormat";
import u from "@/utils";
import { z } from "zod";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post("/", validateFields({
id: z.number(),
name: z.string(),
model: z.string(),
modelName: z.string(),
vendorId: z.number().nullable(),
desc: z.string(),
}), async (req, res) => {
const { id, name, model, modelName, vendorId, desc } = req.body;
await u.db("o_agentDeploy").where({ id }).update({ id, name, model, modelName, vendorId, desc });
res.status(200).send(success("配置成功"));
});

View File

@ -0,0 +1,9 @@
import express from "express";
import { success } from "@/lib/responseFormat";
import u from "@/utils";
const router = express.Router();
export default router.post("/", async (req, res) => {
const data = await u.db("o_agentDeploy").leftJoin("o_vendorConfig", "o_vendorConfig.id", "o_agentDeploy.vendorId").select("o_agentDeploy.*", "o_vendorConfig.icon");
res.status(200).send(success(data));
});

View File

@ -0,0 +1,116 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
import { transform } from "sucrase";
const router = express.Router();
const vendorConfigSchema = z.object({
version: z.number(),
icon: z.string().optional(),
name: z.string(),
inputs: z.array(
z.object({
key: z.string(),
label: z.string(),
type: z.enum(["text", "password", "url"]),
required: z.boolean(),
placeholder: z.string().optional(),
}),
),
inputValues: z.record(z.string(), z.string()),
models: z.array(
z.discriminatedUnion("type", [
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("text"),
multimodal: z.boolean(),
tool: z.boolean(),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("image"),
mode: z.array(z.enum(["text", "singleImage", "multiReference"])),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("video"),
mode: z.array(
z.enum([
"singleImage",
"multiImage",
"gridImage",
"startEndRequired",
"endFrameOptional",
"startFrameOptional",
"text",
"audioReference",
"videoReference",
]),
),
audio: z.union([z.literal("optional"), z.boolean()]),
durationResolutionMap: z.array(
z.object({
duration: z.array(z.number()),
resolution: z.array(z.string()),
}),
),
}),
]),
),
});
export default router.post("/",
validateFields({
id: z.number(),
key: z.string(),
}),
async (req, res) => {
const { id, key } = req.body;
const data = await u.db("o_vendorConfig").where("id", id).select("code", "inputValues").first();
let inputValues = JSON.parse(data?.inputValues || "{}");
inputValues.apiKey = key;
const jsCode = transform(data?.code!, { transforms: ["typescript"] }).code;
const exports = u.vm(jsCode);
if (!exports) return res.status(400).send(success("脚本文件必须导出对象"));
if (!exports.textRequest) return res.status(400).send(success("脚本文件必须导出文本请求对象"));
if (!exports.imageRequest) return res.status(400).send(success("脚本文件必须导出图像请求对象"));
if (!exports.videoRequest) return res.status(400).send(success("脚本文件必须导出视频请求对象"));
if (!exports.vendor) return res.status(400).send(success("脚本文件必须导出vendor对象"));
const vendor = exports.vendor;
const result = vendorConfigSchema.safeParse(vendor);
if (!result.success) {
const errorMsg = result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
return res.status(400).send(error(`vendor配置校验失败: ${errorMsg}`));
}
const replaceBlockValue = (code: string, key: string, newValue: string): string => {
const open = newValue.trimStart()[0] as "[" | "{";
const close = open === "[" ? "]" : "}";
const keyMatch = code.match(new RegExp(`\\b${key}\\s*:\\s*[\\[{]`));
if (!keyMatch || keyMatch.index === undefined) return code;
const valueStart = keyMatch.index + keyMatch[0].length - 1;
let depth = 0;
let valueEnd = -1;
for (let i = valueStart; i < code.length; i++) {
if (code[i] === open) depth++;
else if (code[i] === close) {
depth--;
if (depth === 0) {
valueEnd = i;
break;
}
}
}
if (valueEnd === -1) return code;
return code.slice(0, valueStart) + newValue + code.slice(valueEnd + 1);
};
let updatedTsCode = data?.code!;
updatedTsCode = replaceBlockValue(updatedTsCode, "inputValues", JSON.stringify(inputValues ?? vendor.inputValues, null, 2));
await u.db("o_vendorConfig").where("id", id).update({
inputValues: inputValues ? JSON.stringify(inputValues) : JSON.stringify(vendor.inputValues),
code: updatedTsCode,
});
res.status(200).send(success("保存成功"));
});

View File

@ -1,23 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.number(),
configId: z.number(),
}),
async (req, res) => {
const { id, configId } = req.body;
if (id) {
await u.db("t_aiModelMap").where("id", id).update({
configId,
});
}
res.status(200).send(success("配置成功"));
},
);

View File

@ -0,0 +1,29 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { db } from "@/utils/db";
import initDB from "@/lib/initDB";
const router = express.Router();
export default router.get("/", async (req, res) => {
try {
// 获取所有表名
const tables: { name: string }[] = await db.raw(
`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'knex_%'`,
);
// 禁用外键约束,逐一删除所有表
await db.raw("PRAGMA foreign_keys = OFF");
for (const table of tables) {
await db.schema.dropTableIfExists(table.name);
}
await db.raw("PRAGMA foreign_keys = ON");
// 重新初始化数据库
await initDB(db as any);
res.status(200).send(success("数据库已清空并重新初始化"));
} catch (err: any) {
res.status(500).send(error(err?.message || "清除失败"));
}
});

View File

@ -1,19 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.number(),
}),
async (req, res) => {
const { id } = req.body;
await u.db("t_config").where("id", id).delete();
await u.db("t_aiModelMap").where("configId", id).update("configId",null);
res.status(200).send(success("删除成功"));
},
);

View File

@ -1,42 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
type: z.enum(["text", "image", "video"]),
}),
async (req, res) => {
const { type } = req.body;
const sqlTableMap = {
text: "t_textModel",
image: "t_imageModel",
video: "t_videoModel",
};
const modelLists = await u
.db(sqlTableMap[type as "image" | "text" | "video"])
.whereNot("manufacturer", "other")
.select("id", "manufacturer", "model");
const result: Record<string, any[]> = {};
const modelCache: Record<string, Set<string>> = {};
for (const row of modelLists) {
if (!result[row.manufacturer]) {
result[row.manufacturer] = [];
modelCache[row.manufacturer] = new Set();
}
if (!modelCache[row.manufacturer].has(row.model)) {
result[row.manufacturer].push({ label: row.model, value: row.model });
modelCache[row.manufacturer].add(row.model);
}
}
res.status(200).send(success(result));
},
);

View File

@ -1,13 +0,0 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
const router = express.Router();
export default router.post("/", async (req, res) => {
const configData = await u
.db("t_aiModelMap")
.leftJoin("t_config", "t_aiModelMap.configId", "t_config.id")
.select("t_aiModelMap.name", "t_config.model", "t_aiModelMap.id", "t_aiModelMap.key", "t_config.manufacturer");
res.status(200).send(success(configData));
});

View File

@ -1,17 +0,0 @@
import logger from "@/logger";
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post("/", async (req, res) => {
const { id } = (req as any).user;
if (id !== 1) return res.status(400).send(error("无权限查看仅管理员USERID=1可见"));
const logs = logger.exportLogs();
res.status(200).send(success(logs));
});

View File

@ -1,11 +0,0 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
const router = express.Router();
export default router.post("/", async (req, res) => {
const userId = 1;
const configData = await u.db("t_config").where("type","<>","video").where("userId", userId).select("*");
res.status(200).send(success(configData));
});

View File

@ -0,0 +1,10 @@
import express from "express";
import { success } from "@/lib/responseFormat";
const router = express.Router();
export default router.post(
"/",
async (req, res) => {
res.status(200).send(success("123"));
},
);

View File

@ -1,31 +0,0 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
const router = express.Router();
export default router.post("/", async (req, res) => {
const videoData = await u.db("t_videoModel").select("*");
const allData = videoData.map((i) => {
const durationResolutionMap = JSON.parse(i.durationResolutionMap ?? "[]");
const aspectRatio = JSON.parse(i.aspectRatio ?? "[]");
const type = JSON.parse(i.type ?? "[]");
return {
...i,
durationResolutionMap,
aspectRatio,
type,
audio: i.audio === 1,
};
});
const otherConfig = {
manufacturer: "other",
model: "",
durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["480p", "720p", "1080p"] }],
aspectRatio: ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"],
type: ["text", "endFrameOptional", "singleImage", "multiImage"],
audio: true,
};
const returnData = [otherConfig, ...allData];
res.status(200).send(success(returnData));
});

View File

@ -1,11 +0,0 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
const router = express.Router();
export default router.post("/", async (req, res) => {
const userId = 1;
const configData = await u.db("t_config").where("type","video").where("userId", userId).select("*");
res.status(200).send(success(configData));
});

Some files were not shown because too many files have changed in this diff Show More