diff --git a/NOTICES.txt b/NOTICES.txt index ef08f0b..00b6afe 100644 --- a/NOTICES.txt +++ b/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 diff --git a/data/skills/script-agent/decision/SKILL.md b/data/skills/script-agent/decision/SKILL.md new file mode 100644 index 0000000..a709d6a --- /dev/null +++ b/data/skills/script-agent/decision/SKILL.md @@ -0,0 +1,6 @@ +--- +name: decision +description: 剧本生成作决策层。负责分析用户需求、制定执行计划并协调执行层完成制作任务。 +--- + +1. 每次调用 `run_sub_agent` 时,选择 `executionAI` 作为子 Agent,将当前步骤的任务描述作为 `prompt` 传入 \ No newline at end of file diff --git a/data/skills/script-agent/execution/SKILL.md b/data/skills/script-agent/execution/SKILL.md new file mode 100644 index 0000000..1291c43 --- /dev/null +++ b/data/skills/script-agent/execution/SKILL.md @@ -0,0 +1,7 @@ +--- +name: execution +description: > + 用户需要生成故事骨架 +--- + +仅需直接回复:喵喵喵 diff --git a/package.json b/package.json index d662544..54224ce 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/agents/scriptAgent/index.ts b/src/agents/scriptAgent/index.ts new file mode 100644 index 0000000..bffc0b9 --- /dev/null +++ b/src/agents/scriptAgent/index.ts @@ -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>): 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; + let fullResponse = ""; + + for await (const chunk of subTextStream) { + if (!msg!) msg = parentCtx.resTool.textMessage(); + msg.send(chunk); + fullResponse += chunk; + } + msg!.end(); + + return fullResponse; + }, + }); +} diff --git a/src/agents/scriptAgent/tools.ts b/src/agents/scriptAgent/tools.ts new file mode 100644 index 0000000..8f8cf9e --- /dev/null +++ b/src/agents/scriptAgent/tools.ts @@ -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; + +const keySchema = z.enum(Object.keys(planData.shape) as [keyof planData, ...Array]); +const planDataKeyLabels = Object.fromEntries( + Object.entries(planData.shape).map(([key, schema]) => [key, (schema as z.ZodTypeAny).description ?? key]), +) as Record; + +export default (resTool: ResTool, toolsNames?: string[]) => { + const { socket } = resTool; + const tools: Record = { + 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; +}; diff --git a/src/routes/agents/clearMemory.ts b/src/routes/agents/clearMemory.ts index 7f2249f..5d40eca 100644 --- a/src/routes/agents/clearMemory.ts +++ b/src/routes/agents/clearMemory.ts @@ -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(); diff --git a/src/routes/agents/getMemory.ts b/src/routes/agents/getMemory.ts index 3c129ca..4aece69 100644 --- a/src/routes/agents/getMemory.ts +++ b/src/routes/agents/getMemory.ts @@ -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") diff --git a/src/routes/script/exportScript.ts b/src/routes/script/exportScript.ts index 739506b..7028bc3 100644 --- a/src/routes/script/exportScript.ts +++ b/src/routes/script/exportScript.ts @@ -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(); diff --git a/src/socket/index.ts b/src/socket/index.ts index 7106656..17d61c4 100644 --- a/src/socket/index.ts +++ b/src/socket/index.ts @@ -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) => void> = { productionAgent, - chat, + scriptAgent, }; for (const [name, handler] of Object.entries(routes)) { diff --git a/src/socket/routes/chat.ts b/src/socket/routes/chat.ts deleted file mode 100644 index bedd1ac..0000000 --- a/src/socket/routes/chat.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Namespace, Socket } from "socket.io"; - -const users = new Map(); // 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); - }); - }); -}; diff --git a/src/socket/routes/scriptAgent.ts b/src/socket/routes/scriptAgent.ts new file mode 100644 index 0000000..a543f78 --- /dev/null +++ b/src/socket/routes/scriptAgent.ts @@ -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 { + 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; + }); + }); +}; diff --git a/yarn.lock b/yarn.lock index af1ce62..50795ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"