koishi-plugin-chatluna-plugin-common
Version:
plugin service for agent mode of chatluna
1,407 lines (1,392 loc) • 114 kB
JavaScript
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  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