mirror of
https://github.com/HBAI-Ltd/Toonflow-app.git
synced 2026-06-09 16:33:15 +08:00
完善scriptAgent的通讯
This commit is contained in:
12
NOTICES.txt
12
NOTICES.txt
@@ -112,18 +112,18 @@ Repository: https://github.com/axios/axios
|
||||
|
||||
-----------------------------
|
||||
|
||||
Name: best-effort-json-parser
|
||||
License: BSD-2-Clause
|
||||
Repository: https://github.com/beenotung/best-effort-json-parser
|
||||
|
||||
-----------------------------
|
||||
|
||||
Name: better-sqlite3
|
||||
License: MIT
|
||||
Repository: https://github.com/WiseLibs/better-sqlite3
|
||||
|
||||
-----------------------------
|
||||
|
||||
Name: compressing
|
||||
License: MIT
|
||||
Repository: https://github.com/node-modules/compressing
|
||||
|
||||
-----------------------------
|
||||
|
||||
Name: cors
|
||||
License: MIT
|
||||
Repository: https://github.com/expressjs/cors
|
||||
|
||||
6
data/skills/script-agent/decision/SKILL.md
Normal file
6
data/skills/script-agent/decision/SKILL.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: decision
|
||||
description: 剧本生成作决策层。负责分析用户需求、制定执行计划并协调执行层完成制作任务。
|
||||
---
|
||||
|
||||
1. 每次调用 `run_sub_agent` 时,选择 `executionAI` 作为子 Agent,将当前步骤的任务描述作为 `prompt` 传入
|
||||
7
data/skills/script-agent/execution/SKILL.md
Normal file
7
data/skills/script-agent/execution/SKILL.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: execution
|
||||
description: >
|
||||
用户需要生成故事骨架
|
||||
---
|
||||
|
||||
仅需直接回复:喵喵喵
|
||||
@@ -46,7 +46,6 @@
|
||||
"ai": "^6.0.67",
|
||||
"axios": "^1.13.2",
|
||||
"axios-retry": "^4.5.0",
|
||||
"best-effort-json-parser": "^1.2.1",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"compressing": "^2.1.0",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
143
src/agents/scriptAgent/index.ts
Normal file
143
src/agents/scriptAgent/index.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Socket } from "socket.io";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import u from "@/utils";
|
||||
import Memory from "@/utils/agent/memory";
|
||||
import { useSkill } from "@/utils/agent/skillsTools";
|
||||
import useTools from "@/agents/scriptAgent/tools";
|
||||
import ResTool from "@/socket/resTool";
|
||||
|
||||
export interface AgentContext {
|
||||
socket: Socket;
|
||||
isolationKey: string;
|
||||
text: string;
|
||||
abortSignal?: AbortSignal;
|
||||
resTool: ResTool;
|
||||
}
|
||||
|
||||
function buildSystemPrompt(skillPrompt: string, mem: Awaited<ReturnType<Memory["get"]>>): string {
|
||||
let memoryContext = "";
|
||||
if (mem.rag.length) {
|
||||
memoryContext += `[相关记忆]\n${mem.rag.map((r) => r.content).join("\n")}`;
|
||||
}
|
||||
if (mem.summaries.length) {
|
||||
if (memoryContext) memoryContext += "\n\n";
|
||||
memoryContext += `[历史摘要]\n${mem.summaries.map((s, i) => `${i + 1}. ${s.content}`).join("\n")}`;
|
||||
}
|
||||
if (mem.shortTerm.length) {
|
||||
if (memoryContext) memoryContext += "\n\n";
|
||||
memoryContext += `[近期对话]\n${mem.shortTerm.map((m) => `${m.role}: ${m.content}`).join("\n")}`;
|
||||
}
|
||||
if (!memoryContext) return skillPrompt;
|
||||
return `${skillPrompt}\n\n## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`;
|
||||
}
|
||||
|
||||
const subAgentList = ["executionAI", "supervisionAI"] as const;
|
||||
|
||||
export async function decisionAI(ctx: AgentContext) {
|
||||
const { isolationKey, text, abortSignal } = ctx;
|
||||
const memory = new Memory("scriptAgent", isolationKey);
|
||||
await memory.add("user", text);
|
||||
const [skill, mem] = await Promise.all([useSkill("script-agent", "decision"), memory.get(text)]);
|
||||
|
||||
const systemPrompt = buildSystemPrompt(skill.prompt, mem);
|
||||
|
||||
const prefixSystem = `请调用run_sub_agent完成任务`;
|
||||
|
||||
const { textStream } = await u.Ai.Text("scriptAgent").stream({
|
||||
system: prefixSystem + systemPrompt,
|
||||
messages: [{ role: "user", content: text }],
|
||||
abortSignal,
|
||||
tools: {
|
||||
...skill.tools,
|
||||
...memory.getTools(),
|
||||
run_sub_agent: runSubAgent(ctx),
|
||||
...useTools(ctx.resTool),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:decision", completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
return textStream;
|
||||
}
|
||||
|
||||
//====================== 执行层 ======================
|
||||
|
||||
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"), memory.get(text)]);
|
||||
|
||||
const systemPrompt = buildSystemPrompt(skill.prompt, mem);
|
||||
|
||||
const { textStream } = await u.Ai.Text("scriptAgent").stream({
|
||||
system: systemPrompt,
|
||||
messages: [{ role: "user", content: text }],
|
||||
abortSignal,
|
||||
tools: {
|
||||
...skill.tools,
|
||||
...memory.getTools(),
|
||||
...useTools(ctx.resTool),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:execution", completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
return textStream;
|
||||
}
|
||||
|
||||
export async function supervisionAI(ctx: AgentContext) {
|
||||
const { isolationKey, text, abortSignal } = ctx;
|
||||
const memory = new Memory("scriptAgent", isolationKey);
|
||||
const [skill, mem] = await Promise.all([useSkill("script-agent", "supervision"), memory.get(text)]);
|
||||
|
||||
const systemPrompt = buildSystemPrompt(skill.prompt, mem);
|
||||
|
||||
const { textStream } = await u.Ai.Text("scriptAgent").stream({
|
||||
system: systemPrompt,
|
||||
messages: [{ role: "user", content: text }],
|
||||
abortSignal,
|
||||
tools: {
|
||||
...skill.tools,
|
||||
...memory.getTools(),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:supervision", completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
return textStream;
|
||||
}
|
||||
|
||||
//工具函数
|
||||
function runSubAgent(parentCtx: AgentContext) {
|
||||
return tool({
|
||||
description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI",
|
||||
inputSchema: z.object({
|
||||
agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"),
|
||||
prompt: z.string().describe("交给子Agent的任务描述"),
|
||||
}),
|
||||
execute: async ({ agent, prompt }) => {
|
||||
const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];
|
||||
//运行子Agent
|
||||
const subTextStream = await fn({ ...parentCtx, text: prompt });
|
||||
|
||||
let msg: ReturnType<typeof parentCtx.resTool.textMessage>;
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const chunk of subTextStream) {
|
||||
if (!msg!) msg = parentCtx.resTool.textMessage();
|
||||
msg.send(chunk);
|
||||
fullResponse += chunk;
|
||||
}
|
||||
msg!.end();
|
||||
|
||||
return fullResponse;
|
||||
},
|
||||
});
|
||||
}
|
||||
78
src/agents/scriptAgent/tools.ts
Normal file
78
src/agents/scriptAgent/tools.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { tool, Tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import _ from "lodash";
|
||||
import ResTool from "@/socket/resTool";
|
||||
|
||||
export const planData = z.object({
|
||||
event: z.string().describe("章节事件"),
|
||||
storySkeleton: z.string().describe("故事骨架"),
|
||||
adaptationStrategy: z.string().describe("改编策略"),
|
||||
script: z.string().describe("剧本内容"),
|
||||
});
|
||||
|
||||
export type planData = z.infer<typeof planData>;
|
||||
|
||||
const keySchema = z.enum(Object.keys(planData.shape) as [keyof planData, ...Array<keyof planData>]);
|
||||
const planDataKeyLabels = Object.fromEntries(
|
||||
Object.entries(planData.shape).map(([key, schema]) => [key, (schema as z.ZodTypeAny).description ?? key]),
|
||||
) as Record<keyof planData, string>;
|
||||
|
||||
export default (resTool: ResTool, toolsNames?: string[]) => {
|
||||
const { socket } = resTool;
|
||||
const tools: Record<string, Tool> = {
|
||||
get_planData: tool({
|
||||
description: "获取工作区数据",
|
||||
inputSchema: z.object({
|
||||
key: keySchema.describe("数据key"),
|
||||
}),
|
||||
execute: async ({ key }) => {
|
||||
resTool.systemMessage(`正在阅读 ${planDataKeyLabels[key]} 数据...`);
|
||||
console.log("[tools] get_planData", key);
|
||||
const planData: planData = await new Promise((resolve) => socket.emit("getPlanData", { key }, (res: any) => resolve(res)));
|
||||
return planData[key];
|
||||
},
|
||||
}),
|
||||
set_planData_event: tool({
|
||||
description: "保存章节事件到工作区",
|
||||
inputSchema: z.object({ value: planData.shape.event }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_planData event", value);
|
||||
resTool.systemMessage("正在保存 章节事件 数据");
|
||||
socket.emit("setPlanData", { key: "event", value });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
set_planData_storySkeleton: tool({
|
||||
description: "保存故事骨架到工作区",
|
||||
inputSchema: z.object({ value: planData.shape.storySkeleton }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_planData storySkeleton", value);
|
||||
resTool.systemMessage("正在保存 故事骨架 数据");
|
||||
socket.emit("setPlanData", { key: "storySkeleton", value });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
set_planData_adaptationStrategy: tool({
|
||||
description: "保存改编策略到工作区",
|
||||
inputSchema: z.object({ value: planData.shape.adaptationStrategy }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_planData adaptationStrategy", value);
|
||||
resTool.systemMessage("正在保存 改编策略 数据");
|
||||
socket.emit("setPlanData", { key: "adaptationStrategy", value });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
set_planData_script: tool({
|
||||
description: "保存剧本内容到工作区",
|
||||
inputSchema: z.object({ value: planData.shape.script }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_planData script", value);
|
||||
resTool.systemMessage("正在保存 剧本 数据");
|
||||
socket.emit("setPlanData", { key: "script", value });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return toolsNames ? Object.fromEntries(Object.entries(tools).filter(([n]) => toolsNames.includes(n))) : tools;
|
||||
};
|
||||
@@ -10,11 +10,12 @@ export default router.post(
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
episodesId: z.number().optional(),
|
||||
agentType: z.enum(["scriptAgent", "productionAgent"]),
|
||||
type: z.enum(["message", "summary", "all"]).optional(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, episodesId, type = "all" } = req.body;
|
||||
const isolationKey = `${projectId}:${episodesId ?? ""}`;
|
||||
const { projectId, episodesId,agentType, type = "all" } = req.body;
|
||||
const isolationKey = `${projectId}:${agentType}${episodesId ? `:${episodesId}` : ""}`;
|
||||
|
||||
if (type === "all") {
|
||||
await u.db("memories").where({ isolationKey }).del();
|
||||
|
||||
@@ -18,11 +18,12 @@ export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
agentType: z.enum(["scriptAgent", "productionAgent"]),
|
||||
episodesId: z.number().optional(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, episodesId } = req.body;
|
||||
const isolationKey = `${projectId}:${episodesId ?? ""}`;
|
||||
const { projectId, agentType, episodesId } = req.body;
|
||||
const isolationKey = `${projectId}:${agentType}${episodesId ? `:${episodesId}` : ""}`;
|
||||
|
||||
const rows = await u
|
||||
.db("memories")
|
||||
|
||||
@@ -2,7 +2,7 @@ import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import compressing from 'compressing';
|
||||
import compressing from "compressing";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Server } from "socket.io";
|
||||
import productionAgent from "./routes/productionAgent";
|
||||
import chat from "./routes/chat";
|
||||
import scriptAgent from "./routes/scriptAgent";
|
||||
|
||||
export default (io: Server) => {
|
||||
const routes: Record<string, (nsp: ReturnType<Server["of"]>) => void> = {
|
||||
productionAgent,
|
||||
chat,
|
||||
scriptAgent,
|
||||
};
|
||||
|
||||
for (const [name, handler] of Object.entries(routes)) {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Namespace, Socket } from "socket.io";
|
||||
|
||||
const users = new Map<string, string>(); // socketId -> username
|
||||
|
||||
export default (nsp: Namespace) => {
|
||||
nsp.on("connection", (socket: Socket) => {
|
||||
console.log("[chat] 用户已连接:", socket.id);
|
||||
|
||||
socket.on("userLogin", (username: string) => {
|
||||
users.set(socket.id, username);
|
||||
socket.broadcast.emit("notification", `${username} 加入了聊天室`);
|
||||
});
|
||||
|
||||
socket.on("sendMessage", (data: { message: string }) => {
|
||||
const username = users.get(socket.id) || "匿名";
|
||||
const msg = {
|
||||
type: "user" as const,
|
||||
username,
|
||||
message: data.message,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
};
|
||||
nsp.emit("newMessage", msg);
|
||||
});
|
||||
|
||||
socket.on("typing", () => {
|
||||
const username = users.get(socket.id);
|
||||
if (username) socket.broadcast.emit("userTyping", username);
|
||||
});
|
||||
|
||||
socket.on("stopTyping", () => {
|
||||
socket.broadcast.emit("userStopTyping");
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
const username = users.get(socket.id);
|
||||
if (username) {
|
||||
users.delete(socket.id);
|
||||
socket.broadcast.emit("notification", `${username} 离开了聊天室`);
|
||||
}
|
||||
console.log("[chat] 用户已断开:", socket.id);
|
||||
});
|
||||
});
|
||||
};
|
||||
69
src/socket/routes/scriptAgent.ts
Normal file
69
src/socket/routes/scriptAgent.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import u from "@/utils";
|
||||
import { Namespace, Socket } from "socket.io";
|
||||
import * as agent from "@/agents/scriptAgent/index";
|
||||
import ResTool from "@/socket/resTool";
|
||||
|
||||
async function verifyToken(rawToken: string): Promise<Boolean> {
|
||||
const setting = await u.db("o_setting").where("key", "tokenKey").select("value").first();
|
||||
if (!setting) return false;
|
||||
const { value: tokenKey } = setting;
|
||||
if (!rawToken) return false;
|
||||
const token = rawToken.replace("Bearer ", "");
|
||||
try {
|
||||
jwt.verify(token, tokenKey as string);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default (nsp: Namespace) => {
|
||||
nsp.on("connection", async (socket: Socket) => {
|
||||
const token = socket.handshake.auth.token;
|
||||
if (!token || !(await verifyToken(token))) {
|
||||
console.log("[scriptAgent] 连接失败,token无效");
|
||||
socket.disconnect();
|
||||
return;
|
||||
}
|
||||
const isolationKey = socket.handshake.auth.isolationKey;
|
||||
if (!isolationKey) {
|
||||
console.log("[scriptAgent] 连接失败,缺少 isolationKey");
|
||||
socket.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[scriptAgent] 已连接:", socket.id);
|
||||
|
||||
const resTool = new ResTool(socket);
|
||||
let abortController: AbortController | null = null;
|
||||
|
||||
socket.on("message", async (text: string) => {
|
||||
abortController?.abort();
|
||||
abortController = new AbortController();
|
||||
const currentController = abortController;
|
||||
|
||||
const textStream = await agent.decisionAI({ socket, isolationKey, text, abortSignal: currentController.signal, resTool });
|
||||
|
||||
let msg = resTool.textMessage();
|
||||
|
||||
try {
|
||||
for await (const chunk of textStream) {
|
||||
msg.send(chunk);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name !== "AbortError") throw err;
|
||||
} finally {
|
||||
msg.end();
|
||||
if (abortController === currentController) {
|
||||
abortController = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("stop", () => {
|
||||
abortController?.abort();
|
||||
abortController = null;
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1267,11 +1267,6 @@ basic-auth@~2.0.1:
|
||||
dependencies:
|
||||
safe-buffer "5.1.2"
|
||||
|
||||
best-effort-json-parser@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmmirror.com/best-effort-json-parser/-/best-effort-json-parser-1.2.1.tgz#e8d0b8355a0c268d918681faa0e3cf6aa192ea00"
|
||||
integrity sha512-UICSLibQdzS1f+PBsi3u2YE3SsdXcWicHUg3IMvfuaePS2AYnZJdJeKhGv5OM8/mqJwPt79aDrEJ1oa84tELvw==
|
||||
|
||||
better-sqlite3@^12.6.2:
|
||||
version "12.6.2"
|
||||
resolved "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-12.6.2.tgz#770649f28a62e543a360f3dfa1afe4cc944b1937"
|
||||
|
||||
Reference in New Issue
Block a user