UNPKG

koishi-plugin-chatluna-plugin-common

Version:
1,407 lines (1,392 loc) 114 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/locales/zh-CN.schema.yml var require_zh_CN_schema = __commonJS({ "src/locales/zh-CN.schema.yml"(exports2, module2) { module2.exports = { $inner: [{}, { $desc: "对话思考功能", think: "启用思考功能,可显著提高 Agent 模式的调用质量。", send: "启用提前消息发送工具。", todos: "启用待办事项功能,可让 Agent 生成多步骤的待办事项,自动拆解任务。", chat: "启用交互询问功能。注意:会导致工具链重建。" }, { $desc: "创意生成功能", draw: "启用文生图功能,调用 Koishi 文生图插件。", music: "启用简易音频生成功能。(需要安装并启用 [@dgck81lnn/music](https://github.com/DGCK81LNN/koishi-plugin-music)" }, { $desc: "系统工具功能", request: "启用 request 插件,提供 GET/POST 请求接口。", fs: "启用 fs 插件,提供文件读写接口。", command: "启用指令辅助功能,允许执行 Koishi 机器人指令。", cron: "启用定时提醒功能。", codeSandbox: "启用 Python 代码执行功能。(需要申请 API)" }, { $desc: "扩展服务功能", group: "启用群管插件,提供群管理能力。", knowledge: "启用知识库插件调用功能。(需要安装知识库插件)", actions: "启用 OpenAPI 工具调用功能。(需要启用 request 工具才能请求)" }, [{ $desc: "request 插件配置", requestMaxOutputLength: "request 插件最大输出长度。", requestSelector: "触发 request 工具的关键词。为空时始终选中。", requestHeaders: { $desc: "根据域名匹配设置请求头。", $inner: { matcher: "域名匹配模式(支持通配符,如 *.example.com, api.github.com)。", headers: "该域名匹配时应用的请求头。" } } }], [{ $desc: "fs 插件配置", fsScopePath: "fs 插件作用域路径。留空则为系统任意路径。", fsSelector: "触发 fs 工具的关键词。为空时始终选中。", fsIgnores: "默认忽略的文件夹表达式。" }], [{ $desc: "指令插件配置", commandWithSend: "是否默认发送指令的执行结果。", commandList: { $desc: "可用指令列表。不填写则默认注册所有一级指令。", $inner: { command: "需要触发的指令。", description: "指令工具的描述。请尽量简洁有力的描述这个指令的用途。", selector: "触发指令工具的关键词。", confirm: "执行指令时是否需要二次确认。" } }, commandBlacklist: "屏蔽的一级指令列表(包括所有子指令)。默认屏蔽敏感的管理类指令。" }], [{ $desc: "群管插件配置", groupScopeSelector: { $desc: "允许使用群管功能的成员 ID 列表。" }, groupWhitelist: { $desc: "允许使用群管功能的群 ID 白名单。为空时在所有群可用。" } }], [{ $desc: "定时任务插件配置", cronScopeSelector: { $desc: "允许使用命令类型定时任务的成员 ID 列表。为空时所有人都可以创建提醒任务,但无法创建命令任务。" } }], [{ $desc: "画图插件配置", drawPrompt: "画图插件提示 prompt。", drawCommand: "绘图执行指令。{prompt} 为调用时的 prompt。", drawSelector: "触发绘画工具的关键词。为空时始终选中。" }], [{ $desc: "音乐生成插件配置", musicSelector: "触发音乐生成工具的关键词。为空时始终选中。" }], [{ $desc: "OpenAPI 工具调用插件配置", actionsList: { $desc: "可用 OpenAPI 工具列表。", $inner: { name: "工具名称。请使用纯英文的名称。", description: "工具描述。", headers: "工具请求头。", openAPISpec: "OpenAPI 规范文件内容。", selector: "触发工具的关键词。" } } }]] }; } }); // src/locales/en-US.schema.yml var require_en_US_schema = __commonJS({ "src/locales/en-US.schema.yml"(exports2, module2) { module2.exports = { $inner: [{}, { $desc: "Conversation & AI Features", think: "Enable thinking function (may improve responses)", send: "Enable pre-message sending", todos: "Enable task decomposition. Can generate multi-step task lists and automatically break down tasks", chat: "Enable interactive query. Note: Rebuilds tool chain" }, { $desc: "Creative Content Features", draw: "Enable text-to-image (uses Koishi text-to-image plugin)", music: "Enable simple audio generation. (Requires installation and activation of [@dgck81lnn/music](https://github.com/DGCK81LNN/koishi-plugin-music))" }, { $desc: "System Tools Features", request: "Enable request plugin (GET/POST interface)", fs: "Enable fs plugin (file read/write interface)", command: "Enable command assistance (execute Koishi bot commands)", codeSandbox: "Enable Python code execution (requires API key)", cron: "Enable scheduled reminders. " }, { $desc: "Extended Services Features", group: "Enable group management plugin", knowledge: "Enable knowledge base plugin calls", actions: "Enable OpenAPI tool calls (requires request tool to be enabled)" }, [{ $desc: "Request Plugin Configuration", requestMaxOutputLength: "Max output length for request plugin", requestSelector: "Keywords to trigger request tool. Always selected when empty.", requestHeaders: { $desc: "Configure request headers based on domain matching.", $inner: { matcher: "Domain matcher pattern (supports wildcards, e.g., *.example.com, api.github.com).", headers: "Request headers to apply when this domain matches." } } }], [{ $desc: "File Plugin Configuration", fsScopePath: "Scope path for file plugin. Empty for system-wide access", fsSelector: "Keywords to trigger file system tools. Always selected when empty.", fsIgnores: "Default ignored folder expressions." }], [{ $desc: "Command Plugin Configuration", commandWithSend: "Whether to send command execution results by default.", commandList: { $desc: "Available commands. If empty, all first-level commands are registered", $inner: { command: "Command to trigger", description: "Description of the command tool", selector: "Keyword to trigger the command", confirm: "Whether validation is required when executing the command" } }, commandBlacklist: "Blacklist of top-level commands to block (including all sub-commands). Sensitive management commands are blocked by default." }], [{ $desc: "Group Management Plugin Configuration", groupScopeSelector: { $desc: "Member IDs allowed to use group management functions" }, groupWhitelist: { $desc: "Group ID whitelist for using group management functions. Available in all groups when empty." } }], [{ $desc: "Cron Task Plugin Configuration", cronScopeSelector: { $desc: "Member IDs allowed to create command-type scheduled tasks. When empty, everyone can create reminder tasks but cannot create command tasks." } }], [{ $desc: "Drawing Plugin Configuration", drawPrompt: "Prompt for drawing plugin", drawCommand: "Drawing execution command. {prompt} is the prompt used", drawSelector: "Keywords to trigger drawing tool. Always selected when empty." }], [{ $desc: "Music Generation Plugin Configuration", musicSelector: "Keywords to trigger music generation tool. Always selected when empty." }], [{ $desc: "OpenAPI Tool Call Plugin Configuration", actionsList: { $desc: "List of available OpenAPI tools", $inner: { name: "Tool name. Please use English names only.", description: "Tool description", headers: "Request headers", openAPISpec: "OpenAPI specification content", selector: "Keywords to trigger the tool" } } }]] }; } }); // src/index.ts var index_exports = {}; __export(index_exports, { Config: () => Config2, apply: () => apply11, inject: () => inject, logger: () => logger, name: () => name }); module.exports = __toCommonJS(index_exports); var import_chat2 = require("koishi-plugin-chatluna/services/chat"); // src/plugins/command.ts var import_tools = require("@langchain/core/tools"); var import_string = require("koishi-plugin-chatluna/utils/string"); var import_zod = require("zod"); var import_crypto = require("crypto"); async function apply(ctx, config, plugin2) { if (config.command !== true) { return; } const commandList = getCommandList( ctx, config.commandList, config.commandBlacklist ); for (const command of commandList) { const prompt = generateSingleCommandPrompt(command); const normalizedName = normalizeCommandName(command.name); plugin2.registerTool(`command_execute_${normalizedName}`, { selector(history) { if (command.selector == null || command.selector.length === 0) { return true; } return history.some((item) => { const content = (0, import_string.getMessageContent)(item.content); return (0, import_string.fuzzyQuery)(content, [ "令", "调用", "获取", "get", "help", "command", "执行", "用", "execute", ...command.name.split("."), ...command.selector ?? [] ]); }); }, createTool(params) { return new CommandExecuteTool( ctx, `${normalizedName}`, prompt, command, config.commandWithSend ); } }); } } __name(apply, "apply"); function generateSingleCommandPrompt(command) { return `Tool Description: ${command.description || "No description"} `; } __name(generateSingleCommandPrompt, "generateSingleCommandPrompt"); function getDescription(description) { if (typeof description === "string") { return description; } return description["zh-CN"] || description[""] || description["en-US"] || "No description"; } __name(getDescription, "getDescription"); function getCommandList(ctx, rawCommandList, blacklist = []) { const commandMap = new Map( ctx.$commander._commandList.filter((item) => { if (item.name.includes("chatluna")) { return false; } for (const blocked of blacklist) { if (item.name === blocked || item.name.startsWith(blocked + ".")) { return false; } } return true; }).map((cmd) => [cmd.name, cmd.toJSON()]) ); if (rawCommandList.length > 0) { return rawCommandList.map((rawCommand) => { const item = commandMap.get(rawCommand.command); if (!item) { ctx.logger.warn( `Command "${rawCommand.command}" not found in command list` ); return null; } let description = rawCommand.description; if ((rawCommand.description?.length ?? 0) < 1 && item.description) { description = JSON.stringify(item.description); } return { ...item, selector: rawCommand.selector, confirm: rawCommand.confirm ?? true, description }; }).filter((item) => item !== null); } return Array.from(commandMap.values()).map((item) => ({ ...item, confirm: true, description: typeof item.description === "string" ? item.description : JSON.stringify(item.description) })); } __name(getCommandList, "getCommandList"); var CommandExecuteTool = class extends import_tools.StructuredTool { constructor(ctx, name2, description, command, commandWithSend) { super(); this.ctx = ctx; this.name = name2; this.description = description; this.command = command; this.commandWithSend = commandWithSend; this.schema = this.generateSchema(); } static { __name(this, "CommandExecuteTool"); } schema = import_zod.z.object({ // eslint-disable-next-line @typescript-eslint/no-explicit-any }); // eslint-disable-next-line @typescript-eslint/no-explicit-any generateSchema() { const schemaShape = {}; this.command.arguments.forEach((arg) => { const zodType = this.getZodType(arg.type); const description = getDescription(arg.description); const zodTypeWithDescription = zodType.describe(description); schemaShape[arg.name] = arg.required ? zodTypeWithDescription : zodTypeWithDescription.optional(); }); this.command.options.forEach((opt) => { if (opt.name !== "help") { const zodType = this.getZodType(opt.type); const description = getDescription(opt.description); const zodTypeWithDescription = zodType.describe(description); schemaShape[opt.name] = opt.required ? zodTypeWithDescription : zodTypeWithDescription.optional(); } }); if (Object.keys(schemaShape).length < 1) { return import_zod.z.object({ input: import_zod.z.string().optional().describe("Input for the command") }); } return import_zod.z.object(schemaShape); } getZodType(type) { switch (type) { case "text": case "string": case "date": return import_zod.z.string(); case "integer": case "posint": case "natural": case "number": return import_zod.z.number(); case "boolean": return import_zod.z.boolean(); default: return import_zod.z.string(); } } /** @ignore */ async _call(input, runManager, config) { const koishiCommand = this.parseInput(input); const session = config.configurable.session; if (this.command.confirm ?? true) { const validationString = randomString(8); await session.send( `模型请求执行指令 ${koishiCommand},如需同意,请输入以下字符:${validationString}` ); const canRun = await session.prompt(); if (canRun !== validationString) { await session.send("指令执行失败"); return `The command ${koishiCommand} execution failed, because the user didn't confirm`; } } try { const result = await session.execute(koishiCommand, true); let commandWithSend = this.commandWithSend; const transformedMessage = await this.ctx.chatluna.messageTransformer.transform( session, result, "" ); const content = typeof transformedMessage.content === "string" ? transformedMessage.content : transformedMessage.content.map((part) => { if ((0, import_string.isMessageContentText)(part)) { return part.text; } if ((0, import_string.isMessageContentImageUrl)(part)) { const imageUrl = typeof part.image_url === "string" ? part.image_url : part.image_url.url; if (imageUrl.includes("data:")) { commandWithSend = true; return `[image:${imageUrl.substring(0, 12)}]`; } return `[image:${imageUrl}] Please use ![image](url) send image to user`; } }).join("\n\n"); if (commandWithSend) { await session.send(result); } return `Successfully executed command ${koishiCommand} with result: ${content}`; } catch (e) { this.ctx.logger.error(e); return `The command ${koishiCommand} execution failed, because ${e.message}`; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any parseInput(input) { try { const args = []; const options = []; this.command.arguments.forEach((arg) => { if (arg.name in input) { args.push(String(input[arg.name])); } }); this.command.options.forEach((opt) => { if (opt.name in input && opt.name !== "help") { if (opt.type === "boolean") { if (input[opt.name]) { options.push(`--${opt.name}`); } } else { options.push(`--${opt.name}`, String(input[opt.name])); } } }); const fullCommand = [this.command.name, ...args, ...options].join(" ").trim(); return fullCommand; } catch (error) { console.error("Failed to parse JSON input:", error); throw new Error("Invalid JSON input"); } } }; function randomString(size) { let text = ""; const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (let i = 0; i < size; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; } __name(randomString, "randomString"); function elementToString(elements) { return elements.map((h2) => h2.toString(true)).join("\n\n"); } __name(elementToString, "elementToString"); function normalizeCommandName(name2) { const chineseToEnglish = { // Common command terms 帮助: "help", 列表: "list", 查询: "query", 搜索: "search", 添加: "add", 删除: "delete", 修改: "modify", 更新: "update", 获取: "get", 设置: "set", 创建: "create", 移除: "remove", 显示: "show", 查看: "view", 编辑: "edit", 保存: "save", 加载: "load", 启动: "start", 停止: "stop", 重启: "restart", 状态: "status", 信息: "info", 配置: "config", 管理: "manage", 用户: "user", 消息: "message", 发送: "send", 接收: "receive", 清除: "clear", 重置: "reset", 导入: "import", 导出: "export", 测试: "test", 运行: "run", 执行: "execute", 调用: "call", 刷新: "refresh", 同步: "sync", 连接: "connect", 断开: "disconnect", 登录: "login", 登出: "logout", 注册: "register", 验证: "verify", 授权: "authorize", 禁用: "disable", 启用: "enable", 切换: "toggle", 复制: "copy", 粘贴: "paste", 剪切: "cut", 撤销: "undo", 重做: "redo", 分享: "share", 上传: "upload", 下载: "download", 安装: "install", 卸载: "uninstall", 备份: "backup", 恢复: "restore", 统计: "stats", 分析: "analyze", 报告: "report", 通知: "notify", 提醒: "remind", 订阅: "subscribe", 取消: "cancel", 确认: "confirm", 拒绝: "reject", 接受: "accept", 批准: "approve", 审核: "review", 检查: "check", 扫描: "scan", 过滤: "filter", 排序: "sort", 分组: "group", 合并: "merge", 拆分: "split", 转换: "convert", 翻译: "translate", 计算: "calculate", 比较: "compare", 匹配: "match", 替换: "replace", 插入: "insert", 追加: "append", 前置: "prepend", 打开: "open", 关闭: "close", 锁定: "lock", 解锁: "unlock", 隐藏: "hide", 展开: "expand", 折叠: "collapse", 最小化: "minimize", 最大化: "maximize", 全屏: "fullscreen", 退出: "exit", 返回: "back", 前进: "forward", 跳转: "jump", 导航: "navigate", 定位: "locate", 标记: "mark", 高亮: "highlight", 选择: "select", 取消选择: "deselect", 全选: "selectall", 反选: "invert", 预览: "preview", 打印: "print", 格式化: "format", 美化: "beautify", 压缩: "compress", 解压: "decompress", 加密: "encrypt", 解密: "decrypt", 签名: "sign", 验签: "verifysign", 哈希: "hash", 编码: "encode", 解码: "decode", 解析: "parse", 生成: "generate", 构建: "build", 编译: "compile", 部署: "deploy", 发布: "publish", 回滚: "rollback", 监控: "monitor", 调试: "debug", 日志: "log", 记录: "record", 追踪: "trace", 性能: "performance", 优化: "optimize", 清理: "clean", 维护: "maintain", 修复: "fix", 诊断: "diagnose", 健康: "health", 版本: "version", 关于: "about", 许可: "license", 文档: "doc", 示例: "example", 教程: "tutorial", 指南: "guide", 参考: "reference", 索引: "index", 目录: "catalog", 分类: "category", 标签: "tag", 评论: "comment", 回复: "reply", 点赞: "like", 收藏: "favorite", 关注: "follow", 推荐: "recommend", 排行: "rank", 热门: "hot", 最新: "latest", 随机: "random" }; let result = name2; for (const [chinese, english] of Object.entries(chineseToEnglish)) { result = result.replace(new RegExp(chinese, "g"), english); } result = result.replace(/[^a-zA-Z0-9.]/g, ""); if (result.length === 0 || /^[0-9]/.test(result)) { result = "cmd" + (result || (0, import_crypto.randomUUID)().substring(0, 12).replace(/[^a-zA-Z0-9]/g, "")); } return result; } __name(normalizeCommandName, "normalizeCommandName"); // src/plugins/cron.ts var import_tools2 = require("@langchain/core/tools"); var import_koishi = require("koishi"); var import_string2 = require("koishi-plugin-chatluna/utils/string"); var import_zod2 = require("zod"); async function apply2(ctx, config, plugin2) { if (config.cron !== true) { return; } ctx.model.extend( "chatluna_cron_task", { id: "unsigned", selfId: "string", userId: "string", groupId: "string", guildId: "string", channelId: "string", type: "string", time: "timestamp", lastCall: "timestamp", interval: "integer", content: "text", recipient: "string", command: "text", event: "json", createdAt: "timestamp", executorUserId: "string" }, { autoInc: true } ); const scheduledTasks = {}; async function hasTask(id) { const data = await ctx.database.get("chatluna_cron_task", [id]); return data.length > 0; } __name(hasTask, "hasTask"); async function prepareTask(task, session) { const now = Date.now(); const date = task.time.valueOf(); async function executeTask() { ctx.logger.debug("execute task %d: %s", task.id, task.content); try { if (task.type === "notification") { await sendNotification(task, session); } else if (task.type === "command" && task.command) { await session.execute(task.command); } } catch (error) { ctx.logger.warn(error); } if (!task.lastCall || !task.interval) return; task.lastCall = /* @__PURE__ */ new Date(); await ctx.database.set("chatluna_cron_task", task.id, { lastCall: task.lastCall }); } __name(executeTask, "executeTask"); if (!task.interval) { if (date < now) { await ctx.database.remove("chatluna_cron_task", [task.id]); if (task.lastCall) await executeTask(); return; } ctx.logger.debug( "prepare task %d: %s at %s", task.id, task.content, task.time ); const dispose2 = ctx.setTimeout(async () => { if (!await hasTask(task.id)) return; await ctx.database.remove("chatluna_cron_task", [task.id]); await executeTask(); }, date - now); if (!scheduledTasks[task.id]) scheduledTasks[task.id] = []; scheduledTasks[task.id].push(dispose2); return; } ctx.logger.debug( "prepare task %d: %s from %s every %dms", task.id, task.content, task.time, task.interval ); const timeout = date < now ? task.interval - (now - date) % task.interval : date - now; if (task.lastCall && timeout + now - task.interval > +task.lastCall) { await executeTask(); } const dispose = ctx.setTimeout(async () => { if (!await hasTask(task.id)) return; const intervalDispose = ctx.setInterval(async () => { if (!await hasTask(task.id)) { intervalDispose(); return; } await executeTask(); }, task.interval); await executeTask(); }, timeout); if (!scheduledTasks[task.id]) scheduledTasks[task.id] = []; scheduledTasks[task.id].push(dispose); } __name(prepareTask, "prepareTask"); async function sendNotification(task, session) { const message = task.content; const atElement = import_koishi.h.at( task.recipient === "self" ? session.userId : task.recipient ); await session.send([ !session.isDirect ? atElement : "", import_koishi.h.text(message) ]); } __name(sendNotification, "sendNotification"); ctx.on("ready", async () => { const tasks = await ctx.database.get("chatluna_cron_task", {}); const tasksByBot = {}; tasks.forEach((task) => { if (!task.event) return; const bot = ctx.bots[task.selfId]; if (bot) { prepareTask(task, bot.session(task.event)); } else { ; (tasksByBot[task.selfId] ||= []).push(task); } }); ctx.on("bot-status-updated", (bot) => { if (bot.status !== import_koishi.Universal.Status.ONLINE) return; const items = tasksByBot[bot.sid]; if (!items) return; delete tasksByBot[bot.sid]; items.forEach((task) => { prepareTask(task, bot.session(task.event)); }); }); }); plugin2.registerTool("cron", { selector(history) { return history.some( (message) => (0, import_string2.fuzzyQuery)((0, import_string2.getMessageContent)(message.content), [ "定时", "任务", "醒", "用", "do", "提示", "秒", "分", "时", "天", "星期", "cron", "task", "schedule", "remind", "notification" ]) ); }, createTool(params) { return new CronTool(ctx, config, scheduledTasks, prepareTask); } }); } __name(apply2, "apply"); var CronTool = class extends import_tools2.StructuredTool { constructor(ctx, config, scheduledTasks, prepareTask) { super({}); this.ctx = ctx; this.config = config; this.scheduledTasks = scheduledTasks; this.prepareTask = prepareTask; } static { __name(this, "CronTool"); } name = "cron"; schema = import_zod2.z.object({ action: import_zod2.z.enum(["create", "get", "cancel"]).describe( "The action to perform: create (add new task), get (list tasks), cancel (delete task)" ), type: import_zod2.z.enum(["notification", "command"]).optional().describe( "Task type: notification (send reminder) or command (execute command). Required for create action." ), time: import_zod2.z.string().optional().describe( 'Time format: Xs/Xm/Xh/Xd (delay), HH:MM (specific time), or "time / interval" for repeating. Required for create action.' ), content: import_zod2.z.string().optional().describe( "The message content for notification or command to execute. Required for create action." ), recipient: import_zod2.z.string().optional().describe( 'For notification: "self" (default), "group", or user ID' ), executorUserId: import_zod2.z.string().optional().describe( 'User ID who executes the command. For command type: actual user ID for user-requested, empty defaults to current user. Note: "0" will be converted to current user.' ), taskId: import_zod2.z.number().optional().describe("Task ID for cancel action") }); async _call(input, _, config) { const session = config.configurable.session; const { action, type, time, content, recipient, executorUserId, taskId } = input; switch (action) { case "get": return await this.listTasks(session); case "cancel": return await this.cancelTask(taskId, session); case "create": return await this.createTask( session, type, time, content, recipient, executorUserId ); default: return `Unknown action: ${action}`; } } async listTasks(session) { const tasks = await this.ctx.database.get("chatluna_cron_task", { selfId: session.selfId, userId: session.userId }); if (tasks.length === 0) { await session.send("No scheduled tasks found."); return "No tasks found"; } const taskList = tasks.map((task, idx) => { const timeStr = formatTime(task.time, task.interval); const typeStr = task.type === "notification" ? "Reminder" : "Command"; return `${task.id}. ${typeStr} - ${timeStr}: ${task.content}`; }).join("\n"); await session.send(`Your scheduled tasks: ${taskList}`); return JSON.stringify( tasks.map((t) => ({ id: t.id, type: t.type, time: t.time, content: t.content })) ); } async cancelTask(taskId, session) { if (!taskId) { return "Task ID is required for cancel action"; } const task = await this.ctx.database.get("chatluna_cron_task", [taskId]); if (!task.length || task[0].userId !== session.userId) { await session.send( `Task ${taskId} not found or you don't have permission to cancel it.` ); return `Task ${taskId} not found`; } if (this.scheduledTasks[taskId]) { this.scheduledTasks[taskId].forEach((dispose) => dispose()); delete this.scheduledTasks[taskId]; } await this.ctx.database.remove("chatluna_cron_task", [taskId]); await session.send(`Task ${taskId} has been cancelled.`); return `Task ${taskId} cancelled successfully`; } async createTask(session, type, time, content, recipient, executorUserId) { if (!type || !time || !content) { return "Type, time, and content are required for create action"; } let finalExecutorUserId = executorUserId || session.userId; if (finalExecutorUserId === "0") { finalExecutorUserId = session.userId; } if (type === "command") { const hasPermission = this.config.cronScopeSelector?.includes(finalExecutorUserId); if (!hasPermission) { await session.send( `Error: User ${finalExecutorUserId} does not have permission to create command tasks.` ); return `Permission denied for user ${finalExecutorUserId}`; } } const parsedTime = parseTimeString(time); if (!parsedTime) { await session.send( `Invalid time format: ${time}. Use formats like: 10s, 5m, 2h, 1d, 14:30, or "10m / 1h" for repeating tasks.` ); return `Invalid time format: ${time}`; } const task = await this.ctx.database.create("chatluna_cron_task", { selfId: session.selfId, userId: session.userId, groupId: session.event.guild?.id, guildId: session.guildId, channelId: session.channelId, type, time: parsedTime.time, interval: parsedTime.interval, content, recipient: recipient || "self", command: type === "command" ? content : void 0, event: session.event, createdAt: /* @__PURE__ */ new Date(), executorUserId: finalExecutorUserId }); await this.prepareTask(task, session); const timeStr = formatTime(parsedTime.time, parsedTime.interval); const typeLabel = type === "notification" ? "Reminder" : "Command"; await session.send( `Task #${task.id} created (${typeLabel}) Content: ${content} Scheduled for: ${timeStr}` ); return JSON.stringify({ id: task.id, type, time: parsedTime.time, interval: parsedTime.interval, content }); } description = `Manages scheduled tasks for notifications and command execution. Supports one-time and recurring tasks with flexible time formats. Actions: • create: Create a new scheduled task • get: List all your scheduled tasks • cancel: Delete a scheduled task by ID Task Types: • notification: Send reminder messages to users/groups • command: Execute bot commands (requires executor permission) Time Formats: • Delay: 10s, 5m, 2h, 1d (seconds/minutes/hours/days from now) • Specific time: 14:30 (today at 2:30 PM, or tomorrow if time passed) • Repeating: "10m / 1h" (start after 10 minutes, repeat every 1 hour) Recipient (for notifications): • "self" or empty: Send to task creator (default) • "group": Send to entire group • User ID: Send to specific user Executor Permission (for commands): • User ID: User-requested execution (must have permission) • Empty: Defaults to current user • Note: "0" will be automatically converted to current user Examples: • Create reminder: { "action": "create", "type": "notification", "time": "10m", "content": "Time for a break!", "recipient": "self" } • Create recurring task: { "action": "create", "type": "notification", "time": "14:30 / 1d", "content": "Daily standup", "recipient": "group" } • List tasks: { "action": "get" } • Cancel task: { "action": "cancel", "taskId": 5 } • Execute command: { "action": "create", "type": "command", "time": "1h", "content": "plugin.upgrade" }`; }; function parseTimeString(timeStr) { const now = /* @__PURE__ */ new Date(); if (timeStr.includes("/")) { const [timePart, intervalPart] = timeStr.split("/").map((s) => s.trim()); const baseTime = parseTimeString(timePart); if (!baseTime) return null; const interval2 = parseInterval(intervalPart); if (!interval2) return null; return { time: baseTime.time, interval: interval2 }; } if (/^\d{1,2}:\d{2}$/.test(timeStr)) { const [hours, minutes] = timeStr.split(":").map(Number); const time = new Date(now); time.setHours(hours, minutes, 0, 0); if (time < now) { time.setDate(time.getDate() + 1); } return { time }; } const interval = parseInterval(timeStr); if (interval) { const time = new Date(now.getTime() + interval); return { time }; } return null; } __name(parseTimeString, "parseTimeString"); function parseInterval(intervalStr) { const match = intervalStr.match(/^(\d+)(s|m|h|d)$/); if (!match) return null; const value = parseInt(match[1]); const unit = match[2]; switch (unit) { case "s": return value * 1e3; case "m": return value * 60 * 1e3; case "h": return value * 60 * 60 * 1e3; case "d": return value * 24 * 60 * 60 * 1e3; default: return null; } } __name(parseInterval, "parseInterval"); function formatTime(date, interval) { if (!interval) { return date.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }); } const hours = Math.floor(interval / (60 * 60 * 1e3)); const minutes = Math.floor(interval % (60 * 60 * 1e3) / (60 * 1e3)); const seconds = Math.floor(interval % (60 * 1e3) / 1e3); const parts = []; if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); if (seconds > 0) parts.push(`${seconds}s`); return `every ${parts.join(" ")}`; } __name(formatTime, "formatTime"); // src/plugins/draw.ts var import_tools3 = require("@langchain/core/tools"); var import_string3 = require("koishi-plugin-chatluna/utils/string"); async function apply3(ctx, config, plugin2) { if (config.draw !== true) { return; } plugin2.registerTool("draw", { selector(history) { if (config.drawSelector.length === 0) { return true; } return history.some( (message) => message.content != null && (0, import_string3.fuzzyQuery)( (0, import_string3.getMessageContent)(message.content), config.drawSelector ) ); }, createTool(params) { return new DrawTool(config.drawCommand, config.drawPrompt); } }); } __name(apply3, "apply"); var DrawTool = class extends import_tools3.Tool { constructor(drawCommand, drawPrompt) { super({}); this.drawCommand = drawCommand; this.drawPrompt = drawPrompt; } static { __name(this, "DrawTool"); } name = "draw"; /** @ignore */ async _call(input, _, config) { const session = config.configurable.session; try { const elements = await session.execute( this.drawCommand.replace("{prompt}", input), true ); await session.send(elements); return `Successfully call draw with result ${elementToString(elements)}`; } catch (e) { return `Draw image with prompt ${input} execution failed, because ${e.message}`; } } // eslint-disable-next-line max-len get description() { return this.rawDescription.replace(/\{\{prompts}}/g, this.drawPrompt); } rawDescription = `This tool generates images from text prompts. The AI cannot view the images, but users can receive them. Use the following prompt examples as a guide: {{prompts}} Based on these examples, create high-quality English prompts that match the user's requests. The tool's only input is the prompt text.`; }; // src/plugins/fs.ts var import_tools4 = require("@langchain/core/tools"); var import_promises = __toESM(require("fs/promises"), 1); var import_string4 = require("koishi-plugin-chatluna/utils/string"); var import_path = __toESM(require("path"), 1); var import_micromatch = __toESM(require("micromatch"), 1); var import_zod3 = __toESM(require("zod"), 1); async function apply4(ctx, config, plugin2) { if (config.fs !== true) { return; } const store = new FileStore( config.fsScopePath ?? "", config.fsIgnores ?? [] ); const fileReadTool = new ReadFileTool({ store }); const fileWriteTool = new WriteFileTool({ store }); const listFileTool = new ListFileTool({ store }); const grepTool = new GrepTool({ store }); const globTool = new GlobTool({ store }); const renameTool = new RenameTool({ store }); const multiRenameTool = new MultiRenameTool({ store }); const multiWriteFileTool = new MultiWriteFileTool({ store }); const updateFileTool = new UpdateFileTool({ store }); const fsSelector = /* @__PURE__ */ __name((history) => { if (config.fsSelector.length === 0) { return true; } return history.some( (message) => message.content != null && (0, import_string4.fuzzyQuery)( (0, import_string4.getMessageContent)(message.content), config.fsSelector ) ); }, "fsSelector"); plugin2.registerTool(fileReadTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => fileReadTool, "createTool") }); plugin2.registerTool(fileWriteTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => fileWriteTool, "createTool") }); plugin2.registerTool(listFileTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => listFileTool, "createTool") }); plugin2.registerTool(grepTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => grepTool, "createTool") }); plugin2.registerTool(globTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => globTool, "createTool") }); plugin2.registerTool(renameTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => renameTool, "createTool") }); plugin2.registerTool(multiRenameTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => multiRenameTool, "createTool") }); plugin2.registerTool(multiWriteFileTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => multiWriteFileTool, "createTool") }); plugin2.registerTool(updateFileTool.name, { selector: fsSelector, createTool: /* @__PURE__ */ __name(() => updateFileTool, "createTool") }); } __name(apply4, "apply"); var FileStore = class { constructor(_scope, _ignores = []) { this._scope = _scope; this._ignores = _ignores; } static { __name(this, "FileStore"); } async readFile(path2) { if (!path2.startsWith(this._scope)) { throw new Error(`path "${path2}" is not in scope "${this._scope}"`); } return JSON.stringify({ path: path2, content: (await import_promises.default.readFile(path2)).toString() }); } async writeFile(writePath, contents) { if (!writePath.startsWith(this._scope)) { throw new Error( `path "${writePath}" is not in scope "${this._scope}"` ); } const dir = import_path.default.dirname(writePath); await import_promises.default.mkdir(dir, { recursive: true }); await import_promises.default.writeFile(writePath, contents); } async listFiles(dirPath = this._scope) { if (!dirPath.startsWith(this._scope)) { throw new Error( `path "${dirPath}" is not in scope "${this._scope}"` ); } const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true }); return entries.map((entry) => { const fullPath = import_path.default.join(dirPath, entry.name); return entry.isDirectory() ? `${fullPath}/` : fullPath; }).filter((fullPath) => !this._shouldIgnore(fullPath)); } async grep(pattern, searchPath, globPattern, outputMode = "content") { const searchDir = searchPath || this._scope; if (!searchDir.startsWith(this._scope)) { throw new Error( `path "${searchDir}" is not in scope "${this._scope}"` ); } const isDirectory = /* @__PURE__ */ __name(async (path2) => { try { const stat = await import_promises.default.stat(path2); return stat.isDirectory(); } catch { return false; } }, "isDirectory"); const searchTargets = []; if (await isDirectory(searchDir)) { const files = await this._findFiles(searchDir, globPattern); searchTargets.push(...files); } else { if (!globPattern || this._matchPattern(searchDir, globPattern)) { searchTargets.push(searchDir); } } const regex = new RegExp(pattern, "gm"); let totalMatches = 0; const matchingFiles = /* @__PURE__ */ new Set(); const allResults = []; for (const file of searchTargets) { if (this._shouldIgnore(file)) { continue; } try { const stat = await import_promises.default.stat(file); if (!stat.isFile()) continue; const content = await import_promises.default.readFile(file, "utf-8"); const lines = content.split("\n"); let hasMatch = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineMatches = line.match(regex); if (lineMatches) { hasMatch = true; totalMatches += lineMatches.length; if (outputMode === "content") { allResults.push(`${file}:${i + 1}:${line}`); } } } if (hasMatch) { matchingFiles.add(file); } } catch (error) { continue; } } if (outputMode === "count") { return totalMatches; } if (outputMode === "files_with_matches") { return Array.from(matchingFiles); } return allResults; } async glob(pattern, searchPath) { const searchDir = searchPath || this._scope; if (!searchDir.startsWith(this._scope)) { throw new Error( `path "${searchDir}" is not in scope "${this._scope}"` ); } const isDirectory = /* @__PURE__ */ __name(async (path2) => { try { const stat = await import_promises.default.stat(path2); return stat.isDirectory(); } catch { return false; } }, "isDirectory"); if (!await isDirectory(searchDir)) { if (this._matchPattern(searchDir, pattern)) { return [searchDir]; } return []; } return this._findFiles(searchDir, pattern); } async editFile(filePath, oldText, newText) { if (!filePath.startsWith(this._scope)) { throw new Error( `path "${filePath}" is not in scope "${this._scope}"` ); } try { const content = await import_promises.default.readFile(filePath, "utf-8"); if (!content.includes(oldText)) { return false; } const newContent = content.replace(oldText, newText); await import_promises.default.writeFile(filePath, newContent); return true; } catch (error) { return false; } } async updateFile(filePath, oldString, newString, replaceCount) { if (!filePath.startsWith(this._scope)) { throw new Error( `path "${filePath}" is not in scope "${this._scope}"` ); } const content = await import_promises.default.readFile(filePath, "utf-8"); const lines = content.split("\n"); if (!content.includes(oldString)) { return { success: false, context: "", replacements: 0 }; } let replacements = 0; const modifiedLines = []; const newLines = [...lines]; for (let i = 0; i < newLines.length; i++) { if (newLines[i].includes(oldString)) { if (replaceCount === void 0 || replacements < replaceCount) { newLines[i] = newLines[i].replaceAll(oldString, newString); modifiedLines.push(i); replacements++; } } } await import_promises.default.writeFile(filePath, newLines.join("\n")); const contextLines = []; const contextSet = /* @__PURE__ */ new Set(); for (const lineNum of modifiedLines) { const start = Math.max(0, lineNum - 10); const end = Math.min(newLines.length - 1, lineNum + 10); for (let i = start; i <= end; i++) { contextSet.add(i); } } const sortedContext = Array.from(contextSet).sort((a, b) => a - b); for (const lineNum of sortedContext) { const marker = modifiedLines.includes(lineNum) ? ">" : " "; contextLines.push(`${marker} ${lineNum + 1}: ${newLines[lineNum]}`); } return { success: true, context: contextLines.join("\n"), replacements }; } async rename(oldPath, newPath) { if (!oldPath.startsWith(this._scope)) { throw new Error( `path "${oldPath}" is not in scope "${this._scope}"` ); } if (!newPath.startsWith(this._scope)) { throw new Error( `path "${newPath}" is not in scope "${this._scope}"` ); } const newDir = import_path.default.dirname(newPath); if (await import_promises.default.stat(oldPath).then((stat) => stat.isDirectory())) { await import_promises.default.mkdir(newDir, { recursive: true }); await import_promises.default.rename(oldPath, newPath); } else { await import_promises.default.mkdir(newDir, { recursive: true }); await import_promises.default.rename(oldPath, newPath); } } async _findFiles(dirPath, pattern, includeDirectories = false) { try { const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true }); const subdirectoryPromises = []; const results = []; for (const entry of entries) { const fullPath = import_path.default.join(dirPath, entry.name); if (this._shouldIgnore(fullPath)) { continue; } if (entry.isDirectory()) { if (includeDirectories && (!pattern || this._matchPattern(fullPath, pattern))) { results.push(fullPath); } subdirectoryPromises.push( this._findFiles(fullPath, pattern, includeDirectories) ); } else if (entry.isFile()) { if (!pattern || this._matchPattern(fullPath, pattern)) { results.push(fullPath); } } else if (entry.isSymbolicLink()) { try { const stat = await import_promises.default.stat(fullPath); if (stat.isFile()) { if (!pattern || this._matchPattern(fullPath, pattern)) { results.push(fullPath); } } else if (stat.isDirectory() && includeDirectories) { if (!pattern || this._matchPattern(fullPath, pattern)) { results.push(fullPath); } subdirectoryPromises.push( this._findFiles( fullPath, pattern, includeDirectories ) ); } } catch { } } } const filesFromSubdirectories = await Promise.all(subdirectoryPromises); return results.concat(...filesFromSubdirectories); } catch (error) { return []; } } _matchPattern(filePath, pattern) { const relativePath = import_path.default.relative(this._scope, filePath); const fileName = import_path.default.basename(filePath); return import_micromatch.default.isMatch(relativePath, pattern, { dot: true }) || import_micromatch.default.isMatch(filePath, pattern, { dot: true }) || import_micromatch.default.isMatch(fileName, pattern, { dot: true }) || import_micromatch.default.isMatch(relativePath.replace(/\\/g, "/"), pattern, { dot: true }); } _shouldIgnore(filePath) { if (this._ignores.length === 0) { return false; } const relativePath = import_path.default.relative(this._scope, filePath); const normalizedPath = relativePath.replace(/\\/g, "/"); return import_micromatch.default.isMatch(normalizedPath, this._ignores, { dot: true }); } }; var ReadFileTool = class extends import_tools4.Tool { static { __name(this, "ReadFileTool"); } name = "file_read"; description = "Read file content from disk. Provide the complete file path to read its contents."; store; constructor({ store }) { super(); this.store = store; } async _call(filePath) { try { return await this.store.readFile(filePath); } catch (e) { return "File read failed: " + e.message; } } }; var WriteFileTool = class extends import_tools4.StructuredTool { static { __name(this, "WriteFileTool"); } name = "file_write"; description = "Write text content to a file on disk. Creates the file if it doesn't exist, overwrites if it does."; schema = import_zod3.default.object({ filePath: import_zod3.default.string().describe("The path to write the file."), text: import_zod3.default.string().describe("The content to write to the file.") }); store; constructor({ store, ...rest }) { super(rest); this.store = store; } async _call(input) { const { filePath, text } = input; try { await this.store.writeFile(filePath, text); return "File written to successfully."; } catch (e) { return "File write failed: " + e.message; } } }; var ListFileTool = class extends import_tools4.StructuredTool { static { __name(this, "ListFileTool"); } name = "file_list"; description = "List files and directories. Use recursive option to search subdirectories."; schema = import_zod3.default.object({ dirPath: import_zod3.default.string().describe( "The directory path to list files from. Defaults to the root scope." ), recursive: import_zod3.default.boolean().optional().default(false).describe("Whether to list files recursively.") }); store; constructor({ store, ...rest }) { super(rest); this.st