From 75ec3c809078b0b07164051c935e06f225f23046 Mon Sep 17 00:00:00 2001 From: wmsyw Date: Thu, 4 Dec 2025 15:11:31 +0800 Subject: [PATCH 1/6] Add new plugins with URLs and descriptions --- plugins.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins.json b/plugins.json index 94519656..96a17b6d 100644 --- a/plugins.json +++ b/plugins.json @@ -386,5 +386,13 @@ "rev": { "url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/rev/rev.ts?raw=true", "desc": "反转你的消息" + }, + "kkp": { + "url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/rev/kkp.ts?raw=true", + "desc": "获取NSFW视频" + }, + "botmzt": { + "url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/rev/botmzt.ts?raw=true", + "desc": "随机获取写真图片" } } From a94163ab6c9ea74fcc7ffd6d5e6b0ea66de57dc1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 4 Dec 2025 07:11:45 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=8F=92=E4=BB=B6=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从 plugins.json 同步插件信息 - 按字母顺序排序 - 自动去重处理 插件数量: 99 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1dd7a0e8..f17fd861 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ tpm i <插件名> - `banana` - Nano-Banana 图像编辑 - `bin` - 卡头检测 - `bizhi` - 发送一张壁纸 +- `botmzt` - 随机获取写真图片 - `bs` - 保送 - `bulk_delete` - 批量删除消息 - `calc` - 计算器 @@ -57,6 +58,7 @@ tpm i <插件名> - `keep_online` - 保活自动重启(测试版) 请查看说明操作 - `keyword` - 关键词自动回复 - `kitt` - 高级触发器: 匹配 -> 执行, 高度自定义, 逻辑自由 +- `kkp` - 获取NSFW视频 - `komari` - Komari 服务器监控 - `listusernames` - 列出属于自己的公开群组/频道 - `lottery` - 抽奖 From 900f5e015fa0135df7e3cc81be38dabe694ad89b Mon Sep 17 00:00:00 2001 From: wmsyw Date: Thu, 4 Dec 2025 15:41:15 +0800 Subject: [PATCH 3/6] Add files via upload --- openlist.ts | 864 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 864 insertions(+) create mode 100644 openlist.ts diff --git a/openlist.ts b/openlist.ts new file mode 100644 index 00000000..847a9659 --- /dev/null +++ b/openlist.ts @@ -0,0 +1,864 @@ +import { Plugin } from "@utils/pluginBase"; +import { getPrefixes } from "@utils/pluginManager"; +import { Api } from "telegram"; +import { getGlobalClient } from "@utils/globalClient"; +import * as fs from "fs/promises"; +import * as path from "path"; +import axios from "axios"; +import { JSONFilePreset } from "lowdb/node"; +import { createDirectoryInAssets } from "@utils/pathHelpers"; + +import { exec } from "child_process"; +import { promisify } from "util"; + +const prefixes = getPrefixes(); +const mainPrefix = prefixes[0]; +const pluginName = "openlist"; +const commandName = `${mainPrefix}${pluginName}`; + +const execAsync = promisify(exec); +const GH_BASE_DOWNLOAD = "https://github.com/OpenListTeam/OpenList/releases/latest/download"; + +const helpText = `⚙️ OpenList 管理插件 + +📝 功能描述: +• 📦 安装/管理:一键安装、更新、卸载、修改端口 +• 💾 配置管理:备份和恢复 OpenList 配置 +• 🔑 账户管理:修改用户名和密码 +• 📁 文件保存:快速保存文件到指定目录 + +🔧 使用方法: +• ${commandName} install [目录] - 安装 +• ${commandName} update - 更新 +• ${commandName} uninstall - 卸载 +• ${commandName} status - 查看状态 +• ${commandName} setport [端口] - 修改端口 + +• ${commandName} backup - 备份配置 +• ${commandName} restore [备份名] - 恢复配置 + +• ${commandName} admin setuser [用户名] +• ${commandName} admin setpass [密码] +• ${commandName} admin random +• ${commandName} login [用户] [密码] - 手动配置账号信息 +• ${commandName} setdefault [路径] - 设置默认保存路径 (不填则恢复默认) + +• ${commandName} save [路径] - (回复文件) 保存到 Openlist 目录 (指定路径则上传到挂载盘) + +💡 示例: +• ${commandName} install /data/openlist +• ${commandName} setport 5255 +`; + +class OpenListPlugin extends Plugin { + description: string = `\nOpenList 管理\n\n${helpText}`; + cmdHandlers: Record Promise> = { + openlist: async (msg: Api.Message) => { + await this.handleCommand(msg); + }, + op: async (msg: Api.Message) => { + await this.handleCommand(msg); + }, + }; + + private async handleCommand(msg: Api.Message) { + const args = (msg.message || "").trim().split(/\s+/); + const sub = args[1] || ""; + + switch (sub) { + case "install": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await msg.edit({ text: "正在准备安装..." }); + await this.handleInstall(msg, args[2]); + break; + case "update": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await msg.edit({ text: "正在准备更新..." }); + await this.handleUpdate(msg); + break; + case "uninstall": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await msg.edit({ text: "正在准备卸载..." }); + await this.handleUninstall(msg); + break; + case "status": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await this.handleStatus(msg); + break; + case "backup": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await msg.edit({ text: "正在准备备份..." }); + await this.handleBackup(msg); + break; + case "restore": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await msg.edit({ text: "正在准备恢复..." }); + await this.handleRestore(msg, args[2]); + break; + case "admin": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await this.handleAdmin(msg, args.slice(2)); + break; + case "setport": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await this.handleSetPort(msg, args[2]); + break; + case "save": + await this.handleSave(msg, args[2]); + break; + case "login": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await this.handleLogin(msg, args[2], args[3]); + break; + case "setdefault": + await this.handleSetDefault(msg, args[2]); + break; + default: + await msg.edit({ text: helpText, parseMode: "html" }); + } + } + + private async handleInstall(msg: Api.Message, dirArg?: string) { + try { + if (process.platform !== "linux") { + await msg.edit({ text: "仅支持 Linux(systemd)环境" }); + return; + } + + const hasSystemd = await this.hasCmd("systemctl"); + const hasCurl = await this.hasCmd("curl"); + const hasTar = await this.hasCmd("tar"); + if (!hasSystemd || !hasCurl || !hasTar) { + const missing = [ + !hasSystemd ? "systemctl" : "", + !hasCurl ? "curl" : "", + !hasTar ? "tar" : "", + ] + .filter(Boolean) + .join(", "); + await msg.edit({ text: `缺少依赖:${missing}` }); + return; + } + + const arch = this.mapArch(process.arch); + if (!arch) { + await msg.edit({ text: `暂不支持当前架构:${process.arch}` }); + return; + } + + let installBase = dirArg && dirArg.trim() ? dirArg.trim() : "/opt/openlist"; + const installPath = this.normalizeInstallPath(installBase); + + if (await this.fileExists(`${installPath}/openlist`)) { + await msg.edit({ text: `检测到已安装于:${installPath}\n请使用:${commandName} update` }); + return; + } + + await msg.edit({ text: `开始安装到:${installPath}` }); + await execAsync(`mkdir -p "${installPath}"`); + + const tarPath = "/tmp/openlist.tar.gz"; + const url = `${GH_BASE_DOWNLOAD}/openlist-linux-musl-${arch}.tar.gz`; + await execAsync( + `curl -L --connect-timeout 10 --retry 3 --retry-delay 3 "${url}" -o "${tarPath}"` + ); + await execAsync(`tar zxf "${tarPath}" -C "${installPath}/"`); + await execAsync(`chmod +x "${installPath}/openlist"`); + + const serviceContent = [ + "[Unit]", + "Description=OpenList service", + "After=network.target", + "", + "[Service]", + "Type=simple", + `WorkingDirectory=${installPath}`, + `ExecStart=${installPath}/openlist server`, + "KillMode=process", + "", + "[Install]", + "WantedBy=multi-user.target", + "", + ].join("\n"); + + await execAsync( + `bash -lc 'cat > /etc/systemd/system/openlist.service <<\"EOF\"\n${serviceContent}\nEOF'` + ); + await execAsync(`systemctl daemon-reload`); + await execAsync(`systemctl enable openlist`); + await execAsync(`systemctl restart openlist`); + + const { stdout: randOut } = await execAsync( + `bash -lc 'cd "${installPath}" && ./openlist admin random 2>&1 || true'` + ); + const userMatch = randOut.match(/username:\s*(\S+)/i); + const passMatch = randOut.match(/password:\s*(\S+)/i); + const username = userMatch ? userMatch[1] : ""; + const password = passMatch ? passMatch[1] : ""; + + // 自动保存初始凭证 + if (username && password) { + await this.updateStoredCredentials(username, password); + } + + const { stdout: verOut } = await execAsync( + `bash -lc '"${installPath}/openlist" version 2>&1 || true'` + ); + const verMatch = verOut.match(/Version:\s*([^\s]+)/); + const version = verMatch ? verMatch[1] : ""; + + let ip = ""; + try { + const { stdout } = await execAsync( + `bash -lc 'hostname -I 2>/dev/null | awk "{print $1}"'` + ); + ip = (stdout || "").trim(); + } catch {} + + const lines: string[] = []; + lines.push("安装完成"); + if (version) lines.push(`版本: ${version}`); + lines.push(`目录: ${installPath}`); + lines.push(`访问: http://${ip || "<服务器IP>"}:5244/`); + if (username && password) { + lines.push(`账号: ${username}`); + lines.push(`密码: ${password}`); + } + await msg.edit({ text: lines.join("\n") }); + } catch (error: any) { + await msg.edit({ text: `安装失败: ${error?.message || error}` }); + } + } + + private async handleUpdate(msg: Api.Message) { + try { + if (process.platform !== "linux") { + await msg.edit({ text: "仅支持 Linux(systemd)环境" }); + return; + } + + const hasSystemd = await this.hasCmd("systemctl"); + const hasCurl = await this.hasCmd("curl"); + const hasTar = await this.hasCmd("tar"); + if (!hasSystemd || !hasCurl || !hasTar) { + const missing = [ + !hasSystemd ? "systemctl" : "", + !hasCurl ? "curl" : "", + !hasTar ? "tar" : "", + ] + .filter(Boolean) + .join(", "); + await msg.edit({ text: `缺少依赖:${missing}` }); + return; + } + + const arch = this.mapArch(process.arch); + if (!arch) { + await msg.edit({ text: `暂不支持当前架构:${process.arch}` }); + return; + } + + const installPath = await this.detectInstalledPath(); + if (!(await this.fileExists(`${installPath}/openlist`))) { + await msg.edit({ text: `未检测到已安装版本。可使用:${commandName} install` }); + return; + } + + await msg.edit({ text: "开始更新..." }); + const tarPath = "/tmp/openlist.tar.gz"; + const url = `${GH_BASE_DOWNLOAD}/openlist-linux-musl-${arch}.tar.gz`; + await execAsync( + `curl -L --connect-timeout 10 --retry 3 --retry-delay 3 "${url}" -o "${tarPath}"` + ); + + await execAsync(`systemctl stop openlist || true`); + await execAsync(`cp "${installPath}/openlist" /tmp/openlist.bak || true`); + await execAsync(`tar zxf "${tarPath}" -C "${installPath}/"`); + await execAsync(`chmod +x "${installPath}/openlist"`); + await execAsync(`systemctl restart openlist`); + + const { stdout: verOut } = await execAsync( + `bash -lc '"${installPath}/openlist" version 2>&1 || true'` + ); + const verMatch = verOut.match(/Version:\s*([^\s]+)/); + const version = verMatch ? verMatch[1] : ""; + await msg.edit({ text: `更新完成${version ? `,版本: ${version}` : ""}` }); + } catch (error: any) { + await msg.edit({ text: `更新失败: ${error?.message || error}` }); + } + } + + private async handleUninstall(msg: Api.Message) { + try { + if (process.platform !== "linux") { + await msg.edit({ text: "仅支持 Linux(systemd)环境" }); + return; + } + + const installPath = await this.detectInstalledPath(); + const existed = await this.fileExists(`${installPath}/openlist`); + await execAsync(`systemctl stop openlist || true`); + await execAsync(`systemctl disable openlist || true`); + await execAsync(`rm -f /etc/systemd/system/openlist.service || true`); + await execAsync(`systemctl daemon-reload || true`); + if (existed) { + await execAsync(`rm -rf "${installPath}"`); + } + await msg.edit({ text: "已卸载" }); + } catch (error: any) { + await msg.edit({ text: `卸载失败: ${error?.message || error}` }); + } + } + + private async handleBackup(msg: Api.Message) { + try { + const installPath = await this.detectInstalledPath(); + if (!(await this.dirExists(`${installPath}/data`))) { + await msg.edit({ text: `未找到配置目录:${installPath}/data` }); + return; + } + + const backupBaseDir = "/opt/openlist_backups"; + const { stdout: dateOut } = await execAsync( + `bash -lc 'date +%Y%m%d_%H%M%S'` + ); + const backupDir = `${backupBaseDir}/backup_${(dateOut || "").trim()}`; + await execAsync(`mkdir -p "${backupDir}"`); + await execAsync(`cp -r "${installPath}/data" "${backupDir}/"`); + + await msg.edit({ text: `备份成功\n目录: ${backupDir}` }); + } catch (error: any) { + await msg.edit({ text: `备份失败: ${error?.message || error}` }); + } + } + + private async handleRestore(msg: Api.Message, backupName?: string) { + try { + const installPath = await this.detectInstalledPath(); + const backupBaseDir = "/opt/openlist_backups"; + let targetBackupDir = ""; + + if (backupName) { + targetBackupDir = `${backupBaseDir}/${backupName}`; + } else { + const { stdout: latestOut } = await execAsync( + `bash -lc 'ls -t "${backupBaseDir}" 2>/dev/null | head -n1'` + ); + const latest = (latestOut || "").trim(); + if (!latest) { + await msg.edit({ text: `未找到任何备份于:${backupBaseDir}` }); + return; + } + targetBackupDir = `${backupBaseDir}/${latest}`; + } + + if (!(await this.dirExists(`${targetBackupDir}/data`))) { + await msg.edit({ text: `无效的备份目录:${targetBackupDir}` }); + return; + } + + await msg.edit({ text: `将从 ${targetBackupDir} 恢复...` }); + await execAsync(`systemctl stop openlist || true`); + await execAsync(`cp -r "${targetBackupDir}/data" "${installPath}/"`); + await execAsync(`systemctl start openlist`); + + await msg.edit({ text: "恢复成功" }); + } catch (error: any) { + await msg.edit({ text: `恢复失败: ${error?.message || error}` }); + } + } + + private async handleAdmin(msg: Api.Message, adminArgs: string[]) { + try { + const installPath = await this.detectInstalledPath(); + if (!(await this.fileExists(`${installPath}/openlist`))) { + await msg.edit({ text: "未检测到 OpenList 安装" }); + return; + } + + const sub = adminArgs[0] || ""; + const arg = adminArgs[1] || ""; + let cmd = ""; + + // 记录需要更新的凭证 + let newUser = ""; + let newPass = ""; + + switch (sub) { + case "setuser": + if (!arg) { + await msg.edit({ text: "用法: admin setuser [新用户名]" }); + return; + } + cmd = `admin setuser "${arg}"`; + newUser = arg; + break; + case "setpass": + if (!arg) { + await msg.edit({ text: "用法: admin setpass [新密码]" }); + return; + } + cmd = `admin set "${arg}"`; // 原脚本中使用 'set' 而非 'setpass' + newPass = arg; + break; + case "random": + cmd = "admin random"; + break; + default: + await msg.edit({ text: helpText, parseMode: "html" }); + return; + } + + await msg.edit({ text: `正在执行: ${cmd}` }); + const { stdout } = await execAsync( + `bash -lc 'cd "${installPath}" && ./openlist ${cmd} 2>&1'` + ); + + // 如果是 random,解析输出 + if (sub === "random") { + const userMatch = stdout.match(/username:\s*(\S+)/i); + const passMatch = stdout.match(/password:\s*(\S+)/i); + if (userMatch) newUser = userMatch[1]; + if (passMatch) newPass = passMatch[1]; + } + + // 更新本地凭证 + if (newUser || newPass) { + await this.updateStoredCredentials(newUser, newPass); + await msg.edit({ text: `执行结果:\n\n
${(stdout || "").trim()}
\n\n✅ 凭证已同步更新`, parseMode: "html" }); + } else { + await msg.edit({ text: `执行结果:\n\n
${(stdout || "").trim()}
`, parseMode: "html" }); + } + } catch (error: any) { + await msg.edit({ text: `管理命令失败: ${error?.message || error}` }); + } + } + + private async handleLogin(msg: Api.Message, user?: string, pass?: string) { + if (!user || !pass) { + await msg.edit({ text: `用法: ${commandName} login [用户名] [密码]` }); + return; + } + await this.updateStoredCredentials(user, pass); + await msg.edit({ text: "✅ 账号信息已保存,可以尝试上传文件了。" }); + } + + private async handleSetDefault(msg: Api.Message, path?: string) { + const db = await this.getDb(); + if (!path) { + // 清空默认路径,恢复为宿主机路径 + await db.update((data) => { + data.defaultPath = ""; + }); + await msg.edit({ text: "✅ 默认上传路径已清空,将恢复为宿主机 /root/Openlist 路径。" }); + return; + } + await db.update((data) => { + data.defaultPath = path; + }); + await msg.edit({ text: `✅ 默认上传路径已设置为: ${path}\n\n现在使用 ${commandName} save 时若不指定路径,将默认上传到此位置。` }); + } + + private async updateStoredCredentials(user?: string, pass?: string) { + const db = await this.getDb(); + await db.update((data) => { + if (user) data.username = user; + if (pass) data.password = pass; + }); + } + + private async getDb() { + const dbPath = path.join(createDirectoryInAssets("openlist"), "credentials.json"); + return await JSONFilePreset(dbPath, { username: "", password: "", defaultPath: "" }); + } + + + private async handleSetPort(msg: Api.Message, port?: string) { + try { + if (!port || !/^\d+$/.test(port)) { + await msg.edit({ text: `用法: ${commandName} setport [端口号]` }); + return; + } + + const installPath = await this.detectInstalledPath(); + const configPath = `${installPath}/data/config.json`; + if (!(await this.fileExists(configPath))) { + await msg.edit({ text: "未找到配置文件,请先确保 OpenList 已成功运行一次。" }); + return; + } + + await msg.edit({ text: `正在修改端口为 ${port}...` }); + await execAsync(`systemctl stop openlist || true`); + // 使用 sed 安全地替换端口号 + await execAsync( + `sed -i 's/"port": *[0-9]*/"port": ${port}/g' "${configPath}"` + ); + await execAsync(`systemctl start openlist`); + + await msg.edit({ text: `端口已修改为 ${port},服务已重启。` }); + } catch (error: any) { + await msg.edit({ text: `端口修改失败: ${error?.message || error}` }); + } + } + + private async handleSave(msg: Api.Message, targetPath?: string) { + try { + const replyToMsg = await msg.getReplyMessage(); + if (!replyToMsg || !replyToMsg.media) { + await msg.edit({ text: "请回复一个文件、图片或视频来保存。" }); + return; + } + + // 确定最终保存路径 + let finalPath = targetPath; + if (!finalPath) { + // 尝试读取默认路径 + const db = await this.getDb(); + finalPath = db.data.defaultPath || ""; + } + + const media = replyToMsg.media; + let fileName = ""; + + if (media instanceof Api.MessageMediaPhoto) { + fileName = `photo_${Date.now()}.jpg`; + } else if (media instanceof Api.MessageMediaDocument && media.document instanceof Api.Document) { + const doc = media.document; + const fileNameAttr = doc.attributes.find( + (attr): attr is Api.DocumentAttributeFilename => + attr instanceof Api.DocumentAttributeFilename + ); + + if (fileNameAttr) { + fileName = fileNameAttr.fileName; + } else { + // 根据 mimeType 推断后缀 + let ext = ""; + switch (doc.mimeType) { + case "video/mp4": ext = ".mp4"; break; + case "video/x-matroska": ext = ".mkv"; break; + case "video/quicktime": ext = ".mov"; break; + case "audio/mpeg": ext = ".mp3"; break; + case "audio/ogg": ext = ".ogg"; break; + case "audio/x-wav": ext = ".wav"; break; + case "image/jpeg": ext = ".jpg"; break; + case "image/png": ext = ".png"; break; + case "image/webp": ext = ".webp"; break; + case "image/gif": ext = ".gif"; break; + case "application/pdf": ext = ".pdf"; break; + case "application/zip": ext = ".zip"; break; + default: ext = ""; + } + fileName = `file_${Date.now()}${ext}`; + } + } else { + // 其他媒体类型,暂时命名为 media_xxx + fileName = `media_${Date.now()}`; + } + + await msg.edit({ text: `正在下载: ${fileName}` }); + + const client = await getGlobalClient(); + const buffer = await client.downloadMedia(replyToMsg.media); + + if (!buffer || !(buffer instanceof Buffer)) { + await msg.edit({ text: "文件下载失败或格式不支持。" }); + return; + } + + if (finalPath) { + await this.uploadToOpenList(msg, buffer, fileName, finalPath); + } else { + const saveDir = "/root/Openlist"; + await fs.mkdir(saveDir, { recursive: true }); + const savePath = path.join(saveDir, fileName); + await fs.writeFile(savePath, buffer); + await msg.edit({ text: `文件已保存到: ${savePath}` }); + } + } catch (error: any) { + await msg.edit({ text: `文件保存失败: ${error?.message || error}` }); + } + } + + private async uploadToOpenList(msg: Api.Message, buffer: Buffer, fileName: string, targetDir: string) { + try { + await msg.edit({ text: "正在登录 OpenList API..." }); + const credentials = await this.getOpenListCredentials(); + if (!credentials) { + throw new Error("未找到 OpenList 凭证。\n请使用以下命令手动配置:\n`op login [用户名] [密码]`"); + } + + const token = await this.getOpenListToken(credentials.username, credentials.password); + if (!token) { + throw new Error("登录 OpenList 失败"); + } + + // 处理路径,确保是 API 友好的格式 + let fullPath = path.join(targetDir, fileName).replace(/\\/g, "/"); + if (!fullPath.startsWith("/")) fullPath = "/" + fullPath; + // 移除多余的斜杠 + fullPath = fullPath.replace(/\/+/g, "/"); + + await msg.edit({ text: `正在上传到: ${fullPath}` }); + + const apiUrl = "http://127.0.0.1:5244/api/fs/put"; + + // 注意:Header 中的中文路径需要编码 + await axios.put(apiUrl, buffer, { + headers: { + "Authorization": token, + "File-Path": encodeURIComponent(fullPath), + "path": encodeURIComponent(fullPath), + "Content-Type": "application/octet-stream", + "As-Task": "false" + }, + maxBodyLength: Infinity, + maxContentLength: Infinity + }); + + await msg.edit({ text: `✅ 文件已上传到 OpenList: ${fullPath}` }); + + } catch (error: any) { + console.error("OpenList Upload Error:", error); + const errMsg = error?.response?.data?.message || error.message || "未知错误"; + throw new Error(`上传失败: ${errMsg}`); + } + } + + private async getOpenListCredentials() { + // 1. Get DB credentials + let dbUser = ""; + let dbPass = ""; + try { + const db = await this.getDb(); + dbUser = db.data.username; + dbPass = db.data.password; + } catch (e) {} + + // 2. Get Config credentials + let configUser = ""; + let configPass = ""; + const installPath = await this.detectInstalledPath(); + const configPath = `${installPath}/data/config.json`; + + if (await this.fileExists(configPath)) { + try { + let configContent = ""; + try { + configContent = await fs.readFile(configPath, "utf-8"); + } catch { + const { stdout } = await execAsync(`cat "${configPath}" 2>/dev/null`); + configContent = stdout; + } + + if (configContent) { + const config = JSON.parse(configContent); + if (config.users && config.users.length > 0) { + configUser = config.users[0].username; + configPass = config.users[0].password; + } + } + } catch (e) { + console.error("Error reading config:", e); + } + } + + // 3. Merge (Prefer DB) + // If DB is missing username but has password (e.g. after setpass), use config username + const finalUser = dbUser || configUser; + // If DB is missing password but has username (e.g. after setuser), use config password (if available/valid) + const finalPass = dbPass || configPass; + + if (finalUser && finalPass) { + // Auto-sync if we had to combine sources + if (!dbUser || !dbPass) { + await this.updateStoredCredentials(finalUser, finalPass); + } + return { username: finalUser, password: finalPass }; + } + + return null; + } + + private async getOpenListToken(username: string, password: string): Promise { + try { + const response = await axios.post("http://127.0.0.1:5244/api/auth/login", { + username, + password + }); + if (response.data && response.data.code === 200) { + return response.data.data.token; + } + throw new Error(response.data?.message || "Login failed"); + } catch (error) { + throw error; + } + } + + private async handleStatus(msg: Api.Message) { + try { + if (process.platform !== "linux") { + await msg.edit({ text: "仅支持 Linux(systemd)环境" }); + return; + } + const installPath = await this.detectInstalledPath(); + const installed = await this.fileExists(`${installPath}/openlist`); + const { stdout: activeOut } = await execAsync( + `bash -lc 'systemctl is-active openlist 2>/dev/null || true'` + ); + const status = (activeOut || "").trim() || "unknown"; + + const { stdout: portOut } = await execAsync( + `bash -lc '(ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -q ":5244" && echo listen || echo closed'` + ); + const port = (portOut || "").trim(); + + let version = ""; + if (installed) { + try { + const { stdout: verOut } = await execAsync( + `bash -lc '"${installPath}/openlist" version 2>&1 | grep -E "^Version:" || true'` + ); + const m = verOut.match(/Version:\s*([^\s]+)/); + version = m ? m[1] : ""; + } catch {} + } + + let publicIp = ""; + try { + const { stdout: ipOut } = await execAsync( + `bash -lc 'curl -s4 --connect-timeout 5 ip.sb || curl -s4 --connect-timeout 5 ifconfig.me'` + ); + publicIp = (ipOut || "").trim(); + } catch {} + + const lines: string[] = []; + lines.push(`状态: ${installed ? `已安装` : "未安装"}`); + lines.push(`服务: ${status}`); + if (version) lines.push(`版本: ${version}`); + lines.push(`端口: ${port}`); + if (publicIp && port === "listen") { + lines.push(`链接: http://${publicIp}:5244/`); + } + + // 显示用户账户信息 + const configPath = `${installPath}/data/config.json`; + if (await this.fileExists(configPath)) { + try { + const configContent = await fs.readFile(configPath, "utf-8"); + const config = JSON.parse(configContent); + if (config.users && config.users.length > 0) { + lines.push("\n账户信息:"); + config.users.forEach((user: any, index: number) => { + lines.push(`${index + 1}. 用户: ${user.username} | 密码: ${user.password}`); + }); + } + } catch (e) { + lines.push("\n无法解析账户信息。"); + } + } + + await msg.edit({ text: lines.join("\n"), parseMode: "html" }); + } catch (error: any) { + await msg.edit({ text: `状态获取失败: ${error?.message || error}` }); + } + } + + private mapArch(nodeArch: string): string | null { + const map: Record = { + x64: "amd64", + arm64: "arm64", + s390x: "s390x", + loong64: "loong64", + }; + return map[nodeArch] || null; + } + + private normalizeInstallPath(input: string): string { + let p = input.replace(/\/+$/, ""); + if (!p.endsWith("/openlist")) p = `${p}/openlist`; + return p; + } + + private async detectInstalledPath(): Promise { + try { + const { stdout } = await execAsync( + `bash -lc 'grep -E "^WorkingDirectory=" /etc/systemd/system/openlist.service 2>/dev/null | head -n1 | cut -d= -f2'` + ); + const p = (stdout || "").trim(); + if (p) return p; + } catch {} + return "/opt/openlist"; + } + + private async hasCmd(cmd: string): Promise { + try { + await execAsync(`bash -lc 'command -v ${cmd} >/dev/null 2>&1'`); + return true; + } catch { + return false; + } + } + + private async dirExists(path: string): Promise { + try { + const { stdout } = await execAsync( + `bash -lc '[ -d "${path}" ] && echo 1 || echo 0'` + ); + return stdout.trim() === "1"; + } catch { + return false; + } + } + + private async isSavedMessages(msg: Api.Message): Promise { + const client = await getGlobalClient(); + const me = await client?.getMe(); + if (!me || !msg.peerId) return false; + + return ( + "userId" in msg.peerId && + msg.peerId.userId?.toString() === me.id.toString() + ); + } + + private async fileExists(path: string): Promise { + try { + const { stdout } = await execAsync( + `bash -lc '[ -f "${path}" ] && echo 1 || echo 0'` + ); + return stdout.trim() === "1"; + } catch { + return false; + } + } +} + +export default new OpenListPlugin(); From adb4b7fc4dad1c63590272eac2d2375404ccbfd4 Mon Sep 17 00:00:00 2001 From: wmsyw Date: Thu, 4 Dec 2025 15:41:35 +0800 Subject: [PATCH 4/6] Delete openlist.ts --- openlist.ts | 864 ---------------------------------------------------- 1 file changed, 864 deletions(-) delete mode 100644 openlist.ts diff --git a/openlist.ts b/openlist.ts deleted file mode 100644 index 847a9659..00000000 --- a/openlist.ts +++ /dev/null @@ -1,864 +0,0 @@ -import { Plugin } from "@utils/pluginBase"; -import { getPrefixes } from "@utils/pluginManager"; -import { Api } from "telegram"; -import { getGlobalClient } from "@utils/globalClient"; -import * as fs from "fs/promises"; -import * as path from "path"; -import axios from "axios"; -import { JSONFilePreset } from "lowdb/node"; -import { createDirectoryInAssets } from "@utils/pathHelpers"; - -import { exec } from "child_process"; -import { promisify } from "util"; - -const prefixes = getPrefixes(); -const mainPrefix = prefixes[0]; -const pluginName = "openlist"; -const commandName = `${mainPrefix}${pluginName}`; - -const execAsync = promisify(exec); -const GH_BASE_DOWNLOAD = "https://github.com/OpenListTeam/OpenList/releases/latest/download"; - -const helpText = `⚙️ OpenList 管理插件 - -📝 功能描述: -• 📦 安装/管理:一键安装、更新、卸载、修改端口 -• 💾 配置管理:备份和恢复 OpenList 配置 -• 🔑 账户管理:修改用户名和密码 -• 📁 文件保存:快速保存文件到指定目录 - -🔧 使用方法: -• ${commandName} install [目录] - 安装 -• ${commandName} update - 更新 -• ${commandName} uninstall - 卸载 -• ${commandName} status - 查看状态 -• ${commandName} setport [端口] - 修改端口 - -• ${commandName} backup - 备份配置 -• ${commandName} restore [备份名] - 恢复配置 - -• ${commandName} admin setuser [用户名] -• ${commandName} admin setpass [密码] -• ${commandName} admin random -• ${commandName} login [用户] [密码] - 手动配置账号信息 -• ${commandName} setdefault [路径] - 设置默认保存路径 (不填则恢复默认) - -• ${commandName} save [路径] - (回复文件) 保存到 Openlist 目录 (指定路径则上传到挂载盘) - -💡 示例: -• ${commandName} install /data/openlist -• ${commandName} setport 5255 -`; - -class OpenListPlugin extends Plugin { - description: string = `\nOpenList 管理\n\n${helpText}`; - cmdHandlers: Record Promise> = { - openlist: async (msg: Api.Message) => { - await this.handleCommand(msg); - }, - op: async (msg: Api.Message) => { - await this.handleCommand(msg); - }, - }; - - private async handleCommand(msg: Api.Message) { - const args = (msg.message || "").trim().split(/\s+/); - const sub = args[1] || ""; - - switch (sub) { - case "install": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await msg.edit({ text: "正在准备安装..." }); - await this.handleInstall(msg, args[2]); - break; - case "update": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await msg.edit({ text: "正在准备更新..." }); - await this.handleUpdate(msg); - break; - case "uninstall": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await msg.edit({ text: "正在准备卸载..." }); - await this.handleUninstall(msg); - break; - case "status": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await this.handleStatus(msg); - break; - case "backup": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await msg.edit({ text: "正在准备备份..." }); - await this.handleBackup(msg); - break; - case "restore": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await msg.edit({ text: "正在准备恢复..." }); - await this.handleRestore(msg, args[2]); - break; - case "admin": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await this.handleAdmin(msg, args.slice(2)); - break; - case "setport": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await this.handleSetPort(msg, args[2]); - break; - case "save": - await this.handleSave(msg, args[2]); - break; - case "login": - if (!(await this.isSavedMessages(msg))) { - await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); - return; - } - await this.handleLogin(msg, args[2], args[3]); - break; - case "setdefault": - await this.handleSetDefault(msg, args[2]); - break; - default: - await msg.edit({ text: helpText, parseMode: "html" }); - } - } - - private async handleInstall(msg: Api.Message, dirArg?: string) { - try { - if (process.platform !== "linux") { - await msg.edit({ text: "仅支持 Linux(systemd)环境" }); - return; - } - - const hasSystemd = await this.hasCmd("systemctl"); - const hasCurl = await this.hasCmd("curl"); - const hasTar = await this.hasCmd("tar"); - if (!hasSystemd || !hasCurl || !hasTar) { - const missing = [ - !hasSystemd ? "systemctl" : "", - !hasCurl ? "curl" : "", - !hasTar ? "tar" : "", - ] - .filter(Boolean) - .join(", "); - await msg.edit({ text: `缺少依赖:${missing}` }); - return; - } - - const arch = this.mapArch(process.arch); - if (!arch) { - await msg.edit({ text: `暂不支持当前架构:${process.arch}` }); - return; - } - - let installBase = dirArg && dirArg.trim() ? dirArg.trim() : "/opt/openlist"; - const installPath = this.normalizeInstallPath(installBase); - - if (await this.fileExists(`${installPath}/openlist`)) { - await msg.edit({ text: `检测到已安装于:${installPath}\n请使用:${commandName} update` }); - return; - } - - await msg.edit({ text: `开始安装到:${installPath}` }); - await execAsync(`mkdir -p "${installPath}"`); - - const tarPath = "/tmp/openlist.tar.gz"; - const url = `${GH_BASE_DOWNLOAD}/openlist-linux-musl-${arch}.tar.gz`; - await execAsync( - `curl -L --connect-timeout 10 --retry 3 --retry-delay 3 "${url}" -o "${tarPath}"` - ); - await execAsync(`tar zxf "${tarPath}" -C "${installPath}/"`); - await execAsync(`chmod +x "${installPath}/openlist"`); - - const serviceContent = [ - "[Unit]", - "Description=OpenList service", - "After=network.target", - "", - "[Service]", - "Type=simple", - `WorkingDirectory=${installPath}`, - `ExecStart=${installPath}/openlist server`, - "KillMode=process", - "", - "[Install]", - "WantedBy=multi-user.target", - "", - ].join("\n"); - - await execAsync( - `bash -lc 'cat > /etc/systemd/system/openlist.service <<\"EOF\"\n${serviceContent}\nEOF'` - ); - await execAsync(`systemctl daemon-reload`); - await execAsync(`systemctl enable openlist`); - await execAsync(`systemctl restart openlist`); - - const { stdout: randOut } = await execAsync( - `bash -lc 'cd "${installPath}" && ./openlist admin random 2>&1 || true'` - ); - const userMatch = randOut.match(/username:\s*(\S+)/i); - const passMatch = randOut.match(/password:\s*(\S+)/i); - const username = userMatch ? userMatch[1] : ""; - const password = passMatch ? passMatch[1] : ""; - - // 自动保存初始凭证 - if (username && password) { - await this.updateStoredCredentials(username, password); - } - - const { stdout: verOut } = await execAsync( - `bash -lc '"${installPath}/openlist" version 2>&1 || true'` - ); - const verMatch = verOut.match(/Version:\s*([^\s]+)/); - const version = verMatch ? verMatch[1] : ""; - - let ip = ""; - try { - const { stdout } = await execAsync( - `bash -lc 'hostname -I 2>/dev/null | awk "{print $1}"'` - ); - ip = (stdout || "").trim(); - } catch {} - - const lines: string[] = []; - lines.push("安装完成"); - if (version) lines.push(`版本: ${version}`); - lines.push(`目录: ${installPath}`); - lines.push(`访问: http://${ip || "<服务器IP>"}:5244/`); - if (username && password) { - lines.push(`账号: ${username}`); - lines.push(`密码: ${password}`); - } - await msg.edit({ text: lines.join("\n") }); - } catch (error: any) { - await msg.edit({ text: `安装失败: ${error?.message || error}` }); - } - } - - private async handleUpdate(msg: Api.Message) { - try { - if (process.platform !== "linux") { - await msg.edit({ text: "仅支持 Linux(systemd)环境" }); - return; - } - - const hasSystemd = await this.hasCmd("systemctl"); - const hasCurl = await this.hasCmd("curl"); - const hasTar = await this.hasCmd("tar"); - if (!hasSystemd || !hasCurl || !hasTar) { - const missing = [ - !hasSystemd ? "systemctl" : "", - !hasCurl ? "curl" : "", - !hasTar ? "tar" : "", - ] - .filter(Boolean) - .join(", "); - await msg.edit({ text: `缺少依赖:${missing}` }); - return; - } - - const arch = this.mapArch(process.arch); - if (!arch) { - await msg.edit({ text: `暂不支持当前架构:${process.arch}` }); - return; - } - - const installPath = await this.detectInstalledPath(); - if (!(await this.fileExists(`${installPath}/openlist`))) { - await msg.edit({ text: `未检测到已安装版本。可使用:${commandName} install` }); - return; - } - - await msg.edit({ text: "开始更新..." }); - const tarPath = "/tmp/openlist.tar.gz"; - const url = `${GH_BASE_DOWNLOAD}/openlist-linux-musl-${arch}.tar.gz`; - await execAsync( - `curl -L --connect-timeout 10 --retry 3 --retry-delay 3 "${url}" -o "${tarPath}"` - ); - - await execAsync(`systemctl stop openlist || true`); - await execAsync(`cp "${installPath}/openlist" /tmp/openlist.bak || true`); - await execAsync(`tar zxf "${tarPath}" -C "${installPath}/"`); - await execAsync(`chmod +x "${installPath}/openlist"`); - await execAsync(`systemctl restart openlist`); - - const { stdout: verOut } = await execAsync( - `bash -lc '"${installPath}/openlist" version 2>&1 || true'` - ); - const verMatch = verOut.match(/Version:\s*([^\s]+)/); - const version = verMatch ? verMatch[1] : ""; - await msg.edit({ text: `更新完成${version ? `,版本: ${version}` : ""}` }); - } catch (error: any) { - await msg.edit({ text: `更新失败: ${error?.message || error}` }); - } - } - - private async handleUninstall(msg: Api.Message) { - try { - if (process.platform !== "linux") { - await msg.edit({ text: "仅支持 Linux(systemd)环境" }); - return; - } - - const installPath = await this.detectInstalledPath(); - const existed = await this.fileExists(`${installPath}/openlist`); - await execAsync(`systemctl stop openlist || true`); - await execAsync(`systemctl disable openlist || true`); - await execAsync(`rm -f /etc/systemd/system/openlist.service || true`); - await execAsync(`systemctl daemon-reload || true`); - if (existed) { - await execAsync(`rm -rf "${installPath}"`); - } - await msg.edit({ text: "已卸载" }); - } catch (error: any) { - await msg.edit({ text: `卸载失败: ${error?.message || error}` }); - } - } - - private async handleBackup(msg: Api.Message) { - try { - const installPath = await this.detectInstalledPath(); - if (!(await this.dirExists(`${installPath}/data`))) { - await msg.edit({ text: `未找到配置目录:${installPath}/data` }); - return; - } - - const backupBaseDir = "/opt/openlist_backups"; - const { stdout: dateOut } = await execAsync( - `bash -lc 'date +%Y%m%d_%H%M%S'` - ); - const backupDir = `${backupBaseDir}/backup_${(dateOut || "").trim()}`; - await execAsync(`mkdir -p "${backupDir}"`); - await execAsync(`cp -r "${installPath}/data" "${backupDir}/"`); - - await msg.edit({ text: `备份成功\n目录: ${backupDir}` }); - } catch (error: any) { - await msg.edit({ text: `备份失败: ${error?.message || error}` }); - } - } - - private async handleRestore(msg: Api.Message, backupName?: string) { - try { - const installPath = await this.detectInstalledPath(); - const backupBaseDir = "/opt/openlist_backups"; - let targetBackupDir = ""; - - if (backupName) { - targetBackupDir = `${backupBaseDir}/${backupName}`; - } else { - const { stdout: latestOut } = await execAsync( - `bash -lc 'ls -t "${backupBaseDir}" 2>/dev/null | head -n1'` - ); - const latest = (latestOut || "").trim(); - if (!latest) { - await msg.edit({ text: `未找到任何备份于:${backupBaseDir}` }); - return; - } - targetBackupDir = `${backupBaseDir}/${latest}`; - } - - if (!(await this.dirExists(`${targetBackupDir}/data`))) { - await msg.edit({ text: `无效的备份目录:${targetBackupDir}` }); - return; - } - - await msg.edit({ text: `将从 ${targetBackupDir} 恢复...` }); - await execAsync(`systemctl stop openlist || true`); - await execAsync(`cp -r "${targetBackupDir}/data" "${installPath}/"`); - await execAsync(`systemctl start openlist`); - - await msg.edit({ text: "恢复成功" }); - } catch (error: any) { - await msg.edit({ text: `恢复失败: ${error?.message || error}` }); - } - } - - private async handleAdmin(msg: Api.Message, adminArgs: string[]) { - try { - const installPath = await this.detectInstalledPath(); - if (!(await this.fileExists(`${installPath}/openlist`))) { - await msg.edit({ text: "未检测到 OpenList 安装" }); - return; - } - - const sub = adminArgs[0] || ""; - const arg = adminArgs[1] || ""; - let cmd = ""; - - // 记录需要更新的凭证 - let newUser = ""; - let newPass = ""; - - switch (sub) { - case "setuser": - if (!arg) { - await msg.edit({ text: "用法: admin setuser [新用户名]" }); - return; - } - cmd = `admin setuser "${arg}"`; - newUser = arg; - break; - case "setpass": - if (!arg) { - await msg.edit({ text: "用法: admin setpass [新密码]" }); - return; - } - cmd = `admin set "${arg}"`; // 原脚本中使用 'set' 而非 'setpass' - newPass = arg; - break; - case "random": - cmd = "admin random"; - break; - default: - await msg.edit({ text: helpText, parseMode: "html" }); - return; - } - - await msg.edit({ text: `正在执行: ${cmd}` }); - const { stdout } = await execAsync( - `bash -lc 'cd "${installPath}" && ./openlist ${cmd} 2>&1'` - ); - - // 如果是 random,解析输出 - if (sub === "random") { - const userMatch = stdout.match(/username:\s*(\S+)/i); - const passMatch = stdout.match(/password:\s*(\S+)/i); - if (userMatch) newUser = userMatch[1]; - if (passMatch) newPass = passMatch[1]; - } - - // 更新本地凭证 - if (newUser || newPass) { - await this.updateStoredCredentials(newUser, newPass); - await msg.edit({ text: `执行结果:\n\n
${(stdout || "").trim()}
\n\n✅ 凭证已同步更新`, parseMode: "html" }); - } else { - await msg.edit({ text: `执行结果:\n\n
${(stdout || "").trim()}
`, parseMode: "html" }); - } - } catch (error: any) { - await msg.edit({ text: `管理命令失败: ${error?.message || error}` }); - } - } - - private async handleLogin(msg: Api.Message, user?: string, pass?: string) { - if (!user || !pass) { - await msg.edit({ text: `用法: ${commandName} login [用户名] [密码]` }); - return; - } - await this.updateStoredCredentials(user, pass); - await msg.edit({ text: "✅ 账号信息已保存,可以尝试上传文件了。" }); - } - - private async handleSetDefault(msg: Api.Message, path?: string) { - const db = await this.getDb(); - if (!path) { - // 清空默认路径,恢复为宿主机路径 - await db.update((data) => { - data.defaultPath = ""; - }); - await msg.edit({ text: "✅ 默认上传路径已清空,将恢复为宿主机 /root/Openlist 路径。" }); - return; - } - await db.update((data) => { - data.defaultPath = path; - }); - await msg.edit({ text: `✅ 默认上传路径已设置为: ${path}\n\n现在使用 ${commandName} save 时若不指定路径,将默认上传到此位置。` }); - } - - private async updateStoredCredentials(user?: string, pass?: string) { - const db = await this.getDb(); - await db.update((data) => { - if (user) data.username = user; - if (pass) data.password = pass; - }); - } - - private async getDb() { - const dbPath = path.join(createDirectoryInAssets("openlist"), "credentials.json"); - return await JSONFilePreset(dbPath, { username: "", password: "", defaultPath: "" }); - } - - - private async handleSetPort(msg: Api.Message, port?: string) { - try { - if (!port || !/^\d+$/.test(port)) { - await msg.edit({ text: `用法: ${commandName} setport [端口号]` }); - return; - } - - const installPath = await this.detectInstalledPath(); - const configPath = `${installPath}/data/config.json`; - if (!(await this.fileExists(configPath))) { - await msg.edit({ text: "未找到配置文件,请先确保 OpenList 已成功运行一次。" }); - return; - } - - await msg.edit({ text: `正在修改端口为 ${port}...` }); - await execAsync(`systemctl stop openlist || true`); - // 使用 sed 安全地替换端口号 - await execAsync( - `sed -i 's/"port": *[0-9]*/"port": ${port}/g' "${configPath}"` - ); - await execAsync(`systemctl start openlist`); - - await msg.edit({ text: `端口已修改为 ${port},服务已重启。` }); - } catch (error: any) { - await msg.edit({ text: `端口修改失败: ${error?.message || error}` }); - } - } - - private async handleSave(msg: Api.Message, targetPath?: string) { - try { - const replyToMsg = await msg.getReplyMessage(); - if (!replyToMsg || !replyToMsg.media) { - await msg.edit({ text: "请回复一个文件、图片或视频来保存。" }); - return; - } - - // 确定最终保存路径 - let finalPath = targetPath; - if (!finalPath) { - // 尝试读取默认路径 - const db = await this.getDb(); - finalPath = db.data.defaultPath || ""; - } - - const media = replyToMsg.media; - let fileName = ""; - - if (media instanceof Api.MessageMediaPhoto) { - fileName = `photo_${Date.now()}.jpg`; - } else if (media instanceof Api.MessageMediaDocument && media.document instanceof Api.Document) { - const doc = media.document; - const fileNameAttr = doc.attributes.find( - (attr): attr is Api.DocumentAttributeFilename => - attr instanceof Api.DocumentAttributeFilename - ); - - if (fileNameAttr) { - fileName = fileNameAttr.fileName; - } else { - // 根据 mimeType 推断后缀 - let ext = ""; - switch (doc.mimeType) { - case "video/mp4": ext = ".mp4"; break; - case "video/x-matroska": ext = ".mkv"; break; - case "video/quicktime": ext = ".mov"; break; - case "audio/mpeg": ext = ".mp3"; break; - case "audio/ogg": ext = ".ogg"; break; - case "audio/x-wav": ext = ".wav"; break; - case "image/jpeg": ext = ".jpg"; break; - case "image/png": ext = ".png"; break; - case "image/webp": ext = ".webp"; break; - case "image/gif": ext = ".gif"; break; - case "application/pdf": ext = ".pdf"; break; - case "application/zip": ext = ".zip"; break; - default: ext = ""; - } - fileName = `file_${Date.now()}${ext}`; - } - } else { - // 其他媒体类型,暂时命名为 media_xxx - fileName = `media_${Date.now()}`; - } - - await msg.edit({ text: `正在下载: ${fileName}` }); - - const client = await getGlobalClient(); - const buffer = await client.downloadMedia(replyToMsg.media); - - if (!buffer || !(buffer instanceof Buffer)) { - await msg.edit({ text: "文件下载失败或格式不支持。" }); - return; - } - - if (finalPath) { - await this.uploadToOpenList(msg, buffer, fileName, finalPath); - } else { - const saveDir = "/root/Openlist"; - await fs.mkdir(saveDir, { recursive: true }); - const savePath = path.join(saveDir, fileName); - await fs.writeFile(savePath, buffer); - await msg.edit({ text: `文件已保存到: ${savePath}` }); - } - } catch (error: any) { - await msg.edit({ text: `文件保存失败: ${error?.message || error}` }); - } - } - - private async uploadToOpenList(msg: Api.Message, buffer: Buffer, fileName: string, targetDir: string) { - try { - await msg.edit({ text: "正在登录 OpenList API..." }); - const credentials = await this.getOpenListCredentials(); - if (!credentials) { - throw new Error("未找到 OpenList 凭证。\n请使用以下命令手动配置:\n`op login [用户名] [密码]`"); - } - - const token = await this.getOpenListToken(credentials.username, credentials.password); - if (!token) { - throw new Error("登录 OpenList 失败"); - } - - // 处理路径,确保是 API 友好的格式 - let fullPath = path.join(targetDir, fileName).replace(/\\/g, "/"); - if (!fullPath.startsWith("/")) fullPath = "/" + fullPath; - // 移除多余的斜杠 - fullPath = fullPath.replace(/\/+/g, "/"); - - await msg.edit({ text: `正在上传到: ${fullPath}` }); - - const apiUrl = "http://127.0.0.1:5244/api/fs/put"; - - // 注意:Header 中的中文路径需要编码 - await axios.put(apiUrl, buffer, { - headers: { - "Authorization": token, - "File-Path": encodeURIComponent(fullPath), - "path": encodeURIComponent(fullPath), - "Content-Type": "application/octet-stream", - "As-Task": "false" - }, - maxBodyLength: Infinity, - maxContentLength: Infinity - }); - - await msg.edit({ text: `✅ 文件已上传到 OpenList: ${fullPath}` }); - - } catch (error: any) { - console.error("OpenList Upload Error:", error); - const errMsg = error?.response?.data?.message || error.message || "未知错误"; - throw new Error(`上传失败: ${errMsg}`); - } - } - - private async getOpenListCredentials() { - // 1. Get DB credentials - let dbUser = ""; - let dbPass = ""; - try { - const db = await this.getDb(); - dbUser = db.data.username; - dbPass = db.data.password; - } catch (e) {} - - // 2. Get Config credentials - let configUser = ""; - let configPass = ""; - const installPath = await this.detectInstalledPath(); - const configPath = `${installPath}/data/config.json`; - - if (await this.fileExists(configPath)) { - try { - let configContent = ""; - try { - configContent = await fs.readFile(configPath, "utf-8"); - } catch { - const { stdout } = await execAsync(`cat "${configPath}" 2>/dev/null`); - configContent = stdout; - } - - if (configContent) { - const config = JSON.parse(configContent); - if (config.users && config.users.length > 0) { - configUser = config.users[0].username; - configPass = config.users[0].password; - } - } - } catch (e) { - console.error("Error reading config:", e); - } - } - - // 3. Merge (Prefer DB) - // If DB is missing username but has password (e.g. after setpass), use config username - const finalUser = dbUser || configUser; - // If DB is missing password but has username (e.g. after setuser), use config password (if available/valid) - const finalPass = dbPass || configPass; - - if (finalUser && finalPass) { - // Auto-sync if we had to combine sources - if (!dbUser || !dbPass) { - await this.updateStoredCredentials(finalUser, finalPass); - } - return { username: finalUser, password: finalPass }; - } - - return null; - } - - private async getOpenListToken(username: string, password: string): Promise { - try { - const response = await axios.post("http://127.0.0.1:5244/api/auth/login", { - username, - password - }); - if (response.data && response.data.code === 200) { - return response.data.data.token; - } - throw new Error(response.data?.message || "Login failed"); - } catch (error) { - throw error; - } - } - - private async handleStatus(msg: Api.Message) { - try { - if (process.platform !== "linux") { - await msg.edit({ text: "仅支持 Linux(systemd)环境" }); - return; - } - const installPath = await this.detectInstalledPath(); - const installed = await this.fileExists(`${installPath}/openlist`); - const { stdout: activeOut } = await execAsync( - `bash -lc 'systemctl is-active openlist 2>/dev/null || true'` - ); - const status = (activeOut || "").trim() || "unknown"; - - const { stdout: portOut } = await execAsync( - `bash -lc '(ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -q ":5244" && echo listen || echo closed'` - ); - const port = (portOut || "").trim(); - - let version = ""; - if (installed) { - try { - const { stdout: verOut } = await execAsync( - `bash -lc '"${installPath}/openlist" version 2>&1 | grep -E "^Version:" || true'` - ); - const m = verOut.match(/Version:\s*([^\s]+)/); - version = m ? m[1] : ""; - } catch {} - } - - let publicIp = ""; - try { - const { stdout: ipOut } = await execAsync( - `bash -lc 'curl -s4 --connect-timeout 5 ip.sb || curl -s4 --connect-timeout 5 ifconfig.me'` - ); - publicIp = (ipOut || "").trim(); - } catch {} - - const lines: string[] = []; - lines.push(`状态: ${installed ? `已安装` : "未安装"}`); - lines.push(`服务: ${status}`); - if (version) lines.push(`版本: ${version}`); - lines.push(`端口: ${port}`); - if (publicIp && port === "listen") { - lines.push(`链接: http://${publicIp}:5244/`); - } - - // 显示用户账户信息 - const configPath = `${installPath}/data/config.json`; - if (await this.fileExists(configPath)) { - try { - const configContent = await fs.readFile(configPath, "utf-8"); - const config = JSON.parse(configContent); - if (config.users && config.users.length > 0) { - lines.push("\n账户信息:"); - config.users.forEach((user: any, index: number) => { - lines.push(`${index + 1}. 用户: ${user.username} | 密码: ${user.password}`); - }); - } - } catch (e) { - lines.push("\n无法解析账户信息。"); - } - } - - await msg.edit({ text: lines.join("\n"), parseMode: "html" }); - } catch (error: any) { - await msg.edit({ text: `状态获取失败: ${error?.message || error}` }); - } - } - - private mapArch(nodeArch: string): string | null { - const map: Record = { - x64: "amd64", - arm64: "arm64", - s390x: "s390x", - loong64: "loong64", - }; - return map[nodeArch] || null; - } - - private normalizeInstallPath(input: string): string { - let p = input.replace(/\/+$/, ""); - if (!p.endsWith("/openlist")) p = `${p}/openlist`; - return p; - } - - private async detectInstalledPath(): Promise { - try { - const { stdout } = await execAsync( - `bash -lc 'grep -E "^WorkingDirectory=" /etc/systemd/system/openlist.service 2>/dev/null | head -n1 | cut -d= -f2'` - ); - const p = (stdout || "").trim(); - if (p) return p; - } catch {} - return "/opt/openlist"; - } - - private async hasCmd(cmd: string): Promise { - try { - await execAsync(`bash -lc 'command -v ${cmd} >/dev/null 2>&1'`); - return true; - } catch { - return false; - } - } - - private async dirExists(path: string): Promise { - try { - const { stdout } = await execAsync( - `bash -lc '[ -d "${path}" ] && echo 1 || echo 0'` - ); - return stdout.trim() === "1"; - } catch { - return false; - } - } - - private async isSavedMessages(msg: Api.Message): Promise { - const client = await getGlobalClient(); - const me = await client?.getMe(); - if (!me || !msg.peerId) return false; - - return ( - "userId" in msg.peerId && - msg.peerId.userId?.toString() === me.id.toString() - ); - } - - private async fileExists(path: string): Promise { - try { - const { stdout } = await execAsync( - `bash -lc '[ -f "${path}" ] && echo 1 || echo 0'` - ); - return stdout.trim() === "1"; - } catch { - return false; - } - } -} - -export default new OpenListPlugin(); From cfc2aa14f7932f78456bae4f6bc41c8d9fd4d0b6 Mon Sep 17 00:00:00 2001 From: wmsyw Date: Thu, 4 Dec 2025 15:42:13 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=E6=8C=82=E8=BD=BD=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openlist/openlist.ts | 277 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 255 insertions(+), 22 deletions(-) diff --git a/openlist/openlist.ts b/openlist/openlist.ts index 9773e803..847a9659 100644 --- a/openlist/openlist.ts +++ b/openlist/openlist.ts @@ -4,6 +4,9 @@ import { Api } from "telegram"; import { getGlobalClient } from "@utils/globalClient"; import * as fs from "fs/promises"; import * as path from "path"; +import axios from "axios"; +import { JSONFilePreset } from "lowdb/node"; +import { createDirectoryInAssets } from "@utils/pathHelpers"; import { exec } from "child_process"; import { promisify } from "util"; @@ -37,8 +40,10 @@ const helpText = `⚙️ OpenList 管理插件${commandName} admin setuser [用户名]${commandName} admin setpass [密码]${commandName} admin random +• ${commandName} login [用户] [密码] - 手动配置账号信息 +• ${commandName} setdefault [路径] - 设置默认保存路径 (不填则恢复默认) -• ${commandName} save - (回复文件) 保存到 Openlist 目录 +• ${commandName} save [路径] - (回复文件) 保存到 Openlist 目录 (指定路径则上传到挂载盘) 💡 示例:${commandName} install /data/openlist @@ -51,6 +56,9 @@ class OpenListPlugin extends Plugin { openlist: async (msg: Api.Message) => { await this.handleCommand(msg); }, + op: async (msg: Api.Message) => { + await this.handleCommand(msg); + }, }; private async handleCommand(msg: Api.Message) { @@ -120,7 +128,17 @@ class OpenListPlugin extends Plugin { await this.handleSetPort(msg, args[2]); break; case "save": - await this.handleSave(msg); + await this.handleSave(msg, args[2]); + break; + case "login": + if (!(await this.isSavedMessages(msg))) { + await msg.edit({ text: "⚠️ 此命令仅限在「收藏夹」中使用" }); + return; + } + await this.handleLogin(msg, args[2], args[3]); + break; + case "setdefault": + await this.handleSetDefault(msg, args[2]); break; default: await msg.edit({ text: helpText, parseMode: "html" }); @@ -205,6 +223,11 @@ class OpenListPlugin extends Plugin { const username = userMatch ? userMatch[1] : ""; const password = passMatch ? passMatch[1] : ""; + // 自动保存初始凭证 + if (username && password) { + await this.updateStoredCredentials(username, password); + } + const { stdout: verOut } = await execAsync( `bash -lc '"${installPath}/openlist" version 2>&1 || true'` ); @@ -384,6 +407,10 @@ class OpenListPlugin extends Plugin { const arg = adminArgs[1] || ""; let cmd = ""; + // 记录需要更新的凭证 + let newUser = ""; + let newPass = ""; + switch (sub) { case "setuser": if (!arg) { @@ -391,6 +418,7 @@ class OpenListPlugin extends Plugin { return; } cmd = `admin setuser "${arg}"`; + newUser = arg; break; case "setpass": if (!arg) { @@ -398,6 +426,7 @@ class OpenListPlugin extends Plugin { return; } cmd = `admin set "${arg}"`; // 原脚本中使用 'set' 而非 'setpass' + newPass = arg; break; case "random": cmd = "admin random"; @@ -411,12 +440,66 @@ class OpenListPlugin extends Plugin { const { stdout } = await execAsync( `bash -lc 'cd "${installPath}" && ./openlist ${cmd} 2>&1'` ); - await msg.edit({ text: `执行结果:\n\n
${(stdout || "").trim()}
`, parseMode: "html" }); + + // 如果是 random,解析输出 + if (sub === "random") { + const userMatch = stdout.match(/username:\s*(\S+)/i); + const passMatch = stdout.match(/password:\s*(\S+)/i); + if (userMatch) newUser = userMatch[1]; + if (passMatch) newPass = passMatch[1]; + } + + // 更新本地凭证 + if (newUser || newPass) { + await this.updateStoredCredentials(newUser, newPass); + await msg.edit({ text: `执行结果:\n\n
${(stdout || "").trim()}
\n\n✅ 凭证已同步更新`, parseMode: "html" }); + } else { + await msg.edit({ text: `执行结果:\n\n
${(stdout || "").trim()}
`, parseMode: "html" }); + } } catch (error: any) { await msg.edit({ text: `管理命令失败: ${error?.message || error}` }); } } + private async handleLogin(msg: Api.Message, user?: string, pass?: string) { + if (!user || !pass) { + await msg.edit({ text: `用法: ${commandName} login [用户名] [密码]` }); + return; + } + await this.updateStoredCredentials(user, pass); + await msg.edit({ text: "✅ 账号信息已保存,可以尝试上传文件了。" }); + } + + private async handleSetDefault(msg: Api.Message, path?: string) { + const db = await this.getDb(); + if (!path) { + // 清空默认路径,恢复为宿主机路径 + await db.update((data) => { + data.defaultPath = ""; + }); + await msg.edit({ text: "✅ 默认上传路径已清空,将恢复为宿主机 /root/Openlist 路径。" }); + return; + } + await db.update((data) => { + data.defaultPath = path; + }); + await msg.edit({ text: `✅ 默认上传路径已设置为: ${path}\n\n现在使用 ${commandName} save 时若不指定路径,将默认上传到此位置。` }); + } + + private async updateStoredCredentials(user?: string, pass?: string) { + const db = await this.getDb(); + await db.update((data) => { + if (user) data.username = user; + if (pass) data.password = pass; + }); + } + + private async getDb() { + const dbPath = path.join(createDirectoryInAssets("openlist"), "credentials.json"); + return await JSONFilePreset(dbPath, { username: "", password: "", defaultPath: "" }); + } + + private async handleSetPort(msg: Api.Message, port?: string) { try { if (!port || !/^\d+$/.test(port)) { @@ -445,50 +528,200 @@ class OpenListPlugin extends Plugin { } } - private async handleSave(msg: Api.Message) { + private async handleSave(msg: Api.Message, targetPath?: string) { try { const replyToMsg = await msg.getReplyMessage(); if (!replyToMsg || !replyToMsg.media) { - await msg.edit({ text: "请回复一个文件来保存。" }); + await msg.edit({ text: "请回复一个文件、图片或视频来保存。" }); return; } - const media = replyToMsg.media; - if ( - !(media instanceof Api.MessageMediaDocument) || - !(media.document instanceof Api.Document) - ) { - await msg.edit({ text: "回复的消息不是一个有效的文件。" }); - return; + // 确定最终保存路径 + let finalPath = targetPath; + if (!finalPath) { + // 尝试读取默认路径 + const db = await this.getDb(); + finalPath = db.data.defaultPath || ""; } - const doc = media.document; - const fileNameAttr = doc.attributes.find( - (attr): attr is Api.DocumentAttributeFilename => - attr instanceof Api.DocumentAttributeFilename - ); + const media = replyToMsg.media; + let fileName = ""; + + if (media instanceof Api.MessageMediaPhoto) { + fileName = `photo_${Date.now()}.jpg`; + } else if (media instanceof Api.MessageMediaDocument && media.document instanceof Api.Document) { + const doc = media.document; + const fileNameAttr = doc.attributes.find( + (attr): attr is Api.DocumentAttributeFilename => + attr instanceof Api.DocumentAttributeFilename + ); - const fileName = fileNameAttr ? fileNameAttr.fileName : `file_${Date.now()}`; + if (fileNameAttr) { + fileName = fileNameAttr.fileName; + } else { + // 根据 mimeType 推断后缀 + let ext = ""; + switch (doc.mimeType) { + case "video/mp4": ext = ".mp4"; break; + case "video/x-matroska": ext = ".mkv"; break; + case "video/quicktime": ext = ".mov"; break; + case "audio/mpeg": ext = ".mp3"; break; + case "audio/ogg": ext = ".ogg"; break; + case "audio/x-wav": ext = ".wav"; break; + case "image/jpeg": ext = ".jpg"; break; + case "image/png": ext = ".png"; break; + case "image/webp": ext = ".webp"; break; + case "image/gif": ext = ".gif"; break; + case "application/pdf": ext = ".pdf"; break; + case "application/zip": ext = ".zip"; break; + default: ext = ""; + } + fileName = `file_${Date.now()}${ext}`; + } + } else { + // 其他媒体类型,暂时命名为 media_xxx + fileName = `media_${Date.now()}`; + } - await msg.edit({ text: `正在下载文件: ${fileName}` }); + await msg.edit({ text: `正在下载: ${fileName}` }); const client = await getGlobalClient(); const buffer = await client.downloadMedia(replyToMsg.media); - if (buffer) { + if (!buffer || !(buffer instanceof Buffer)) { + await msg.edit({ text: "文件下载失败或格式不支持。" }); + return; + } + + if (finalPath) { + await this.uploadToOpenList(msg, buffer, fileName, finalPath); + } else { const saveDir = "/root/Openlist"; await fs.mkdir(saveDir, { recursive: true }); const savePath = path.join(saveDir, fileName); await fs.writeFile(savePath, buffer); await msg.edit({ text: `文件已保存到: ${savePath}` }); - } else { - await msg.edit({ text: "文件下载失败。" }); } } catch (error: any) { await msg.edit({ text: `文件保存失败: ${error?.message || error}` }); } } + private async uploadToOpenList(msg: Api.Message, buffer: Buffer, fileName: string, targetDir: string) { + try { + await msg.edit({ text: "正在登录 OpenList API..." }); + const credentials = await this.getOpenListCredentials(); + if (!credentials) { + throw new Error("未找到 OpenList 凭证。\n请使用以下命令手动配置:\n`op login [用户名] [密码]`"); + } + + const token = await this.getOpenListToken(credentials.username, credentials.password); + if (!token) { + throw new Error("登录 OpenList 失败"); + } + + // 处理路径,确保是 API 友好的格式 + let fullPath = path.join(targetDir, fileName).replace(/\\/g, "/"); + if (!fullPath.startsWith("/")) fullPath = "/" + fullPath; + // 移除多余的斜杠 + fullPath = fullPath.replace(/\/+/g, "/"); + + await msg.edit({ text: `正在上传到: ${fullPath}` }); + + const apiUrl = "http://127.0.0.1:5244/api/fs/put"; + + // 注意:Header 中的中文路径需要编码 + await axios.put(apiUrl, buffer, { + headers: { + "Authorization": token, + "File-Path": encodeURIComponent(fullPath), + "path": encodeURIComponent(fullPath), + "Content-Type": "application/octet-stream", + "As-Task": "false" + }, + maxBodyLength: Infinity, + maxContentLength: Infinity + }); + + await msg.edit({ text: `✅ 文件已上传到 OpenList: ${fullPath}` }); + + } catch (error: any) { + console.error("OpenList Upload Error:", error); + const errMsg = error?.response?.data?.message || error.message || "未知错误"; + throw new Error(`上传失败: ${errMsg}`); + } + } + + private async getOpenListCredentials() { + // 1. Get DB credentials + let dbUser = ""; + let dbPass = ""; + try { + const db = await this.getDb(); + dbUser = db.data.username; + dbPass = db.data.password; + } catch (e) {} + + // 2. Get Config credentials + let configUser = ""; + let configPass = ""; + const installPath = await this.detectInstalledPath(); + const configPath = `${installPath}/data/config.json`; + + if (await this.fileExists(configPath)) { + try { + let configContent = ""; + try { + configContent = await fs.readFile(configPath, "utf-8"); + } catch { + const { stdout } = await execAsync(`cat "${configPath}" 2>/dev/null`); + configContent = stdout; + } + + if (configContent) { + const config = JSON.parse(configContent); + if (config.users && config.users.length > 0) { + configUser = config.users[0].username; + configPass = config.users[0].password; + } + } + } catch (e) { + console.error("Error reading config:", e); + } + } + + // 3. Merge (Prefer DB) + // If DB is missing username but has password (e.g. after setpass), use config username + const finalUser = dbUser || configUser; + // If DB is missing password but has username (e.g. after setuser), use config password (if available/valid) + const finalPass = dbPass || configPass; + + if (finalUser && finalPass) { + // Auto-sync if we had to combine sources + if (!dbUser || !dbPass) { + await this.updateStoredCredentials(finalUser, finalPass); + } + return { username: finalUser, password: finalPass }; + } + + return null; + } + + private async getOpenListToken(username: string, password: string): Promise { + try { + const response = await axios.post("http://127.0.0.1:5244/api/auth/login", { + username, + password + }); + if (response.data && response.data.code === 200) { + return response.data.data.token; + } + throw new Error(response.data?.message || "Login failed"); + } catch (error) { + throw error; + } + } + private async handleStatus(msg: Api.Message) { try { if (process.platform !== "linux") { From 55a776ef1956792ffc04394f3ac8d6bb12b377c3 Mon Sep 17 00:00:00 2001 From: wmsyw Date: Thu, 4 Dec 2025 15:53:11 +0800 Subject: [PATCH 6/6] Update plugins.json --- plugins.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins.json b/plugins.json index 96a17b6d..cf0f0e21 100644 --- a/plugins.json +++ b/plugins.json @@ -388,11 +388,11 @@ "desc": "反转你的消息" }, "kkp": { - "url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/rev/kkp.ts?raw=true", + "url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/kkp/kkp.ts?raw=true", "desc": "获取NSFW视频" }, "botmzt": { - "url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/rev/botmzt.ts?raw=true", + "url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/botmzt/botmzt.ts?raw=true", "desc": "随机获取写真图片" } }