diff --git a/git_PR/git_PR.ts b/git_PR/git_PR.ts new file mode 100644 index 00000000..19bdf0b3 --- /dev/null +++ b/git_PR/git_PR.ts @@ -0,0 +1,297 @@ + +import { Plugin } from "@utils/pluginBase"; +import { Api } from "telegram"; +import { getPrefixes } from "@utils/pluginManager"; +import { JSONFilePreset } from "lowdb/node"; +import * as path from "path"; +import { createDirectoryInAssets } from "@utils/pathHelpers"; +import axios from "axios"; + +// HTML转义函数 +const htmlEscape = (text: string): string => + text.replace(/[&<>"']/g, (m) => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[m] || m)); + +// Telegram 长消息处理 +const MAX_MESSAGE_LENGTH = 4096; +function splitMessage(text: string): string[] { + if ((text || "").length <= MAX_MESSAGE_LENGTH) return [text]; + const parts: string[] = []; + let current = ""; + for (const line of (text || "").split("\n")) { + if ((current + (current ? "\n" : "") + line).length > MAX_MESSAGE_LENGTH) { + parts.push(current); + current = line; + } else { + current += (current ? "\n" : "") + line; + } + } + if (current) parts.push(current); + return parts.length ? parts : [text]; +} +async function sendLongMessage(msg: Api.Message, text: string) { + const parts = splitMessage(text); + if (parts.length === 1) { + await msg.edit({ text: parts[0], parseMode: "html" }); + return; + } + await msg.edit({ text: parts[0] + `\n\n📄 (1/${parts.length})`, parseMode: "html" }); + for (let i = 1; i < parts.length; i++) { + await msg.reply({ message: parts[i] + `\n\n📄 (${i + 1}/${parts.length})`, parseMode: "html" }); + } +} + +const prefixes = getPrefixes(); +const mainPrefix = prefixes[0]; +const pluginName = "git"; + +const help_text = `⚙️ Git PR 管理插件 + +命令: +• ${mainPrefix}${pluginName} login <邮箱> <用户名> <Token> - 登录Git +• ${mainPrefix}${pluginName} repos - 列出有编辑权限的仓库 +• ${mainPrefix}${pluginName} prs <仓库名> - 列出仓库的PR +• ${mainPrefix}${pluginName} merge <仓库名> <PR编号> - 合并PR +• ${mainPrefix}${pluginName} help - 显示此帮助消息`; + +// 配置键 +const CONFIG_KEYS = { + EMAIL: "git_email", + USERNAME: "git_username", + TOKEN: "git_token", + API_BASE_URL: "git_api_base_url", +}; + +// 默认配置 +const DEFAULT_CONFIG: Record = { + [CONFIG_KEYS.EMAIL]: "", + [CONFIG_KEYS.USERNAME]: "", + [CONFIG_KEYS.TOKEN]: "", + [CONFIG_KEYS.API_BASE_URL]: "https://api.github.com", +}; + +// 配置管理器 +class ConfigManager { + private static db: any = null; + private static initialized = false; + + private static async init(): Promise { + if (this.initialized) return; + try { + const configPath = path.join( + createDirectoryInAssets("git_manager"), + "config.json" + ); + this.db = await JSONFilePreset>( + configPath, + { ...DEFAULT_CONFIG } + ); + this.initialized = true; + } catch (error) { + console.error("[git] 初始化配置失败:", error); + } + } + + static async get(key: string): Promise { + await this.init(); + return this.db?.data[key] ?? DEFAULT_CONFIG[key] ?? ""; + } + + static async set(key: string, value: string): Promise { + await this.init(); + if (!this.db) return false; + try { + this.db.data[key] = value; + await this.db.write(); + return true; + } catch (error) { + console.error(`[git] 设置配置失败 ${key}:`, error); + return false; + } + } +} + +// 统一创建 GitHub API 客户端 +async function getApi() { + const baseURL = await ConfigManager.get(CONFIG_KEYS.API_BASE_URL); + const token = await ConfigManager.get(CONFIG_KEYS.TOKEN); + if (!token) throw new Error("请先使用 `login` 命令登录"); + + return axios.create({ + baseURL, + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", + "User-Agent": "telebox-git-plugin", + "X-GitHub-Api-Version": "2022-11-28" + } + }); +} + +class GitManagerPlugin extends Plugin { + description: string = `通过Git API管理PR\n\n${help_text}`; + + cmdHandlers = { + [pluginName]: async (msg: Api.Message) => { + const lines = msg.text?.trim()?.split(/\r?\n/g) || []; + const parts = lines?.[0]?.trim()?.split(/\s+/g) || []; + const [, ...args] = parts; + const sub = (args[0] || "").toLowerCase(); + + try { + // 无参数:显示帮助 + if (!sub) { + await sendLongMessage(msg, help_text); + return; + } + + // help 在前:.git help [sub] + if (sub === "help" || sub === "h") { + await sendLongMessage(msg, help_text); + return; + } + + // help 在后:.git [sub] help + if (args[1] && (args[1].toLowerCase() === "help" || args[1].toLowerCase() === "h")) { + await sendLongMessage(msg, help_text); + return; + } + + switch (sub) { + case "login": + await this.handleLogin(msg, args.slice(1)); + break; + case "repos": + await this.handleRepos(msg); + break; + case "prs": + await this.handlePRs(msg, args.slice(1)); + break; + case "merge": + await this.handleMerge(msg, args.slice(1)); + break; + default: + await msg.edit({ text: `❌ 未知子命令: ${htmlEscape(sub)}\n\n${help_text}`, parseMode: "html" }); + } + } catch (error: any) { + console.error('[git] 插件执行失败:', error); + await msg.edit({ text: `❌ 操作失败: ${htmlEscape(error.message)}`, parseMode: "html" }); + } + }, + }; + + private async handleLogin(msg: Api.Message, args: string[]) { + if (args.length < 3) { + await msg.edit({ text: `❌ 参数不足\n\n格式: ${mainPrefix}${pluginName} login <邮箱> <用户名> <Token>`, parseMode: "html" }); + return; + } + + const [email, username, token] = args; + await ConfigManager.set(CONFIG_KEYS.EMAIL, email); + await ConfigManager.set(CONFIG_KEYS.USERNAME, username); + await ConfigManager.set(CONFIG_KEYS.TOKEN, token); + + await msg.edit({ text: "✅ 登录信息已保存", parseMode: "html" }); + } + + private async handleRepos(msg: Api.Message) { + await msg.edit({ text: "🔄 正在获取仓库列表...", parseMode: "html" }); + const api = await getApi(); + const response = await api.get(`/user/repos`, { params: { per_page: 100 } }); + + const repos = (response.data as any[]) + .filter((r: any) => r?.permissions?.push || r?.permissions?.admin || r?.permissions?.maintain) + .map((r: any) => r.full_name); + if (!repos.length) { + await msg.edit({ text: "ℹ️ 未找到有编辑权限的仓库。", parseMode: "html" }); + return; + } + + const repoList = repos.map((repo: string) => `• ${htmlEscape(repo)}`).join("\n"); + await sendLongMessage(msg, `🗂️ 有编辑权限的仓库:\n\n${repoList}`); + } + + private async handlePRs(msg: Api.Message, args: string[]) { + if (args.length < 1) { + throw new Error("参数不足,需要提供仓库名"); + } + const repoName = args[0]; + await msg.edit({ text: `🔄 正在获取 ${htmlEscape(repoName)} 的PR列表...`, parseMode: "html" }); + + const parts = repoName.split("/"); + if (parts.length !== 2) { + throw new Error("仓库名格式应为 owner/repo,例如 octocat/Hello-World"); + } + const [owner, repo] = parts; + + const api = await getApi(); + const response = await api.get(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, { + params: { state: "open", per_page: 50 } + }); + + const list: any[] = response.data || []; + if (!list.length) { + await msg.edit({ text: `ℹ️ 仓库 ${htmlEscape(repoName)} 中没有待处理的PR。`, parseMode: "html" }); + return; + } + + // 获取可合并状态(可能为 null),尽量标注 + const details = [] as { number: number; title: string; user: string; mergeable?: boolean; state?: string }[]; + for (const item of list) { + try { + const pr = await api.get(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${item.number}`); + details.push({ + number: item.number, + title: item.title || "", + user: item?.user?.login || "", + mergeable: pr.data?.mergeable, + state: pr.data?.mergeable_state + }); + } catch { + details.push({ number: item.number, title: item.title || "", user: item?.user?.login || "" }); + } + } + + const prList = details.map((pr) => { + const flag = pr.mergeable === true ? "✅ 可合并" : pr.mergeable === false ? `⛔ 不可合并(${pr.state || "unknown"})` : "❓ 未知"; + return `• #${pr.number}: ${htmlEscape(pr.title)}\n 作者: ${htmlEscape(pr.user)} | 状态: ${flag}`; + }).join("\n\n"); + + await sendLongMessage(msg, `📬 待处理的PR:\n\n${prList}`); + } + + private async handleMerge(msg: Api.Message, args: string[]) { + if (args.length < 2) { + throw new Error("参数不足,需要提供仓库名和PR编号"); + } + const [repoName, prNumberStr] = args; + const prNumber = parseInt(prNumberStr, 10); + if (isNaN(prNumber)) { + throw new Error("PR编号必须是数字"); + } + + await msg.edit({ text: `🔄 正在合并 ${htmlEscape(repoName)} 中的 PR #${prNumber}...`, parseMode: "html" }); + + const parts = repoName.split("/"); + if (parts.length !== 2) { + throw new Error("仓库名格式应为 owner/repo,例如 octocat/Hello-World"); + } + const [owner, repo] = parts; + + const api = await getApi(); + try { + await api.put(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/merge`); + await msg.edit({ text: `✅ 成功合并 PR #${prNumber}`, parseMode: "html" }); + } catch (error: any) { + const errorMsg = error.response?.data?.message || error.message; + throw new Error(`合并失败: ${errorMsg}`); + } + } +} + +export default new GitManagerPlugin(); diff --git a/plugins.json b/plugins.json index 19b65875..769e2036 100644 --- a/plugins.json +++ b/plugins.json @@ -302,5 +302,9 @@ "service": { "url": "https://github.com/TeleBoxDev/TeleBox_Plugins/blob/main/service/service.ts?raw=true", "desc": "systemd服务状态查看工具" + }, + "git_PR": { + "url": "https://github.com/TeleBoxDev/TeleBox_Plugins/blob/main/git_PR/git_PR.ts?raw=true", + "desc": "Git PR 管理插件" } }