From fbe1ff6c6bcf7983fda419b0c761b95b391560ea Mon Sep 17 00:00:00 2001 From: zhishi <1951671751@qq.com> Date: Mon, 23 Mar 2026 17:38:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=8B=E4=BB=B6=E5=88=86?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/initDB.ts | 2 + src/router.ts | 156 +++++++++--------- src/routes/novel/addNovel.ts | 20 ++- src/routes/novel/event/generateEvents.ts | 83 ++-------- src/routes/novel/event/getEvent.ts | 12 +- src/routes/novel/getNovel.ts | 6 +- src/routes/novel/getNovelEventState.ts | 19 +++ src/routes/novel/getNovelIndex.ts | 19 +++ src/routes/novel/updateNovel.ts | 4 +- src/routes/production/assets/getAssetsData.ts | 60 +++++++ src/types/database.d.ts | 14 +- src/utils/cleanNovel.ts | 141 +++------------- 12 files changed, 259 insertions(+), 277 deletions(-) create mode 100644 src/routes/novel/getNovelEventState.ts create mode 100644 src/routes/novel/getNovelIndex.ts create mode 100644 src/routes/production/assets/getAssetsData.ts diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index 10d796f..6a2bb5d 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -202,6 +202,8 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => table.text("chapter"); table.text("chapterData"); table.integer("projectId"); + table.integer("eventState"); + table.text("event"); table.integer("createTime"); table.primary(["id"]); table.unique(["id"]); diff --git a/src/router.ts b/src/router.ts index 9552da9..cf34e88 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash 3d0673061005074e704638efccc539d1 +// @routes-hash 5b4dcd9ee97003a7a3cc25c24e5e7505 import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -31,43 +31,46 @@ import route27 from "./routes/novel/event/deletEvent"; import route28 from "./routes/novel/event/generateEvents"; import route29 from "./routes/novel/event/getEvent"; import route30 from "./routes/novel/getNovel"; -import route31 from "./routes/novel/updateNovel"; -import route32 from "./routes/other/deleteAllData"; -import route33 from "./routes/other/getCaptcha"; -import route34 from "./routes/production/editStoryboard/generateStoryboardImage"; -import route35 from "./routes/production/editStoryboard/getStoryboardFlow"; -import route36 from "./routes/production/editStoryboard/saveStoryboardFlow"; -import route37 from "./routes/production/editStoryboard/updateStoryboardFlow"; -import route38 from "./routes/production/getProductionData"; -import route39 from "./routes/production/getStoryboardData"; -import route40 from "./routes/production/workbench/generateVideo"; -import route41 from "./routes/production/workbench/getVideoModelDetail"; -import route42 from "./routes/project/addProject"; -import route43 from "./routes/project/delProject"; -import route44 from "./routes/project/editProject"; -import route45 from "./routes/project/getProject"; -import route46 from "./routes/script/addScript"; -import route47 from "./routes/script/delScript"; -import route48 from "./routes/script/getScrptApi"; -import route49 from "./routes/script/updateScript"; -import route50 from "./routes/setting/agentDeploy/deployAgentModel"; -import route51 from "./routes/setting/agentDeploy/getAgentDeploy"; -import route52 from "./routes/setting/agentDeploy/updateKey"; -import route53 from "./routes/setting/dbConfig/clearData"; -import route54 from "./routes/setting/getTextModel"; -import route55 from "./routes/setting/loginConfig/getUser"; -import route56 from "./routes/setting/loginConfig/updateUserPwd"; -import route57 from "./routes/setting/memoryConfig/getMemory"; -import route58 from "./routes/setting/memoryConfig/sureMemory"; -import route59 from "./routes/setting/vendorConfig/addVendor"; -import route60 from "./routes/setting/vendorConfig/deleteVendor"; -import route61 from "./routes/setting/vendorConfig/getVendorList"; -import route62 from "./routes/setting/vendorConfig/modelTest"; -import route63 from "./routes/setting/vendorConfig/updateVendor"; -import route64 from "./routes/task/getMyTaskApi"; -import route65 from "./routes/task/getTaskCategories"; -import route66 from "./routes/task/taskDetails"; -import route67 from "./routes/test/test"; +import route31 from "./routes/novel/getNovelEventState"; +import route32 from "./routes/novel/getNovelIndex"; +import route33 from "./routes/novel/updateNovel"; +import route34 from "./routes/other/deleteAllData"; +import route35 from "./routes/other/getCaptcha"; +import route36 from "./routes/production/assets/getAssetsData"; +import route37 from "./routes/production/editStoryboard/generateStoryboardImage"; +import route38 from "./routes/production/editStoryboard/getStoryboardFlow"; +import route39 from "./routes/production/editStoryboard/saveStoryboardFlow"; +import route40 from "./routes/production/editStoryboard/updateStoryboardFlow"; +import route41 from "./routes/production/getProductionData"; +import route42 from "./routes/production/getStoryboardData"; +import route43 from "./routes/production/workbench/generateVideo"; +import route44 from "./routes/production/workbench/getVideoModelDetail"; +import route45 from "./routes/project/addProject"; +import route46 from "./routes/project/delProject"; +import route47 from "./routes/project/editProject"; +import route48 from "./routes/project/getProject"; +import route49 from "./routes/script/addScript"; +import route50 from "./routes/script/delScript"; +import route51 from "./routes/script/getScrptApi"; +import route52 from "./routes/script/updateScript"; +import route53 from "./routes/setting/agentDeploy/deployAgentModel"; +import route54 from "./routes/setting/agentDeploy/getAgentDeploy"; +import route55 from "./routes/setting/agentDeploy/updateKey"; +import route56 from "./routes/setting/dbConfig/clearData"; +import route57 from "./routes/setting/getTextModel"; +import route58 from "./routes/setting/loginConfig/getUser"; +import route59 from "./routes/setting/loginConfig/updateUserPwd"; +import route60 from "./routes/setting/memoryConfig/getMemory"; +import route61 from "./routes/setting/memoryConfig/sureMemory"; +import route62 from "./routes/setting/vendorConfig/addVendor"; +import route63 from "./routes/setting/vendorConfig/deleteVendor"; +import route64 from "./routes/setting/vendorConfig/getVendorList"; +import route65 from "./routes/setting/vendorConfig/modelTest"; +import route66 from "./routes/setting/vendorConfig/updateVendor"; +import route67 from "./routes/task/getMyTaskApi"; +import route68 from "./routes/task/getTaskCategories"; +import route69 from "./routes/task/taskDetails"; +import route70 from "./routes/test/test"; export default async (app: Express) => { app.use("/api/agents/clearMemory", route1); @@ -100,41 +103,44 @@ export default async (app: Express) => { app.use("/api/novel/event/generateEvents", route28); app.use("/api/novel/event/getEvent", route29); app.use("/api/novel/getNovel", route30); - app.use("/api/novel/updateNovel", route31); - app.use("/api/other/deleteAllData", route32); - app.use("/api/other/getCaptcha", route33); - app.use("/api/production/editStoryboard/generateStoryboardImage", route34); - app.use("/api/production/editStoryboard/getStoryboardFlow", route35); - app.use("/api/production/editStoryboard/saveStoryboardFlow", route36); - app.use("/api/production/editStoryboard/updateStoryboardFlow", route37); - app.use("/api/production/getProductionData", route38); - app.use("/api/production/getStoryboardData", route39); - app.use("/api/production/workbench/generateVideo", route40); - app.use("/api/production/workbench/getVideoModelDetail", route41); - app.use("/api/project/addProject", route42); - app.use("/api/project/delProject", route43); - app.use("/api/project/editProject", route44); - app.use("/api/project/getProject", route45); - app.use("/api/script/addScript", route46); - app.use("/api/script/delScript", route47); - app.use("/api/script/getScrptApi", route48); - app.use("/api/script/updateScript", route49); - app.use("/api/setting/agentDeploy/deployAgentModel", route50); - app.use("/api/setting/agentDeploy/getAgentDeploy", route51); - app.use("/api/setting/agentDeploy/updateKey", route52); - app.use("/api/setting/dbConfig/clearData", route53); - app.use("/api/setting/getTextModel", route54); - app.use("/api/setting/loginConfig/getUser", route55); - app.use("/api/setting/loginConfig/updateUserPwd", route56); - app.use("/api/setting/memoryConfig/getMemory", route57); - app.use("/api/setting/memoryConfig/sureMemory", route58); - app.use("/api/setting/vendorConfig/addVendor", route59); - app.use("/api/setting/vendorConfig/deleteVendor", route60); - app.use("/api/setting/vendorConfig/getVendorList", route61); - app.use("/api/setting/vendorConfig/modelTest", route62); - app.use("/api/setting/vendorConfig/updateVendor", route63); - app.use("/api/task/getMyTaskApi", route64); - app.use("/api/task/getTaskCategories", route65); - app.use("/api/task/taskDetails", route66); - app.use("/api/test/test", route67); + app.use("/api/novel/getNovelEventState", route31); + app.use("/api/novel/getNovelIndex", route32); + app.use("/api/novel/updateNovel", route33); + app.use("/api/other/deleteAllData", route34); + app.use("/api/other/getCaptcha", route35); + app.use("/api/production/assets/getAssetsData", route36); + app.use("/api/production/editStoryboard/generateStoryboardImage", route37); + app.use("/api/production/editStoryboard/getStoryboardFlow", route38); + app.use("/api/production/editStoryboard/saveStoryboardFlow", route39); + app.use("/api/production/editStoryboard/updateStoryboardFlow", route40); + app.use("/api/production/getProductionData", route41); + app.use("/api/production/getStoryboardData", route42); + app.use("/api/production/workbench/generateVideo", route43); + app.use("/api/production/workbench/getVideoModelDetail", route44); + app.use("/api/project/addProject", route45); + app.use("/api/project/delProject", route46); + app.use("/api/project/editProject", route47); + app.use("/api/project/getProject", route48); + app.use("/api/script/addScript", route49); + app.use("/api/script/delScript", route50); + app.use("/api/script/getScrptApi", route51); + app.use("/api/script/updateScript", route52); + app.use("/api/setting/agentDeploy/deployAgentModel", route53); + app.use("/api/setting/agentDeploy/getAgentDeploy", route54); + app.use("/api/setting/agentDeploy/updateKey", route55); + app.use("/api/setting/dbConfig/clearData", route56); + app.use("/api/setting/getTextModel", route57); + app.use("/api/setting/loginConfig/getUser", route58); + app.use("/api/setting/loginConfig/updateUserPwd", route59); + app.use("/api/setting/memoryConfig/getMemory", route60); + app.use("/api/setting/memoryConfig/sureMemory", route61); + app.use("/api/setting/vendorConfig/addVendor", route62); + app.use("/api/setting/vendorConfig/deleteVendor", route63); + app.use("/api/setting/vendorConfig/getVendorList", route64); + app.use("/api/setting/vendorConfig/modelTest", route65); + app.use("/api/setting/vendorConfig/updateVendor", route66); + app.use("/api/task/getMyTaskApi", route67); + app.use("/api/task/getTaskCategories", route68); + app.use("/api/task/taskDetails", route69); + app.use("/api/test/test", route70); } diff --git a/src/routes/novel/addNovel.ts b/src/routes/novel/addNovel.ts index 622c936..c3fce63 100644 --- a/src/routes/novel/addNovel.ts +++ b/src/routes/novel/addNovel.ts @@ -16,23 +16,35 @@ export default router.post( reel: z.string(), chapter: z.string(), chapterData: z.string(), - }) + }), ), }), async (req, res) => { const { projectId, data } = req.body; - + const totalNovelId = []; for (const item of data) { - await u.db("o_novel").insert({ + const [id] = await u.db("o_novel").insert({ projectId, chapterIndex: item.index, reel: item.reel, chapter: item.chapter, chapterData: item.chapterData, createTime: Date.now(), + eventState: 0, }); + totalNovelId.push(id); } + const chapterAllList = await u.db("o_novel").where("projectId", projectId).whereIn("id", totalNovelId); + const novelClass = new u.cleanNovel(); + novelClass.emitter.on("item", async (item) => { + if (item.event) + await u + .db("o_novel") + .where("id", item.id) + .update({ event: item.event, eventState: item.event ? 1 : -1 }); + }); + novelClass.start(chapterAllList, projectId); res.status(200).send(success({ message: "新增原文成功" })); - } + }, ); diff --git a/src/routes/novel/event/generateEvents.ts b/src/routes/novel/event/generateEvents.ts index 3577bd8..ee63e56 100644 --- a/src/routes/novel/event/generateEvents.ts +++ b/src/routes/novel/event/generateEvents.ts @@ -6,84 +6,31 @@ 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 + novelIds: z.array(z.number()), }), async (req, res) => { - const { projectId, windowSize, overlap } = req.body; - //删除之前的事件 + const { projectId, novelIds } = req.body; + const [allChapters, novel] = await Promise.all([ - u.db("o_novel").where("projectId", projectId), - Promise.resolve(new u.cleanNovel(windowSize, overlap)), + u.db("o_novel").where("projectId", projectId).whereIn("id", novelIds), + Promise.resolve(new u.cleanNovel()), ]); - 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_novel").where("projectId", projectId).update({ eventState: 0, event: null }); + novel.emitter.on("item", async (item) => { + if (item.event) + await u + .db("o_novel") + .where("id", item.id) + .update({ event: item.event, eventState: item.event ? 1 : -1 }); + }); + novel.start(allChapters, projectId); - 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)); + return res.status(200).send(success("生成事件成功")); }, ); diff --git a/src/routes/novel/event/getEvent.ts b/src/routes/novel/event/getEvent.ts index 7a60962..69f4462 100644 --- a/src/routes/novel/event/getEvent.ts +++ b/src/routes/novel/event/getEvent.ts @@ -6,10 +6,6 @@ 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({ @@ -43,13 +39,7 @@ export default router.post( // 分页查询:每个事件对应多个 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"), - ) + .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); diff --git a/src/routes/novel/getNovel.ts b/src/routes/novel/getNovel.ts index 22e4ce6..da4ebc6 100644 --- a/src/routes/novel/getNovel.ts +++ b/src/routes/novel/getNovel.ts @@ -13,7 +13,6 @@ export default router.post( page: z.number(), limit: z.number(), search: z.string().optional(), - }), async (req, res) => { const { projectId, page, limit, search } = req.body; @@ -21,7 +20,7 @@ export default router.post( const data = await u .db("o_novel") .where("projectId", projectId) - .select("id", "chapterIndex as index", "reel", "chapter", "chapterData") + .select("id", "chapterIndex as index", "reel", "chapter", "chapterData", "event", "eventState") .andWhere((qb) => { if (search) { qb.where("chapter", "like", `%${search}%`); @@ -30,6 +29,7 @@ export default router.post( .orderBy("chapterIndex", "asc") .limit(limit) .offset(offset); + // 统计总数 const totalQuery = (await u .db("o_novel") @@ -43,5 +43,5 @@ export default router.post( .first()) as any; res.status(200).send(success({ data, total: totalQuery.total })); - } + }, ); diff --git a/src/routes/novel/getNovelEventState.ts b/src/routes/novel/getNovelEventState.ts new file mode 100644 index 0000000..3593c8c --- /dev/null +++ b/src/routes/novel/getNovelEventState.ts @@ -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({ + ids: z.array(z.number()), + }), + async (req, res) => { + const { ids } = req.body; + const data = await u.db("o_novel").whereIn("id", ids).whereNot("eventState", 0).select("id", "event", "eventState"); + res.status(200).send(success(data)); + }, +); diff --git a/src/routes/novel/getNovelIndex.ts b/src/routes/novel/getNovelIndex.ts new file mode 100644 index 0000000..b5cfbd4 --- /dev/null +++ b/src/routes/novel/getNovelIndex.ts @@ -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({ + projectId: z.number(), + }), + async (req, res) => { + const { projectId } = req.body; + const data = await u.db("o_novel").where("projectId", projectId).select("id", "chapterIndex as index", "chapter").orderBy("chapterIndex", "asc"); + + res.status(200).send(success(data)); + }, +); diff --git a/src/routes/novel/updateNovel.ts b/src/routes/novel/updateNovel.ts index 08f2d8a..3e9f435 100644 --- a/src/routes/novel/updateNovel.ts +++ b/src/routes/novel/updateNovel.ts @@ -14,15 +14,17 @@ export default router.post( reel: z.string(), chapter: z.string(), chapterData: z.string(), + event: z.string(), }), async (req, res) => { - const { id, index, reel, chapter, chapterData } = req.body; + const { id, index, reel, chapter, chapterData, event } = req.body; await u.db("o_novel").where("id", id).update({ chapterIndex: index, reel, chapter, chapterData, + event: event, }); res.status(200).send(success({ message: "更新原文成功" })); diff --git a/src/routes/production/assets/getAssetsData.ts b/src/routes/production/assets/getAssetsData.ts new file mode 100644 index 0000000..e992603 --- /dev/null +++ b/src/routes/production/assets/getAssetsData.ts @@ -0,0 +1,60 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import { success } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import { o_assets } from "@/types/database"; +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + projectId: z.number(), + }), + async (req, res) => { + const { projectId } = req.body; + const parentAssetsData = await u.db("o_assets").where("projectId", projectId).whereNotNull("sonId"); + const parentIds = parentAssetsData.map((i) => i.id); + const parnetIdsMap: Record = {}; + const sonAssetsData = await u.db("o_assets").whereIn("sonId", parentIds); + const sonAssetsMap: Record = {}; + + const imageIds = [...parentAssetsData.map((i) => i.imageId).concat(sonAssetsData.map((i) => i.imageId))].filter(Boolean); + const imagePaths = await u + .db("o_image") + .whereIn("id", imageIds as unknown as string[]) + .select("id", "filePath"); + const imageSignUrls = await Promise.all( + imagePaths.map(async (i) => { + return { id: i.id, src: i.filePath ? await u.oss.getFileUrl(i.filePath) : null }; + }), + ); + const imageUrlMap: Record = {}; + imageSignUrls.forEach((i, index) => { + imageUrlMap[i.id!] = i.src; + }); + sonAssetsData.forEach((i) => { + if (!sonAssetsMap[i.sonId!]) { + sonAssetsMap[i.sonId!] = []; + } + const obj = { + assetsId: i.id, + name: i.name, + desc: i.describe, + src: imageUrlMap[i.imageId!] ?? null, + derive: sonAssetsMap[i.id!] ?? [], + }; + sonAssetsMap[i.sonId!].push(obj); + }); + const returnData = parentAssetsData.map((i) => { + return { + assetsId: i.id, + name: i.name, + desc: i.describe, + src: imageUrlMap[i.imageId!] ?? null, + derive: sonAssetsMap[i.id!] ?? [], + }; + }); + res.status(200).send(success(returnData)); + }, +); diff --git a/src/types/database.d.ts b/src/types/database.d.ts index d43ec7f..74e7f02 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,6 +1,15 @@ -// @db-hash 307e7d70184bd3663540410a66e5c54d +// @db-hash 3baf950505da72215da0b884d17c0ecb //该文件由脚本自动生成,请勿手动修改 +export interface _o_novel_old_20260323 { + 'chapter'?: string | null; + 'chapterData'?: string | null; + 'chapterIndex'?: number | null; + 'createTime'?: number | null; + 'id'?: number; + 'projectId'?: number | null; + 'reel'?: string | null; +} export interface _o_storyboard_old_20260321 { 'createTime'?: number | null; 'detail'?: string | null; @@ -128,6 +137,8 @@ export interface o_novel { 'chapterData'?: string | null; 'chapterIndex'?: number | null; 'createTime'?: number | null; + 'event'?: string | null; + 'eventState'?: number | null; 'id'?: number; 'projectId'?: number | null; 'reel'?: string | null; @@ -273,6 +284,7 @@ export interface o_videoConfig { } export interface DB { + "_o_novel_old_20260323": _o_novel_old_20260323; "_o_storyboard_old_20260321": _o_storyboard_old_20260321; "_o_storyboard_old_20260321_1": _o_storyboard_old_20260321_1; "memories": memories; diff --git a/src/utils/cleanNovel.ts b/src/utils/cleanNovel.ts index 64080d2..2867ee4 100644 --- a/src/utils/cleanNovel.ts +++ b/src/utils/cleanNovel.ts @@ -1,40 +1,13 @@ import * as z from "zod"; import { ModelMessage, Output } from "ai"; +import { EventEmitter } from "events"; import { o_novel } from "@/types/database"; import ai from "@/utils/ai"; import u from "@/utils"; export interface EventType { - name: string; - detail: string; - chapter: string; -} - -export interface Novel { - event: EventType[]; -} -// 章节拆分 -function getChapterGroups(chapters: T[], windowSize: number = 5, overlap: number = 1): T[][] { - const res: T[][] = []; - if (windowSize < 1 || overlap < 0) return res; - let i = 0; - const length = chapters.length; - while (i < length) { - if (res.length === 0) { - // 第一组,直接取 windowSize 个 - res.push(chapters.slice(i, i + windowSize)); - i += windowSize; - } else { - // 取上一组最后 overlap 个,加上新的 windowSize 个 - const prevGroup = res[res.length - 1]; - const overlapItems = prevGroup.slice(-overlap); - const newItems = chapters.slice(i, i + windowSize); - if (newItems.length === 0) break; // 已经取完,跳出 - res.push([...overlapItems, ...newItems]); - i += windowSize; - } - } - return res; + id: number; + event: string; } /* 文本数据清洗 @@ -45,42 +18,18 @@ function getChapterGroups(chapters: T[], windowSize: number = 5, overlap: num */ class CleanNovel { - windowSize: number; - overlap: number; - constructor(windowSize: number = 5, overlap: number = 1) { - this.windowSize = windowSize; - this.overlap = overlap; + emitter: EventEmitter; + constructor() { + this.emitter = new EventEmitter(); } async start(allChapters: o_novel[], projectId: number): Promise { - const groups = getChapterGroups(allChapters!, this.windowSize, this.overlap); - - let preData: Novel | null = null; //所有事件 let totalEvent: EventType[] = []; const intansce = u.Ai.Text("eventExtractAi"); try { - for (let gi = 0; gi < groups.length; gi++) { - const group = groups[gi]; - // 第一批没有交叠章节,后续批次前 overlap 个是交叠章节(仅作上下文,不输出事件) - const overlapCount = gi === 0 ? 0 : this.overlap; - const overlapChapterIndexes = group.slice(0, overlapCount).map((i) => i.chapterIndex); - - const cleanText = group - .map((i, index: number) => { - const isOverlap = overlapChapterIndexes.includes(i.chapterIndex); - return { - role: "user", - content: isOverlap - ? `【上文衔接章节,仅供上下文参考,禁止为本章生成情节单元】\n第${i.chapterIndex}章:\n\n${i.chapterData}` - : `第${i.chapterIndex}章:\n\n${i.chapterData}`, - } as ModelMessage; - }) - .filter(Boolean); - const taskRecord = await u.task(projectId, "事件提取", "gpt-4.1", { - describe: "根据小说原文,提取情节单元", - content: cleanText, - }); + for (let gi = 0; gi < allChapters.length; gi++) { + const novel = allChapters[gi]; let resData; try { resData = await intansce.invoke({ @@ -88,67 +37,31 @@ class CleanNovel { { role: "system", content: ` -你是专业剧本结构分析师,负责将用户提供的章节文本拆分为标准情节单元。请严格遵循以下规则执行。 - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -【情节单元拆分规则】 -━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -▍拆分粒度 -- 连续多个章节若属同一戏剧动作,可合并为 1 个情节单元,禁止过细拆分; -- 每个情节单元的 detail 字数控制在 100~200 字。 - -▍每个情节单元包含以下三个字段 -- chapter:事件覆盖的章节范围(如"1-3章"),每个章节只能归属一个事件; -- name:事件名称,须具体描述实际戏剧动作,禁止使用"XXX踏上征程""命运转折"等笼统标题; -- detail:事件过程详情,包含时间、地点、涉及人物、起因、经过、结果。 - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -【执行规则】 -━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -✅ 必须执行 -1. 所有章节按剧情顺序逐一覆盖,不得遗漏; -2. 标注为【上文衔接章节】的内容仅作上下文理解使用,禁止为其生成任何情节单元。 - -🚫 禁止出现 -- 笼统事件名称; -- 单个章节拆分为多个情节单元; -- 遗漏任何章节。 +你是一位专业的叙事结构分析师。 +请阅读以下小说章节,用**一段话**提炼本章的情节单元摘要。 +## 要求 +- 按事件发生顺序,串联本章核心情节节点 +- 突出人物行为、关键转折、因果关系 +- 语言简洁紧凑,100-150字以内 +- 不加主观评价,只陈述"发生了什么" +--- +【章节内容】: `, }, - ...cleanText, + { + role: "user", + content: novel.chapterData!, + }, ], - output: Output.object({ - schema: z.object({ - event: z.array( - z - .object({ - chapter: z - .string() - .describe( - "事件覆盖的章节(如1-3章、4-6章),章节划分必须连续,每个章节范围只能属于一个事件。事件分割不可过细——避免只描述琐碎、日常细节的微小事件。", - ), - name: z.string().describe("事件名称"), - detail: z.string().describe("事件过程详情(包括起因、经过、结果、场景、人物等)"), - }) - .describe("事件必须在100-200字说明起因经过结果,不可将单一章节或细小场景独立成事件,"), - ), - }), - }), }); + + const preData = resData.text; + + this.emitter.emit("item", { id: novel.id, event: preData }); + totalEvent.push({ id: novel.id!, event: preData }); } catch (e) { - taskRecord(-1, u.error(e).message); - throw e; + this.emitter.emit("item", { id: novel.id, event: null }); } - taskRecord(1); - - preData = JSON.parse(resData.text); - - const newEvents = preData?.event || []; - newEvents.forEach((newItem) => { - totalEvent.push({ ...newItem }); - }); } } catch (e) { console.error(e);