完善scriptAgent的通讯
This commit is contained in:
parent
e927e47489
commit
b32bfc93fa
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
|
Name: better-sqlite3
|
||||||
License: MIT
|
License: MIT
|
||||||
Repository: https://github.com/WiseLibs/better-sqlite3
|
Repository: https://github.com/WiseLibs/better-sqlite3
|
||||||
|
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
Name: compressing
|
||||||
|
License: MIT
|
||||||
|
Repository: https://github.com/node-modules/compressing
|
||||||
|
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
Name: cors
|
Name: cors
|
||||||
License: MIT
|
License: MIT
|
||||||
Repository: https://github.com/expressjs/cors
|
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",
|
"ai": "^6.0.67",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"axios-retry": "^4.5.0",
|
"axios-retry": "^4.5.0",
|
||||||
"best-effort-json-parser": "^1.2.1",
|
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"compressing": "^2.1.0",
|
"compressing": "^2.1.0",
|
||||||
"cors": "^2.8.5",
|
"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({
|
validateFields({
|
||||||
projectId: z.number(),
|
projectId: z.number(),
|
||||||
episodesId: z.number().optional(),
|
episodesId: z.number().optional(),
|
||||||
|
agentType: z.enum(["scriptAgent", "productionAgent"]),
|
||||||
type: z.enum(["message", "summary", "all"]).optional(),
|
type: z.enum(["message", "summary", "all"]).optional(),
|
||||||
}),
|
}),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const { projectId, episodesId, type = "all" } = req.body;
|
const { projectId, episodesId,agentType, type = "all" } = req.body;
|
||||||
const isolationKey = `${projectId}:${episodesId ?? ""}`;
|
const isolationKey = `${projectId}:${agentType}${episodesId ? `:${episodesId}` : ""}`;
|
||||||
|
|
||||||
if (type === "all") {
|
if (type === "all") {
|
||||||
await u.db("memories").where({ isolationKey }).del();
|
await u.db("memories").where({ isolationKey }).del();
|
||||||
|
|||||||
@ -18,11 +18,12 @@ export default router.post(
|
|||||||
"/",
|
"/",
|
||||||
validateFields({
|
validateFields({
|
||||||
projectId: z.number(),
|
projectId: z.number(),
|
||||||
|
agentType: z.enum(["scriptAgent", "productionAgent"]),
|
||||||
episodesId: z.number().optional(),
|
episodesId: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const { projectId, episodesId } = req.body;
|
const { projectId, agentType, episodesId } = req.body;
|
||||||
const isolationKey = `${projectId}:${episodesId ?? ""}`;
|
const isolationKey = `${projectId}:${agentType}${episodesId ? `:${episodesId}` : ""}`;
|
||||||
|
|
||||||
const rows = await u
|
const rows = await u
|
||||||
.db("memories")
|
.db("memories")
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import express from "express";
|
|||||||
import u from "@/utils";
|
import u from "@/utils";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { success } from "@/lib/responseFormat";
|
import { success } from "@/lib/responseFormat";
|
||||||
import compressing from 'compressing';
|
import compressing from "compressing";
|
||||||
import { validateFields } from "@/middleware/middleware";
|
import { validateFields } from "@/middleware/middleware";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import productionAgent from "./routes/productionAgent";
|
import productionAgent from "./routes/productionAgent";
|
||||||
import chat from "./routes/chat";
|
import scriptAgent from "./routes/scriptAgent";
|
||||||
|
|
||||||
export default (io: Server) => {
|
export default (io: Server) => {
|
||||||
const routes: Record<string, (nsp: ReturnType<Server["of"]>) => void> = {
|
const routes: Record<string, (nsp: ReturnType<Server["of"]>) => void> = {
|
||||||
productionAgent,
|
productionAgent,
|
||||||
chat,
|
scriptAgent,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [name, handler] of Object.entries(routes)) {
|
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:
|
dependencies:
|
||||||
safe-buffer "5.1.2"
|
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:
|
better-sqlite3@^12.6.2:
|
||||||
version "12.6.2"
|
version "12.6.2"
|
||||||
resolved "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-12.6.2.tgz#770649f28a62e543a360f3dfa1afe4cc944b1937"
|
resolved "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-12.6.2.tgz#770649f28a62e543a360f3dfa1afe4cc944b1937"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user