Merge branch '108' of https://github.com/HBAI-Ltd/Toonflow-app into 108
# Conflicts: # src/agents/scriptAgent/index.ts # src/router.ts # src/routes/assets/uploadClip.ts # src/types/database.d.ts
This commit is contained in:
commit
5550e513a9
@ -11,8 +11,10 @@ export interface AgentContext {
|
||||
socket: Socket;
|
||||
isolationKey: string;
|
||||
text: string;
|
||||
userMessageTime?: number;
|
||||
abortSignal?: AbortSignal;
|
||||
resTool: ResTool;
|
||||
msg: ReturnType<ResTool["newMessage"]>;
|
||||
}
|
||||
|
||||
function buildSystemPrompt(skillPrompt: string, mem: Awaited<ReturnType<Memory["get"]>>): string {
|
||||
@ -35,19 +37,16 @@ function buildSystemPrompt(skillPrompt: string, mem: Awaited<ReturnType<Memory["
|
||||
const subAgentList = ["executionAI", "supervisionAI"] as const;
|
||||
|
||||
export async function decisionAI(ctx: AgentContext) {
|
||||
const { isolationKey, text, abortSignal, resTool } = ctx;
|
||||
|
||||
resTool.systemMessage("决策层AI 接管聊天");
|
||||
const { isolationKey, text, userMessageTime, abortSignal, resTool } = ctx;
|
||||
|
||||
const memory = new Memory("scriptAgent", isolationKey);
|
||||
await memory.add("user", text);
|
||||
await memory.add("user", text, { createTime: userMessageTime });
|
||||
const [skill, mem] = await Promise.all([useSkill("script_agent_decision.md"), memory.get(text)]);
|
||||
|
||||
const systemPrompt = buildSystemPrompt(skill.prompt, mem);
|
||||
|
||||
const projectData = await u.db("o_project").where("id", resTool.data.projectId).first();
|
||||
const novelData = await u.db("o_novel").where("projectId", resTool.data.projectId).select("id", "chapterIndex as index");
|
||||
console.log("%c Line:50 🥒 novelData", "background:#2eafb0", novelData);
|
||||
|
||||
const projectInfo = [
|
||||
"## 项目信息",
|
||||
@ -70,9 +69,6 @@ export async function decisionAI(ctx: AgentContext) {
|
||||
run_sub_agent: runSubAgent(ctx),
|
||||
...useTools(ctx.resTool),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:decision", completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
return textStream;
|
||||
@ -82,9 +78,6 @@ export async function decisionAI(ctx: AgentContext) {
|
||||
|
||||
export async function executionAI(ctx: AgentContext) {
|
||||
const { isolationKey, text, abortSignal, resTool } = ctx;
|
||||
|
||||
resTool.systemMessage("执行层AI 接管聊天");
|
||||
|
||||
const memory = new Memory("scriptAgent", isolationKey);
|
||||
const [skill, mem] = await Promise.all([useSkill("script_agent_execution.md"), memory.get(text)]);
|
||||
|
||||
@ -99,9 +92,6 @@ export async function executionAI(ctx: AgentContext) {
|
||||
...memory.getTools(),
|
||||
...useTools(ctx.resTool),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:execution", completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
return textStream;
|
||||
@ -110,8 +100,6 @@ export async function executionAI(ctx: AgentContext) {
|
||||
export async function supervisionAI(ctx: AgentContext) {
|
||||
const { isolationKey, text, abortSignal, resTool } = ctx;
|
||||
|
||||
resTool.systemMessage("监督层AI 接管聊天");
|
||||
|
||||
const memory = new Memory("scriptAgent", isolationKey);
|
||||
const [skill, mem] = await Promise.all([useSkill("script_agent_supervision.md"), memory.get(text)]);
|
||||
|
||||
@ -125,9 +113,6 @@ export async function supervisionAI(ctx: AgentContext) {
|
||||
...skill.tools,
|
||||
...useTools(ctx.resTool),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:supervision", completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
return textStream;
|
||||
@ -135,6 +120,7 @@ export async function supervisionAI(ctx: AgentContext) {
|
||||
|
||||
//工具函数
|
||||
function runSubAgent(parentCtx: AgentContext) {
|
||||
const memory = new Memory("scriptAgent", parentCtx.isolationKey);
|
||||
return tool({
|
||||
description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI",
|
||||
inputSchema: z.object({
|
||||
@ -143,17 +129,30 @@ function runSubAgent(parentCtx: AgentContext) {
|
||||
}),
|
||||
execute: async ({ agent, prompt }) => {
|
||||
const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];
|
||||
//运行子Agent
|
||||
|
||||
const subMsg = parentCtx.resTool.newMessage("assistant", agent == "executionAI" ? "编剧" : "编辑");
|
||||
|
||||
// 先完成主Agent当前的消息
|
||||
parentCtx.msg.complete();
|
||||
// 子Agent用新消息回复
|
||||
const subTextStream = await fn({ ...parentCtx, text: prompt });
|
||||
|
||||
let msg = parentCtx.resTool.textMessage();
|
||||
let text = subMsg.text();
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const chunk of subTextStream) {
|
||||
msg.send(chunk);
|
||||
text.append(chunk);
|
||||
fullResponse += chunk;
|
||||
}
|
||||
msg!.end();
|
||||
text.complete();
|
||||
subMsg.complete();
|
||||
if (fullResponse.trim()) {
|
||||
await memory.add(`assistant:${agent === "executionAI" ? "execution" : "supervision"}`, fullResponse, {
|
||||
name: agent === "executionAI" ? "编剧" : "编辑",
|
||||
createTime: new Date(subMsg.datetime).getTime(),
|
||||
});
|
||||
}
|
||||
|
||||
// 为主Agent后续输出创建新消息
|
||||
parentCtx.msg = parentCtx.resTool.newMessage("assistant", "统筹");
|
||||
|
||||
return fullResponse;
|
||||
},
|
||||
|
||||
@ -38,7 +38,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
|
||||
ids: z.array(z.number()).describe("章节id,注意区分"),
|
||||
}),
|
||||
execute: async ({ ids }) => {
|
||||
resTool.systemMessage(`正在阅读 章节事件 数据...`);
|
||||
resTool.newMessage('system').text(`正在获取章节事件,章节ID:${ids.join(",")}`).complete();
|
||||
console.log("[tools] get_novel_events", ids);
|
||||
const data = await u
|
||||
.db("o_novel")
|
||||
@ -55,7 +55,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
|
||||
key: keySchema.describe("数据key"),
|
||||
}),
|
||||
execute: async ({ key }) => {
|
||||
resTool.systemMessage(`正在阅读 ${planDataKeyLabels[key]} 数据...`);
|
||||
resTool.newMessage('system').text(`正在阅读 ${planDataKeyLabels[key]} 数据...`).complete();
|
||||
console.log("[tools] get_planData", key);
|
||||
const planData: planData = await new Promise((resolve) => socket.emit("getPlanData", { key }, (res: any) => resolve(res)));
|
||||
return planData[key];
|
||||
@ -77,7 +77,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
|
||||
inputSchema: z.object({ value: planData.shape.storySkeleton }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_planData storySkeleton", value);
|
||||
resTool.systemMessage("正在保存 故事骨架 数据");
|
||||
resTool.newMessage('system').text("正在保存 故事骨架 数据").complete();
|
||||
socket.emit("setPlanData", { key: "storySkeleton", value });
|
||||
return true;
|
||||
},
|
||||
@ -87,7 +87,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
|
||||
inputSchema: z.object({ value: planData.shape.adaptationStrategy }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_planData adaptationStrategy", value);
|
||||
resTool.systemMessage("正在保存 改编策略 数据");
|
||||
resTool.newMessage('system').text("正在保存 改编策略 数据").complete();
|
||||
socket.emit("setPlanData", { key: "adaptationStrategy", value });
|
||||
return true;
|
||||
},
|
||||
|
||||
@ -27,4 +27,5 @@ export default async (knex: Knex): Promise<void> => {
|
||||
// memories 表新增字段
|
||||
await addColumn("memories", "episodesId", "text");
|
||||
await addColumn("memories", "agentType", "text");
|
||||
await addColumn("memories", "name", "text");
|
||||
};
|
||||
|
||||
@ -78,7 +78,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
modelName: "",
|
||||
vendorId: null,
|
||||
key: "scriptAgent",
|
||||
name: "剧本Agent",
|
||||
name: "剧本AI",
|
||||
desc: "用于读取原文生成故事骨架、改编策略,建议使用具备强大文本理解和生成能力的模型",
|
||||
disabled: false,
|
||||
},
|
||||
@ -87,7 +87,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
modelName: "",
|
||||
vendorId: null,
|
||||
key: "productionAgent",
|
||||
name: "生产Agent",
|
||||
name: "生产AI",
|
||||
desc: "对工作流进行调度和管理,建议使用具备较强的逻辑推理和任务管理能力的模型",
|
||||
disabled: false,
|
||||
},
|
||||
@ -96,7 +96,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
modelName: "",
|
||||
vendorId: null,
|
||||
key: "universalAgent",
|
||||
name: "通用Agent",
|
||||
name: "通用AI",
|
||||
desc: "用于小说事件提取、资产提示词生成、台词提取等边缘功能,建议使用具备较强文本处理能力的模型",
|
||||
disabled: false,
|
||||
},
|
||||
@ -184,6 +184,18 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
},
|
||||
initData: async (knex) => {},
|
||||
},
|
||||
//提示词表
|
||||
{
|
||||
name: "o_prompt",
|
||||
builder: (table) => {
|
||||
table.integer("id").notNullable();
|
||||
table.string("name");
|
||||
table.text("rompt");
|
||||
table.primary(["id"]);
|
||||
table.unique(["id"]);
|
||||
},
|
||||
initData: async (knex) => {},
|
||||
},
|
||||
//小说原文表
|
||||
{
|
||||
name: "o_novel",
|
||||
@ -278,6 +290,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
table.integer("assetsId");
|
||||
table.integer("projectId");
|
||||
table.integer("startTime");
|
||||
table.string("promptState");
|
||||
table.primary(["id"]);
|
||||
table.unique(["id"]);
|
||||
},
|
||||
@ -791,6 +804,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
table.text("isolationKey").notNullable(); // 记忆隔离键
|
||||
table.text("type").notNullable(); // 'message' | 'summary'
|
||||
table.text("role"); // 'user' | 'assistant'
|
||||
table.text("name");
|
||||
table.text("content").notNullable();
|
||||
table.text("embedding"); // 向量嵌入 JSON
|
||||
table.text("relatedMessageIds"); // summary关联的message id列表 JSON
|
||||
|
||||
242
src/router.ts
242
src/router.ts
@ -1,4 +1,4 @@
|
||||
// @routes-hash 1e5ddf0bf4594499634aaa6c96a492c7
|
||||
// @routes-hash 63d067de9d3f97b0602ef91a69334bc8
|
||||
import { Express } from "express";
|
||||
|
||||
import route1 from "./routes/agents/clearMemory";
|
||||
@ -14,66 +14,66 @@ import route10 from "./routes/assets/delAssets";
|
||||
import route11 from "./routes/assets/getAssetsApi";
|
||||
import route12 from "./routes/assets/getImage";
|
||||
import route13 from "./routes/assets/getMaterialData";
|
||||
import route14 from "./routes/assets/saveAssets";
|
||||
import route15 from "./routes/assets/updateAssets";
|
||||
import route16 from "./routes/assets/uploadClip";
|
||||
import route17 from "./routes/assetsGenerate/generateAssets";
|
||||
import route18 from "./routes/assetsGenerate/polishAssetsPrompt";
|
||||
import route19 from "./routes/cornerScape/getAllAssets";
|
||||
import route20 from "./routes/general/generalStatistics";
|
||||
import route21 from "./routes/general/getSingleProject";
|
||||
import route22 from "./routes/general/updateProject";
|
||||
import route23 from "./routes/login/login";
|
||||
import route24 from "./routes/migrate/migrateData";
|
||||
import route25 from "./routes/modelSelect/getModelDetail";
|
||||
import route26 from "./routes/modelSelect/getModelList";
|
||||
import route27 from "./routes/novel/addNovel";
|
||||
import route28 from "./routes/novel/batchDeleteNovel";
|
||||
import route29 from "./routes/novel/delNovel";
|
||||
import route30 from "./routes/novel/event/batchDeleteEvent";
|
||||
import route31 from "./routes/novel/event/deletEvent";
|
||||
import route32 from "./routes/novel/event/generateEvents";
|
||||
import route33 from "./routes/novel/event/getEvent";
|
||||
import route34 from "./routes/novel/getNovel";
|
||||
import route35 from "./routes/novel/getNovelEventState";
|
||||
import route36 from "./routes/novel/getNovelIndex";
|
||||
import route37 from "./routes/novel/updateNovel";
|
||||
import route38 from "./routes/other/deleteAllData";
|
||||
import route39 from "./routes/other/getVersion";
|
||||
import route40 from "./routes/production/assets/getAssetsData";
|
||||
import route41 from "./routes/production/editImage/generateFlowImage";
|
||||
import route42 from "./routes/production/editImage/getImageFlow";
|
||||
import route43 from "./routes/production/editImage/saveImageFlow";
|
||||
import route44 from "./routes/production/editImage/updateImageFlow";
|
||||
import route45 from "./routes/production/exportImage";
|
||||
import route46 from "./routes/production/getFlowData";
|
||||
import route47 from "./routes/production/getProductionData";
|
||||
import route48 from "./routes/production/getStoryboardData";
|
||||
import route49 from "./routes/production/saveFlowData";
|
||||
import route50 from "./routes/production/storyboard/downPreviewImage";
|
||||
import route51 from "./routes/production/storyboard/getStoryboardData";
|
||||
import route52 from "./routes/production/storyboard/previewImage";
|
||||
import route53 from "./routes/production/workbench/confirmSelection";
|
||||
import route54 from "./routes/production/workbench/delVideo";
|
||||
import route55 from "./routes/production/workbench/generateVideo";
|
||||
import route56 from "./routes/production/workbench/getChatLines";
|
||||
import route57 from "./routes/production/workbench/getVideoModelDetail";
|
||||
import route58 from "./routes/production/workbench/videoPolling";
|
||||
import route59 from "./routes/project/addProject";
|
||||
import route60 from "./routes/project/delProject";
|
||||
import route61 from "./routes/project/editProject";
|
||||
import route62 from "./routes/project/getProject";
|
||||
import route63 from "./routes/script/addScript";
|
||||
import route64 from "./routes/script/delScript";
|
||||
import route65 from "./routes/script/exportScript";
|
||||
import route66 from "./routes/script/extractAssets";
|
||||
import route67 from "./routes/script/getScrptApi";
|
||||
import route68 from "./routes/script/pollScriptAssets";
|
||||
import route69 from "./routes/script/updateScript";
|
||||
import route70 from "./routes/scriptAgent/getPlanData";
|
||||
import route71 from "./routes/scriptAgent/setPlanData";
|
||||
import route72 from "./routes/setting/about/checkUpdate";
|
||||
import route73 from "./routes/setting/about/downloadApp";
|
||||
import route14 from "./routes/assets/pollingImageAssets";
|
||||
import route15 from "./routes/assets/pollingPromptAssets";
|
||||
import route16 from "./routes/assets/saveAssets";
|
||||
import route17 from "./routes/assets/updateAssets";
|
||||
import route18 from "./routes/assets/uploadClip";
|
||||
import route19 from "./routes/assetsGenerate/generateAssets";
|
||||
import route20 from "./routes/assetsGenerate/polishAssetsPrompt";
|
||||
import route21 from "./routes/cornerScape/getAllAssets";
|
||||
import route22 from "./routes/general/generalStatistics";
|
||||
import route23 from "./routes/general/getSingleProject";
|
||||
import route24 from "./routes/general/updateProject";
|
||||
import route25 from "./routes/login/login";
|
||||
import route26 from "./routes/migrate/migrateData";
|
||||
import route27 from "./routes/modelSelect/getModelDetail";
|
||||
import route28 from "./routes/modelSelect/getModelList";
|
||||
import route29 from "./routes/novel/addNovel";
|
||||
import route30 from "./routes/novel/batchDeleteNovel";
|
||||
import route31 from "./routes/novel/delNovel";
|
||||
import route32 from "./routes/novel/event/batchDeleteEvent";
|
||||
import route33 from "./routes/novel/event/deletEvent";
|
||||
import route34 from "./routes/novel/event/generateEvents";
|
||||
import route35 from "./routes/novel/event/getEvent";
|
||||
import route36 from "./routes/novel/getNovel";
|
||||
import route37 from "./routes/novel/getNovelEventState";
|
||||
import route38 from "./routes/novel/getNovelIndex";
|
||||
import route39 from "./routes/novel/updateNovel";
|
||||
import route40 from "./routes/other/deleteAllData";
|
||||
import route41 from "./routes/other/getVersion";
|
||||
import route42 from "./routes/production/assets/getAssetsData";
|
||||
import route43 from "./routes/production/editImage/generateFlowImage";
|
||||
import route44 from "./routes/production/editImage/getImageFlow";
|
||||
import route45 from "./routes/production/editImage/saveImageFlow";
|
||||
import route46 from "./routes/production/editImage/updateImageFlow";
|
||||
import route47 from "./routes/production/exportImage";
|
||||
import route48 from "./routes/production/getFlowData";
|
||||
import route49 from "./routes/production/getProductionData";
|
||||
import route50 from "./routes/production/getStoryboardData";
|
||||
import route51 from "./routes/production/saveFlowData";
|
||||
import route52 from "./routes/production/storyboard/downPreviewImage";
|
||||
import route53 from "./routes/production/storyboard/getStoryboardData";
|
||||
import route54 from "./routes/production/storyboard/previewImage";
|
||||
import route55 from "./routes/production/workbench/confirmSelection";
|
||||
import route56 from "./routes/production/workbench/delVideo";
|
||||
import route57 from "./routes/production/workbench/generateVideo";
|
||||
import route58 from "./routes/production/workbench/getChatLines";
|
||||
import route59 from "./routes/production/workbench/getVideoModelDetail";
|
||||
import route60 from "./routes/production/workbench/videoPolling";
|
||||
import route61 from "./routes/project/addProject";
|
||||
import route62 from "./routes/project/delProject";
|
||||
import route63 from "./routes/project/editProject";
|
||||
import route64 from "./routes/project/getProject";
|
||||
import route65 from "./routes/script/addScript";
|
||||
import route66 from "./routes/script/delScript";
|
||||
import route67 from "./routes/script/exportScript";
|
||||
import route68 from "./routes/script/extractAssets";
|
||||
import route69 from "./routes/script/getScrptApi";
|
||||
import route70 from "./routes/script/updateScript";
|
||||
import route71 from "./routes/scriptAgent/getPlanData";
|
||||
import route72 from "./routes/scriptAgent/setPlanData";
|
||||
import route73 from "./routes/setting/about/checkUpdate";
|
||||
import route74 from "./routes/setting/agentDeploy/agentSetKey";
|
||||
import route75 from "./routes/setting/agentDeploy/deployAgentModel";
|
||||
import route76 from "./routes/setting/agentDeploy/getAgentDeploy";
|
||||
@ -120,66 +120,66 @@ export default async (app: Express) => {
|
||||
app.use("/api/assets/getAssetsApi", route11);
|
||||
app.use("/api/assets/getImage", route12);
|
||||
app.use("/api/assets/getMaterialData", route13);
|
||||
app.use("/api/assets/saveAssets", route14);
|
||||
app.use("/api/assets/updateAssets", route15);
|
||||
app.use("/api/assets/uploadClip", route16);
|
||||
app.use("/api/assetsGenerate/generateAssets", route17);
|
||||
app.use("/api/assetsGenerate/polishAssetsPrompt", route18);
|
||||
app.use("/api/cornerScape/getAllAssets", route19);
|
||||
app.use("/api/general/generalStatistics", route20);
|
||||
app.use("/api/general/getSingleProject", route21);
|
||||
app.use("/api/general/updateProject", route22);
|
||||
app.use("/api/login/login", route23);
|
||||
app.use("/api/migrate/migrateData", route24);
|
||||
app.use("/api/modelSelect/getModelDetail", route25);
|
||||
app.use("/api/modelSelect/getModelList", route26);
|
||||
app.use("/api/novel/addNovel", route27);
|
||||
app.use("/api/novel/batchDeleteNovel", route28);
|
||||
app.use("/api/novel/delNovel", route29);
|
||||
app.use("/api/novel/event/batchDeleteEvent", route30);
|
||||
app.use("/api/novel/event/deletEvent", route31);
|
||||
app.use("/api/novel/event/generateEvents", route32);
|
||||
app.use("/api/novel/event/getEvent", route33);
|
||||
app.use("/api/novel/getNovel", route34);
|
||||
app.use("/api/novel/getNovelEventState", route35);
|
||||
app.use("/api/novel/getNovelIndex", route36);
|
||||
app.use("/api/novel/updateNovel", route37);
|
||||
app.use("/api/other/deleteAllData", route38);
|
||||
app.use("/api/other/getVersion", route39);
|
||||
app.use("/api/production/assets/getAssetsData", route40);
|
||||
app.use("/api/production/editImage/generateFlowImage", route41);
|
||||
app.use("/api/production/editImage/getImageFlow", route42);
|
||||
app.use("/api/production/editImage/saveImageFlow", route43);
|
||||
app.use("/api/production/editImage/updateImageFlow", route44);
|
||||
app.use("/api/production/exportImage", route45);
|
||||
app.use("/api/production/getFlowData", route46);
|
||||
app.use("/api/production/getProductionData", route47);
|
||||
app.use("/api/production/getStoryboardData", route48);
|
||||
app.use("/api/production/saveFlowData", route49);
|
||||
app.use("/api/production/storyboard/downPreviewImage", route50);
|
||||
app.use("/api/production/storyboard/getStoryboardData", route51);
|
||||
app.use("/api/production/storyboard/previewImage", route52);
|
||||
app.use("/api/production/workbench/confirmSelection", route53);
|
||||
app.use("/api/production/workbench/delVideo", route54);
|
||||
app.use("/api/production/workbench/generateVideo", route55);
|
||||
app.use("/api/production/workbench/getChatLines", route56);
|
||||
app.use("/api/production/workbench/getVideoModelDetail", route57);
|
||||
app.use("/api/production/workbench/videoPolling", route58);
|
||||
app.use("/api/project/addProject", route59);
|
||||
app.use("/api/project/delProject", route60);
|
||||
app.use("/api/project/editProject", route61);
|
||||
app.use("/api/project/getProject", route62);
|
||||
app.use("/api/script/addScript", route63);
|
||||
app.use("/api/script/delScript", route64);
|
||||
app.use("/api/script/exportScript", route65);
|
||||
app.use("/api/script/extractAssets", route66);
|
||||
app.use("/api/script/getScrptApi", route67);
|
||||
app.use("/api/script/pollScriptAssets", route68);
|
||||
app.use("/api/script/updateScript", route69);
|
||||
app.use("/api/scriptAgent/getPlanData", route70);
|
||||
app.use("/api/scriptAgent/setPlanData", route71);
|
||||
app.use("/api/setting/about/checkUpdate", route72);
|
||||
app.use("/api/setting/about/downloadApp", route73);
|
||||
app.use("/api/assets/pollingImageAssets", route14);
|
||||
app.use("/api/assets/pollingPromptAssets", route15);
|
||||
app.use("/api/assets/saveAssets", route16);
|
||||
app.use("/api/assets/updateAssets", route17);
|
||||
app.use("/api/assets/uploadClip", route18);
|
||||
app.use("/api/assetsGenerate/generateAssets", route19);
|
||||
app.use("/api/assetsGenerate/polishAssetsPrompt", route20);
|
||||
app.use("/api/cornerScape/getAllAssets", route21);
|
||||
app.use("/api/general/generalStatistics", route22);
|
||||
app.use("/api/general/getSingleProject", route23);
|
||||
app.use("/api/general/updateProject", route24);
|
||||
app.use("/api/login/login", route25);
|
||||
app.use("/api/migrate/migrateData", route26);
|
||||
app.use("/api/modelSelect/getModelDetail", route27);
|
||||
app.use("/api/modelSelect/getModelList", route28);
|
||||
app.use("/api/novel/addNovel", route29);
|
||||
app.use("/api/novel/batchDeleteNovel", route30);
|
||||
app.use("/api/novel/delNovel", route31);
|
||||
app.use("/api/novel/event/batchDeleteEvent", route32);
|
||||
app.use("/api/novel/event/deletEvent", route33);
|
||||
app.use("/api/novel/event/generateEvents", route34);
|
||||
app.use("/api/novel/event/getEvent", route35);
|
||||
app.use("/api/novel/getNovel", route36);
|
||||
app.use("/api/novel/getNovelEventState", route37);
|
||||
app.use("/api/novel/getNovelIndex", route38);
|
||||
app.use("/api/novel/updateNovel", route39);
|
||||
app.use("/api/other/deleteAllData", route40);
|
||||
app.use("/api/other/getVersion", route41);
|
||||
app.use("/api/production/assets/getAssetsData", route42);
|
||||
app.use("/api/production/editImage/generateFlowImage", route43);
|
||||
app.use("/api/production/editImage/getImageFlow", route44);
|
||||
app.use("/api/production/editImage/saveImageFlow", route45);
|
||||
app.use("/api/production/editImage/updateImageFlow", route46);
|
||||
app.use("/api/production/exportImage", route47);
|
||||
app.use("/api/production/getFlowData", route48);
|
||||
app.use("/api/production/getProductionData", route49);
|
||||
app.use("/api/production/getStoryboardData", route50);
|
||||
app.use("/api/production/saveFlowData", route51);
|
||||
app.use("/api/production/storyboard/downPreviewImage", route52);
|
||||
app.use("/api/production/storyboard/getStoryboardData", route53);
|
||||
app.use("/api/production/storyboard/previewImage", route54);
|
||||
app.use("/api/production/workbench/confirmSelection", route55);
|
||||
app.use("/api/production/workbench/delVideo", route56);
|
||||
app.use("/api/production/workbench/generateVideo", route57);
|
||||
app.use("/api/production/workbench/getChatLines", route58);
|
||||
app.use("/api/production/workbench/getVideoModelDetail", route59);
|
||||
app.use("/api/production/workbench/videoPolling", route60);
|
||||
app.use("/api/project/addProject", route61);
|
||||
app.use("/api/project/delProject", route62);
|
||||
app.use("/api/project/editProject", route63);
|
||||
app.use("/api/project/getProject", route64);
|
||||
app.use("/api/script/addScript", route65);
|
||||
app.use("/api/script/delScript", route66);
|
||||
app.use("/api/script/exportScript", route67);
|
||||
app.use("/api/script/extractAssets", route68);
|
||||
app.use("/api/script/getScrptApi", route69);
|
||||
app.use("/api/script/updateScript", route70);
|
||||
app.use("/api/scriptAgent/getPlanData", route71);
|
||||
app.use("/api/scriptAgent/setPlanData", route72);
|
||||
app.use("/api/setting/about/checkUpdate", route73);
|
||||
app.use("/api/setting/agentDeploy/agentSetKey", route74);
|
||||
app.use("/api/setting/agentDeploy/deployAgentModel", route75);
|
||||
app.use("/api/setting/agentDeploy/getAgentDeploy", route76);
|
||||
|
||||
@ -9,11 +9,6 @@ function normalizeRole(role?: string | null): "user" | "assistant" {
|
||||
return role?.startsWith("assistant") ? "assistant" : "user";
|
||||
}
|
||||
|
||||
function getAssistantName(role?: string | null): string | undefined {
|
||||
if (!role?.startsWith("assistant:")) return undefined;
|
||||
return role.split(":")[1] || "assistant";
|
||||
}
|
||||
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
@ -29,12 +24,14 @@ export default router.post(
|
||||
.db("memories")
|
||||
.where({ isolationKey, type: "message" })
|
||||
.orderBy("createTime", "asc")
|
||||
.select("id", "role", "content", "createTime");
|
||||
.select("id", "role", "name", "content", "createTime");
|
||||
|
||||
const history = rows.map((row) => ({
|
||||
id: row.id,
|
||||
role: normalizeRole(row.role),
|
||||
name: getAssistantName(row.role),
|
||||
name: row.name ?? undefined,
|
||||
status: "complete",
|
||||
datetime: new Date(row.createTime).toISOString(),
|
||||
content: [{ type: "markdown", status: "complete", data: row.content }],
|
||||
createTime: row.createTime,
|
||||
}));
|
||||
|
||||
@ -21,7 +21,7 @@ export default router.post(
|
||||
let query = u
|
||||
.db("o_assets")
|
||||
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
|
||||
.select("o_assets.*", "o_image.filePath")
|
||||
.select("o_assets.*", "o_image.filePath", "o_image.state")
|
||||
.where("o_assets.projectId", projectId)
|
||||
.andWhere("o_assets.type", type);
|
||||
if (name) {
|
||||
@ -34,7 +34,7 @@ export default router.post(
|
||||
let childQuery = u
|
||||
.db("o_assets")
|
||||
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
|
||||
.select("o_assets.*", "o_image.filePath")
|
||||
.select("o_assets.*", "o_image.filePath", "o_image.state")
|
||||
.where("o_assets.projectId", projectId)
|
||||
.andWhere("o_assets.type", type)
|
||||
.whereNotNull("o_assets.assetsId");
|
||||
|
||||
28
src/routes/assets/pollingImageAssets.ts
Normal file
28
src/routes/assets/pollingImageAssets.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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_assets")
|
||||
.leftJoin("o_image", "o_assets.id", "o_image.assetsId")
|
||||
.whereIn("o_assets.id", ids)
|
||||
.select("o_image.state", "o_assets.id", "o_image.filePath");
|
||||
const result = await Promise.all(
|
||||
data.map(async (item: any) => ({
|
||||
...item,
|
||||
filePath: item.filePath ? await u.oss.getFileUrl(item.filePath) : null,
|
||||
})),
|
||||
);
|
||||
res.status(200).send(success(result));
|
||||
},
|
||||
);
|
||||
18
src/routes/assets/pollingPromptAssets.ts
Normal file
18
src/routes/assets/pollingPromptAssets.ts
Normal 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({
|
||||
ids: z.array(z.number()),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { ids } = req.body;
|
||||
const data = await u.db("o_assets").whereIn("id", ids).select("*");
|
||||
res.status(200).send(success(data));
|
||||
},
|
||||
);
|
||||
@ -33,7 +33,7 @@ export default router.post(
|
||||
assetsId: id,
|
||||
filePath: savePath,
|
||||
type: type,
|
||||
state: "生成成功",
|
||||
state: "已完成",
|
||||
});
|
||||
// 更新资产表图片为新图片
|
||||
await u
|
||||
|
||||
@ -87,6 +87,7 @@ export default router.post("/", validateFields(requestSchema), async (req, res)
|
||||
state: "生成中",
|
||||
assetsId: id,
|
||||
});
|
||||
await u.db("o_assets").where("id", id).update({ imageId });
|
||||
|
||||
// 3. 准备生成参数
|
||||
const imagePath = `/${projectId}/${cfg.dir}/${uuidv4()}.jpg`;
|
||||
@ -106,7 +107,7 @@ export default router.post("/", validateFields(requestSchema), async (req, res)
|
||||
describe,
|
||||
projectId,
|
||||
relatedObjects: JSON.stringify(relatedObjects),
|
||||
});
|
||||
})
|
||||
aiImage.save(imagePath);
|
||||
|
||||
// 5. 更新记录 & 返回结果
|
||||
@ -114,7 +115,7 @@ export default router.post("/", validateFields(requestSchema), async (req, res)
|
||||
if (!imageData) return res.status(500).send("资产已被删除");
|
||||
|
||||
await u.db("o_image").where("id", imageId).update({
|
||||
state: "生成成功",
|
||||
state: "已完成",
|
||||
filePath: imagePath,
|
||||
type,
|
||||
model: model.split(":")[1],
|
||||
|
||||
@ -61,6 +61,7 @@ export default router.post(
|
||||
if (!project) return res.status(500).send(success({ message: "项目为空" }));
|
||||
|
||||
const allOutlineDataList: { data: string }[] = await u.db("o_outline").where("projectId", projectId).select("data");
|
||||
await u.db("o_assets").where("id", assetsId).update({ promptState: "生成中" });
|
||||
|
||||
const itemMap: Record<string, ResultItem> = {};
|
||||
|
||||
@ -124,7 +125,7 @@ export default router.post(
|
||||
})) as any;
|
||||
|
||||
if (!_output) return res.status(500).send("失败");
|
||||
await u.db("o_assets").where("id", assetsId).update({ prompt: _output });
|
||||
await u.db("o_assets").where("id", assetsId).update({ prompt: _output, promptState: "已完成" });
|
||||
|
||||
res.status(200).send(success({ prompt: _output, assetsId }));
|
||||
} catch (e: any) {
|
||||
|
||||
@ -14,7 +14,7 @@ export default router.post(
|
||||
const { id } = req.body;
|
||||
await u.db("o_agentDeploy").whereIn("id", id).where("disabled", "<>", 1).update({
|
||||
model: "gpt-4.1",
|
||||
modelName: "1:gpt-4.1",
|
||||
modelName: "toonflow:gpt-4.1",
|
||||
vendorId: 1,
|
||||
});
|
||||
res.status(200).send(success("配置成功"));
|
||||
|
||||
@ -76,7 +76,6 @@ export default router.post(
|
||||
for await (const chunk of textStream) {
|
||||
fullResponse += chunk;
|
||||
}
|
||||
console.log("%c Line:78 🥝 fullResponse", "background:#ea7e5c", fullResponse);
|
||||
res.status(200).send(success(fullResponse));
|
||||
} else {
|
||||
const aiTypeFn = {
|
||||
|
||||
58
src/socket/chatMessagesData.d.ts
vendored
Normal file
58
src/socket/chatMessagesData.d.ts
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
import type { ToolCallEventType } from './adapters/agui/types/events';
|
||||
|
||||
export type ChatMessageStatus = 'pending' | 'streaming' | 'complete' | 'stop' | 'error';
|
||||
export type AttachmentType = 'image' | 'video' | 'audio' | 'pdf' | 'doc' | 'ppt' | 'txt';
|
||||
export type ChatComment = 'good' | 'bad' | '';
|
||||
|
||||
// 基础内容接口
|
||||
export interface ChatBaseContent<T extends string, D> {
|
||||
type: T;
|
||||
data: D;
|
||||
status?: ChatMessageStatus;
|
||||
id?: string;
|
||||
strategy?: 'merge' | 'append';
|
||||
ext?: Record<string, any>;
|
||||
}
|
||||
|
||||
// 内容类型定义
|
||||
export type TextContent = ChatBaseContent<'text', string>;
|
||||
export type MarkdownContent = ChatBaseContent<'markdown', string>;
|
||||
export type ImageContent = ChatBaseContent<'image', { name?: string; url?: string; width?: number; height?: number }>;
|
||||
export type ThinkingContent = ChatBaseContent<'thinking', { text?: string; title?: string }>;
|
||||
export type SearchContent = ChatBaseContent<'search', { title?: string; references?: { title: string; icon?: string; type?: string; url?: string; content?: string; site?: string; date?: string }[] }>;
|
||||
export type SuggestionContent = ChatBaseContent<'suggestion', { title: string; prompt?: string }[]>;
|
||||
export type AttachmentContent = ChatBaseContent<'attachment', { fileType: AttachmentType; size?: number; name?: string; url?: string; isReference?: boolean; width?: number; height?: number; extension?: string; metadata?: Record<string, any> }[]>;
|
||||
export type ToolCallContent = ChatBaseContent<'toolcall', { toolCallId: string; toolCallName: string; eventType?: ToolCallEventType; parentMessageId?: string; args?: string; chunk?: string; result?: string }>;
|
||||
export type ActivityContent<T = Record<string, any>> = ChatBaseContent<'activity', { activityType: string; messageId?: string; content: T; deltaInfo?: { fromIndex: number; toIndex: number } }>;
|
||||
|
||||
// 聚合内容类型
|
||||
export type AIMessageContent = TextContent | MarkdownContent | ImageContent | ThinkingContent | SearchContent | SuggestionContent | ReasoningContent | ToolCallContent | ActivityContent;
|
||||
export type ReasoningContent = ChatBaseContent<'reasoning', AIMessageContent[]>;
|
||||
export type UserMessageContent = TextContent | AttachmentContent;
|
||||
|
||||
// 消息类型定义
|
||||
export interface ChatBaseMessage {
|
||||
id: string;
|
||||
status?: ChatMessageStatus;
|
||||
datetime?: string;
|
||||
ext?: any;
|
||||
}
|
||||
|
||||
export interface UserMessage extends ChatBaseMessage {
|
||||
role: 'user';
|
||||
content: UserMessageContent[];
|
||||
}
|
||||
|
||||
export interface AIMessage extends ChatBaseMessage {
|
||||
role: 'assistant';
|
||||
content?: AIMessageContent[];
|
||||
history?: AIMessageContent[][];
|
||||
comment?: ChatComment;
|
||||
}
|
||||
|
||||
export interface SystemMessage extends ChatBaseMessage {
|
||||
role: 'system';
|
||||
content: TextContent[];
|
||||
}
|
||||
|
||||
export type ChatMessagesData = UserMessage | AIMessage | SystemMessage;
|
||||
79
src/socket/resTool copy.ts
Normal file
79
src/socket/resTool copy.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import u from "@/utils";
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
class ResTool {
|
||||
public socket: Socket;
|
||||
public data: Record<string, any>;
|
||||
constructor(socket: Socket, data: Record<string, any> = {}) {
|
||||
this.socket = socket;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
textMessage(name: string = "AI") {
|
||||
const messageId = u.uuid();
|
||||
this.socket.emit("textMessage", {
|
||||
type: "start",
|
||||
messageId,
|
||||
delta: null,
|
||||
role: "assistant",
|
||||
name,
|
||||
});
|
||||
const handle = {
|
||||
send: (delta: string) => {
|
||||
this.socket.emit("textMessage", {
|
||||
type: "content",
|
||||
messageId,
|
||||
delta,
|
||||
role: "assistant",
|
||||
name,
|
||||
});
|
||||
return handle;
|
||||
},
|
||||
end: () => {
|
||||
this.socket.emit("textMessage", {
|
||||
type: "end",
|
||||
messageId,
|
||||
delta: null,
|
||||
role: "assistant",
|
||||
name,
|
||||
});
|
||||
},
|
||||
};
|
||||
return handle;
|
||||
}
|
||||
thinkMessage() {
|
||||
const messageId = u.uuid();
|
||||
this.socket.emit("thinkMessage", {
|
||||
type: "start",
|
||||
messageId,
|
||||
delta: null,
|
||||
role: "assistant",
|
||||
});
|
||||
const handle = {
|
||||
send: (delta: string) => {
|
||||
this.socket.emit("thinkMessage", {
|
||||
type: "content",
|
||||
messageId,
|
||||
delta,
|
||||
role: "assistant",
|
||||
});
|
||||
return handle;
|
||||
},
|
||||
end: () => {
|
||||
this.socket.emit("thinkMessage", {
|
||||
type: "end",
|
||||
messageId,
|
||||
delta: null,
|
||||
role: "assistant",
|
||||
});
|
||||
},
|
||||
};
|
||||
return handle;
|
||||
}
|
||||
systemMessage(content: string) {
|
||||
const messageId = u.uuid();
|
||||
this.socket.emit("systemMessage", { messageId, content });
|
||||
}
|
||||
}
|
||||
|
||||
export default ResTool;
|
||||
@ -1,79 +1,544 @@
|
||||
import u from "@/utils";
|
||||
import { Socket } from "socket.io";
|
||||
import type {
|
||||
ChatMessageStatus,
|
||||
AIMessageContent,
|
||||
UserMessageContent,
|
||||
TextContent,
|
||||
MarkdownContent,
|
||||
ImageContent,
|
||||
ThinkingContent,
|
||||
SearchContent,
|
||||
SuggestionContent,
|
||||
ToolCallContent,
|
||||
ActivityContent,
|
||||
ReasoningContent,
|
||||
AttachmentContent,
|
||||
} from "./ChatMessagesData";
|
||||
|
||||
type ContentType = AIMessageContent["type"];
|
||||
|
||||
class ResTool {
|
||||
public socket: Socket;
|
||||
public data: Record<string, any>;
|
||||
|
||||
constructor(socket: Socket, data: Record<string, any> = {}) {
|
||||
this.socket = socket;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
textMessage(name: string = "AI") {
|
||||
// 创建新消息
|
||||
newMessage(role: "assistant" | "user" | "system" = "assistant", name?: string) {
|
||||
const messageId = u.uuid();
|
||||
this.socket.emit("textMessage", {
|
||||
type: "start",
|
||||
messageId,
|
||||
delta: null,
|
||||
role: "assistant",
|
||||
const datetime = new Date().toISOString();
|
||||
|
||||
this.socket.emit("message", {
|
||||
id: messageId,
|
||||
role,
|
||||
name,
|
||||
status: "pending" as ChatMessageStatus,
|
||||
datetime,
|
||||
content: [],
|
||||
});
|
||||
const handle = {
|
||||
send: (delta: string) => {
|
||||
this.socket.emit("textMessage", {
|
||||
type: "content",
|
||||
messageId,
|
||||
delta,
|
||||
role: "assistant",
|
||||
name,
|
||||
});
|
||||
return handle;
|
||||
},
|
||||
end: () => {
|
||||
this.socket.emit("textMessage", {
|
||||
type: "end",
|
||||
messageId,
|
||||
delta: null,
|
||||
role: "assistant",
|
||||
name,
|
||||
});
|
||||
},
|
||||
};
|
||||
return handle;
|
||||
|
||||
return new MessageBuilder(this.socket, messageId, role, name, datetime);
|
||||
}
|
||||
thinkMessage() {
|
||||
const messageId = u.uuid();
|
||||
this.socket.emit("thinkMessage", {
|
||||
type: "start",
|
||||
messageId,
|
||||
delta: null,
|
||||
role: "assistant",
|
||||
|
||||
// 发送错误消息
|
||||
sendError(messageId: string, error: string) {
|
||||
this.socket.emit("message:update", {
|
||||
id: messageId,
|
||||
status: "error" as ChatMessageStatus,
|
||||
ext: { error },
|
||||
});
|
||||
const handle = {
|
||||
send: (delta: string) => {
|
||||
this.socket.emit("thinkMessage", {
|
||||
type: "content",
|
||||
messageId,
|
||||
delta,
|
||||
role: "assistant",
|
||||
});
|
||||
return handle;
|
||||
},
|
||||
end: () => {
|
||||
this.socket.emit("thinkMessage", {
|
||||
type: "end",
|
||||
messageId,
|
||||
delta: null,
|
||||
role: "assistant",
|
||||
});
|
||||
},
|
||||
};
|
||||
return handle;
|
||||
}
|
||||
systemMessage(content: string) {
|
||||
const messageId = u.uuid();
|
||||
this.socket.emit("systemMessage", { messageId, content });
|
||||
|
||||
// 发送完成状态
|
||||
sendComplete(messageId: string) {
|
||||
this.socket.emit("message:update", {
|
||||
id: messageId,
|
||||
status: "complete" as ChatMessageStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 消息构建器
|
||||
class MessageBuilder {
|
||||
private socket: Socket;
|
||||
private messageId: string;
|
||||
private messageRole: "assistant" | "user" | "system";
|
||||
private messageName?: string;
|
||||
private messageDatetime: string;
|
||||
|
||||
constructor(socket: Socket, messageId: string, role: "assistant" | "user" | "system", name?: string, datetime?: string) {
|
||||
this.socket = socket;
|
||||
this.messageId = messageId;
|
||||
this.messageRole = role;
|
||||
this.messageName = name;
|
||||
this.messageDatetime = datetime ?? new Date().toISOString();
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.messageId;
|
||||
}
|
||||
|
||||
get role() {
|
||||
return this.messageRole;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.messageName;
|
||||
}
|
||||
|
||||
get datetime() {
|
||||
return this.messageDatetime;
|
||||
}
|
||||
|
||||
// 更新消息状态
|
||||
updateStatus(status: ChatMessageStatus) {
|
||||
this.socket.emit("message:update", {
|
||||
id: this.messageId,
|
||||
status,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 添加文本内容
|
||||
text(initialText = "") {
|
||||
const contentId = u.uuid();
|
||||
const content: TextContent = {
|
||||
type: "text",
|
||||
id: contentId,
|
||||
data: initialText,
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
});
|
||||
|
||||
return new ContentStream<string>(this.socket, this.messageId, contentId, "text");
|
||||
}
|
||||
|
||||
// 添加 Markdown 内容
|
||||
markdown(initialText = "") {
|
||||
const contentId = u.uuid();
|
||||
const content: MarkdownContent = {
|
||||
type: "markdown",
|
||||
id: contentId,
|
||||
data: initialText,
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
});
|
||||
|
||||
return new ContentStream<string>(this.socket, this.messageId, contentId, "markdown");
|
||||
}
|
||||
|
||||
// 添加思考内容
|
||||
thinking(title = "思考中...") {
|
||||
const contentId = u.uuid();
|
||||
const content: ThinkingContent = {
|
||||
type: "thinking",
|
||||
id: contentId,
|
||||
data: { title, text: "" },
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
});
|
||||
|
||||
return new ThinkingStream(this.socket, this.messageId, contentId);
|
||||
}
|
||||
|
||||
// 添加搜索内容
|
||||
search(title = "搜索中...") {
|
||||
const contentId = u.uuid();
|
||||
const content: SearchContent = {
|
||||
type: "search",
|
||||
id: contentId,
|
||||
data: { title, references: [] },
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
});
|
||||
|
||||
return new SearchStream(this.socket, this.messageId, contentId);
|
||||
}
|
||||
|
||||
// 添加图片内容
|
||||
image(data: ImageContent["data"]) {
|
||||
const contentId = u.uuid();
|
||||
const content: ImageContent = {
|
||||
type: "image",
|
||||
id: contentId,
|
||||
data,
|
||||
status: "complete",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// 添加建议内容
|
||||
suggestion(suggestions: SuggestionContent["data"]) {
|
||||
const contentId = u.uuid();
|
||||
const content: SuggestionContent = {
|
||||
type: "suggestion",
|
||||
id: contentId,
|
||||
data: suggestions,
|
||||
status: "complete",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// 添加工具调用内容
|
||||
toolCall(data: ToolCallContent["data"]) {
|
||||
const contentId = u.uuid();
|
||||
const content: ToolCallContent = {
|
||||
type: "toolcall",
|
||||
id: contentId,
|
||||
data: { ...data, parentMessageId: this.messageId },
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
});
|
||||
|
||||
return new ToolCallStream(this.socket, this.messageId, contentId, data.toolCallId);
|
||||
}
|
||||
|
||||
// 添加活动内容
|
||||
activity<T = Record<string, any>>(activityType: string, content: T) {
|
||||
const contentId = u.uuid();
|
||||
const activityContent: ActivityContent<T> = {
|
||||
type: "activity",
|
||||
id: contentId,
|
||||
data: {
|
||||
activityType,
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
},
|
||||
status: "complete",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content: activityContent,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// 添加推理内容
|
||||
reasoning() {
|
||||
const contentId = u.uuid();
|
||||
const content: ReasoningContent = {
|
||||
type: "reasoning",
|
||||
id: contentId,
|
||||
data: [],
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
this.socket.emit("content:add", {
|
||||
messageId: this.messageId,
|
||||
content,
|
||||
});
|
||||
|
||||
return new ReasoningBuilder(this.socket, this.messageId, contentId);
|
||||
}
|
||||
|
||||
// 完成消息
|
||||
complete() {
|
||||
this.socket.emit("message:update", {
|
||||
id: this.messageId,
|
||||
status: "complete" as ChatMessageStatus,
|
||||
});
|
||||
}
|
||||
|
||||
// 停止消息
|
||||
stop() {
|
||||
this.socket.emit("message:update", {
|
||||
id: this.messageId,
|
||||
status: "stop" as ChatMessageStatus,
|
||||
});
|
||||
}
|
||||
|
||||
// 错误
|
||||
error(errorMsg?: string) {
|
||||
this.socket.emit("message:update", {
|
||||
id: this.messageId,
|
||||
status: "error" as ChatMessageStatus,
|
||||
ext: errorMsg ? { error: errorMsg } : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 内容流基类
|
||||
class ContentStream<T> {
|
||||
protected socket: Socket;
|
||||
protected messageId: string;
|
||||
protected contentId: string;
|
||||
protected contentType: ContentType;
|
||||
|
||||
constructor(socket: Socket, messageId: string, contentId: string, contentType: ContentType) {
|
||||
this.socket = socket;
|
||||
this.messageId = messageId;
|
||||
this.contentId = contentId;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.contentId;
|
||||
}
|
||||
|
||||
// 流式追加数据
|
||||
append(chunk: string) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: this.contentType,
|
||||
data: chunk,
|
||||
strategy: "append",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 合并/替换数据
|
||||
merge(data: T) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: this.contentType,
|
||||
data,
|
||||
strategy: "merge",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 完成内容
|
||||
complete(finalData?: T) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: this.contentType,
|
||||
data: finalData,
|
||||
status: "complete",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 错误
|
||||
error() {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
status: "error",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// 思考内容流
|
||||
class ThinkingStream extends ContentStream<ThinkingContent["data"]> {
|
||||
constructor(socket: Socket, messageId: string, contentId: string) {
|
||||
super(socket, messageId, contentId, "thinking");
|
||||
}
|
||||
|
||||
// 追加思考文本
|
||||
appendText(chunk: string) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "thinking",
|
||||
data: { text: chunk },
|
||||
strategy: "append",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 更新标题
|
||||
updateTitle(title: string) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "thinking",
|
||||
data: { title },
|
||||
strategy: "merge",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索内容流
|
||||
class SearchStream extends ContentStream<SearchContent["data"]> {
|
||||
constructor(socket: Socket, messageId: string, contentId: string) {
|
||||
super(socket, messageId, contentId, "search");
|
||||
}
|
||||
|
||||
// 添加引用
|
||||
addReference(ref: Exclude<SearchContent["data"]["references"], undefined>[0]) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "search",
|
||||
data: { references: [ref] },
|
||||
strategy: "append",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 批量添加引用
|
||||
addReferences(refs: SearchContent["data"]["references"]) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "search",
|
||||
data: { references: refs },
|
||||
strategy: "append",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 更新标题
|
||||
updateTitle(title: string) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "search",
|
||||
data: { title },
|
||||
strategy: "merge",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具调用流
|
||||
class ToolCallStream extends ContentStream<ToolCallContent["data"]> {
|
||||
private toolCallId: string;
|
||||
|
||||
constructor(socket: Socket, messageId: string, contentId: string, toolCallId: string) {
|
||||
super(socket, messageId, contentId, "toolcall");
|
||||
this.toolCallId = toolCallId;
|
||||
}
|
||||
|
||||
// 追加参数块
|
||||
appendArgs(chunk: string) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "toolcall",
|
||||
data: { toolCallId: this.toolCallId, args: chunk },
|
||||
strategy: "append",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 追加结果块
|
||||
appendResult(chunk: string) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "toolcall",
|
||||
data: { toolCallId: this.toolCallId, chunk },
|
||||
strategy: "append",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 设置完整结果
|
||||
setResult(result: string) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "toolcall",
|
||||
data: { toolCallId: this.toolCallId, result },
|
||||
strategy: "merge",
|
||||
status: "complete",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 更新事件类型
|
||||
updateEventType(eventType: ToolCallContent["data"]["eventType"]) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "toolcall",
|
||||
data: { toolCallId: this.toolCallId, eventType },
|
||||
strategy: "merge",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// 推理构建器
|
||||
class ReasoningBuilder {
|
||||
private socket: Socket;
|
||||
private messageId: string;
|
||||
private contentId: string;
|
||||
|
||||
constructor(socket: Socket, messageId: string, contentId: string) {
|
||||
this.socket = socket;
|
||||
this.messageId = messageId;
|
||||
this.contentId = contentId;
|
||||
}
|
||||
|
||||
// 添加子内容
|
||||
addContent(content: AIMessageContent) {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "reasoning",
|
||||
data: [content],
|
||||
strategy: "append",
|
||||
status: "streaming",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 完成推理
|
||||
complete() {
|
||||
this.socket.emit("content:update", {
|
||||
messageId: this.messageId,
|
||||
contentId: this.contentId,
|
||||
type: "reasoning",
|
||||
status: "complete",
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default ResTool;
|
||||
export { MessageBuilder, ContentStream, ThinkingStream, SearchStream, ToolCallStream, ReasoningBuilder };
|
||||
|
||||
@ -3,6 +3,7 @@ import u from "@/utils";
|
||||
import { Namespace, Socket } from "socket.io";
|
||||
import * as agent from "@/agents/scriptAgent/index";
|
||||
import ResTool from "@/socket/resTool";
|
||||
import Memory from "@/utils/agent/memory";
|
||||
|
||||
async function verifyToken(rawToken: string): Promise<Boolean> {
|
||||
const setting = await u.db("o_setting").where("key", "tokenKey").select("value").first();
|
||||
@ -40,23 +41,61 @@ export default (nsp: Namespace) => {
|
||||
});
|
||||
let abortController: AbortController | null = null;
|
||||
|
||||
socket.on("message", async (text: string) => {
|
||||
socket.on("chat", async (data: { content: string }) => {
|
||||
const { content } = data;
|
||||
abortController?.abort();
|
||||
abortController = new AbortController();
|
||||
const currentController = abortController;
|
||||
const memory = new Memory("scriptAgent", isolationKey);
|
||||
|
||||
const textStream = await agent.decisionAI({ socket, isolationKey, text, abortSignal: currentController.signal, resTool });
|
||||
const msg = resTool.newMessage("assistant", "统筹");
|
||||
const ctx: agent.AgentContext = {
|
||||
socket,
|
||||
isolationKey,
|
||||
text: content,
|
||||
userMessageTime: new Date(msg.datetime).getTime() - 1,
|
||||
abortSignal: currentController.signal,
|
||||
resTool,
|
||||
msg,
|
||||
};
|
||||
|
||||
let msg = resTool.textMessage();
|
||||
const textStream = await agent.decisionAI(ctx);
|
||||
|
||||
let currentMsg = ctx.msg;
|
||||
let text = currentMsg.text();
|
||||
let currentContent = "";
|
||||
|
||||
const persistCurrentMessage = async () => {
|
||||
if (!currentContent.trim()) return;
|
||||
await memory.add("assistant:decision", currentContent, {
|
||||
name: "统筹",
|
||||
createTime: new Date(currentMsg.datetime).getTime(),
|
||||
});
|
||||
currentContent = "";
|
||||
};
|
||||
|
||||
const syncCurrentMessage = async () => {
|
||||
if (ctx.msg === currentMsg) return;
|
||||
text.complete();
|
||||
currentMsg.complete();
|
||||
await persistCurrentMessage();
|
||||
currentMsg = ctx.msg;
|
||||
text = currentMsg.text();
|
||||
};
|
||||
|
||||
try {
|
||||
for await (const chunk of textStream) {
|
||||
msg.send(chunk);
|
||||
await syncCurrentMessage();
|
||||
text.append(chunk);
|
||||
currentContent += chunk;
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name !== "AbortError") throw err;
|
||||
} finally {
|
||||
msg.end();
|
||||
await syncCurrentMessage();
|
||||
text.complete();
|
||||
currentMsg.complete();
|
||||
await persistCurrentMessage();
|
||||
if (abortController === currentController) {
|
||||
abortController = null;
|
||||
}
|
||||
|
||||
19
src/types/database.d.ts
vendored
19
src/types/database.d.ts
vendored
@ -1,19 +1,13 @@
|
||||
// @db-hash 1af54b27110c54bf92390a017ee6b240
|
||||
// @db-hash 8e5f2b7a28d4494b291d802b055b6399
|
||||
//该文件由脚本自动生成,请勿手动修改
|
||||
|
||||
export interface _o_script_old_20260327 {
|
||||
'content'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'id'?: number;
|
||||
'name'?: string | null;
|
||||
'projectId'?: number | null;
|
||||
}
|
||||
export interface memories {
|
||||
'content': string;
|
||||
'createTime': number;
|
||||
'embedding'?: string | null;
|
||||
'id'?: string;
|
||||
'isolationKey': string;
|
||||
'name'?: string | null;
|
||||
'relatedMessageIds'?: string | null;
|
||||
'role'?: string | null;
|
||||
'summarized'?: number | null;
|
||||
@ -124,11 +118,14 @@ export interface o_project {
|
||||
'videoModel'?: string | null;
|
||||
'videoRatio'?: string | null;
|
||||
}
|
||||
export interface o_prompt {
|
||||
'id'?: number;
|
||||
'name'?: string | null;
|
||||
'rompt'?: string | null;
|
||||
}
|
||||
export interface o_script {
|
||||
'content'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'errorReason'?: string | null;
|
||||
'extractState'?: number | null;
|
||||
'id'?: number;
|
||||
'name'?: string | null;
|
||||
'projectId'?: number | null;
|
||||
@ -230,7 +227,6 @@ export interface o_videoConfig {
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
"_o_script_old_20260327": _o_script_old_20260327;
|
||||
"memories": memories;
|
||||
"o_agentDeploy": o_agentDeploy;
|
||||
"o_agentWorkData": o_agentWorkData;
|
||||
@ -245,6 +241,7 @@ export interface DB {
|
||||
"o_outline": o_outline;
|
||||
"o_outlineNovel": o_outlineNovel;
|
||||
"o_project": o_project;
|
||||
"o_prompt": o_prompt;
|
||||
"o_script": o_script;
|
||||
"o_scriptAssets": o_scriptAssets;
|
||||
"o_setting": o_setting;
|
||||
|
||||
@ -8,6 +8,7 @@ import getPath from "@/utils/getPath";
|
||||
import vm from "@/utils/vm";
|
||||
import task from "@/utils/taskRecord";
|
||||
import Ai from "@/utils/ai";
|
||||
import { getPrompts } from "@/utils/getPrompts";
|
||||
|
||||
export default {
|
||||
db,
|
||||
@ -20,4 +21,5 @@ export default {
|
||||
getPath,
|
||||
Ai,
|
||||
task,
|
||||
getPrompts,
|
||||
};
|
||||
|
||||
@ -82,7 +82,8 @@ class Memory {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async add(role: string = "user", content: string) {
|
||||
|
||||
async add(role: string = "user", content: string, options?: { name?: string; createTime?: number }) {
|
||||
const { messagesPerSummary } = await this.getConfigData({ messagesPerSummary: DEFAULTS.messagesPerSummary });
|
||||
const id = uuidv4();
|
||||
const embedding = await getEmbedding(content);
|
||||
@ -93,11 +94,12 @@ class Memory {
|
||||
isolationKey,
|
||||
type: "message",
|
||||
role,
|
||||
name: options?.name,
|
||||
content,
|
||||
embedding: JSON.stringify(embedding),
|
||||
relatedMessageIds: null,
|
||||
summarized: 0,
|
||||
createTime: Date.now(),
|
||||
createTime: options?.createTime ?? Date.now(),
|
||||
} as any);
|
||||
|
||||
// 检查未总结消息数量
|
||||
@ -154,7 +156,7 @@ class Memory {
|
||||
const ragResults = vectorSearch(allMessages, queryEmbedding, Number(ragLimit));
|
||||
|
||||
return {
|
||||
shortTerm: shortTerm.map((m: any) => ({ id: m.id, role: m.role, content: m.content, createTime: m.createTime })),
|
||||
shortTerm: shortTerm.map((m: any) => ({ id: m.id, role: m.role, name: m.name, content: m.content, createTime: m.createTime })),
|
||||
summaries: summaries.map((s) => ({
|
||||
id: s.id,
|
||||
content: s.content,
|
||||
|
||||
@ -26,21 +26,25 @@ class CleanNovel {
|
||||
|
||||
private async processChapter(novel: o_novel, intansce: ReturnType<typeof u.Ai.Text>): Promise<EventType | null> {
|
||||
try {
|
||||
const skill = await useSkill("universal_agent.md");
|
||||
|
||||
const prompt = await u.getPrompts("event");
|
||||
const resData = await intansce.invoke({
|
||||
system: skill.prompt,
|
||||
system: prompt,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "请根据以下小说章节生成事件摘要:\n" + novel.chapterData!,
|
||||
content:
|
||||
"请根据以下小说章节数:" +
|
||||
novel.chapterIndex +
|
||||
"小说章节券:" +
|
||||
novel.reel +
|
||||
"小说章节名称:" +
|
||||
novel.chapter +
|
||||
"、小说章节内容生成事件摘要:\n" +
|
||||
novel.chapterData!,
|
||||
},
|
||||
],
|
||||
tools: skill.tools,
|
||||
});
|
||||
|
||||
const preData = resData.text;
|
||||
|
||||
this.emitter.emit("item", { id: novel.id, event: preData });
|
||||
return { id: novel.id!, event: preData };
|
||||
} catch (e) {
|
||||
|
||||
59
src/utils/getPrompts.ts
Normal file
59
src/utils/getPrompts.ts
Normal file
@ -0,0 +1,59 @@
|
||||
export async function getPrompts(type: string) {
|
||||
if (type == "event") {
|
||||
return `
|
||||
# 事件提取指令
|
||||
|
||||
你是小说文本分析助手。用户每次提供一个章节的原文,你提取该章的结构化事件信息。
|
||||
|
||||
## ⚠️ 输出约束(最高优先级,违反任何一条即为失败)
|
||||
|
||||
1. 你的**完整回复**只有一行,以 \`|\` 开头、以 \`|\` 结尾,恰好 7 个字段
|
||||
2. 回复的**第一个字符**必须是 \`|\`,**最后一个字符**必须是 \`|\`
|
||||
3. \`|\` 之前不许有任何字符——没有引导语、没有解释、没有"根据……"、没有"以下是……"
|
||||
4. \`|\` 之后不许有任何字符——没有总结、没有提取说明、没有改编建议
|
||||
5. 不输出表头行、分隔线、Markdown 标题、emoji、代码块标记
|
||||
|
||||
## 输出格式
|
||||
|
||||
\`\`\`
|
||||
| 第X章 {章节标题} | {涉及角色} | {核心事件} | {主线关系} | {信息密度} | {预估集长} | {情绪强度} |
|
||||
\`\`\`
|
||||
|
||||
### 字段规范
|
||||
|
||||
| 字段 | 格式要求 | 示例 |
|
||||
|------|----------|------|
|
||||
| 章节 | \`第X章 {章节标题}\` | \`第1章 职业危机与许愿\` |
|
||||
| 涉及角色 | 有实际戏份的角色,顿号分隔 | \`林逸、白有容\` |
|
||||
| 核心事件 | 30-60字,必须含动作+结果 | \`林逸因解密风潮事业崩塌,颓废中许愿触发魔法系统绑定\` |
|
||||
| 主线关系 | **必须**为 \`强/中/弱(3-8字理由)\` | \`强(动机建立+系统激活)\` |
|
||||
| 信息密度 | \`高\` / \`中\` / \`低\` | \`高\` |
|
||||
| 预估集长 | **必须**为 \`X秒\`,禁止用分钟 | \`50秒\` |
|
||||
| 情绪强度 | 文字标签,\`+\` 连接,禁止星级/数字 | \`转折+悬疑\` |
|
||||
|
||||
**主线关系判定**:强=直接推动主角弧线;中=补充世界观/人物关系/伏笔;弱=过渡/气氛。
|
||||
|
||||
**预估集长参考**:高密度+高情绪→45-60秒;中→35-45秒;低→25-35秒。
|
||||
|
||||
**可用情绪标签**:\`冲突\`、\`恐怖\`、\`情感\`、\`转折\`、\`高潮\`、\`平铺\`、\`喜剧\`、\`悬疑\`、\`情感崩溃\`。
|
||||
|
||||
## 输出示例
|
||||
|
||||
以下两个示例展示的是**完整回复**——除这一行外没有任何其他内容:
|
||||
|
||||
\`\`\`
|
||||
| 第1章 职业危机与许愿 | 林逸 | 职业魔术师林逸因解密打假风潮导致事业崩塌,颓废中感慨"如果会魔法就好了",意外触发神奇魔法系统绑定 | 强(主角动机建立+系统激活) | 高 | 50秒 | 转折+悬疑 |
|
||||
\`\`\`
|
||||
\`\`\`
|
||||
| 第12章 山间小憩 | 凌玄、苏晚卿 | 凌玄与苏晚卿在山间歇脚,苏晚卿回忆幼时往事,两人关系略有缓和但未实质推进 | 弱(气氛过渡) | 低 | 25秒 | 平铺+情感 |
|
||||
\`\`\`
|
||||
|
||||
## 提取规则
|
||||
|
||||
- 忠于原文,不推测、不脑补、不加入原文未出现的情节
|
||||
- 角色使用文中主要称呼,保持一致
|
||||
- 多条平行事件线时,选对主角影响最大的一条,其余简要带过
|
||||
- 对话密集章节,关注对话推动了什么结果,而非复述对话内容
|
||||
`;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user