koishi-plugin-github-webhook-pusher 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/parser.d.ts +28 -0
- package/lib/types.d.ts +1 -1
- package/package.json +5 -2
- package/readme.md +25 -11
- package/lib/index.js +0 -1016
package/lib/parser.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ import { ParsedEvent } from './types';
|
|
|
8
8
|
* 需求 4.1: 提取 issue 标题、编号、操作者和链接
|
|
9
9
|
*/
|
|
10
10
|
export declare function parseIssuesEvent(payload: any): ParsedEvent | null;
|
|
11
|
+
/**
|
|
12
|
+
* 解析 Issue Comment 事件
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseIssueCommentEvent(payload: any): ParsedEvent | null;
|
|
11
15
|
/**
|
|
12
16
|
* 解析 Release 事件
|
|
13
17
|
* 需求 4.2: 提取版本号、发布者和下载链接
|
|
@@ -23,11 +27,35 @@ export declare function parsePushEvent(payload: any): ParsedEvent | null;
|
|
|
23
27
|
* 需求 4.4: 提取 PR 标题、编号、操作者和链接
|
|
24
28
|
*/
|
|
25
29
|
export declare function parsePullRequestEvent(payload: any): ParsedEvent | null;
|
|
30
|
+
/**
|
|
31
|
+
* 解析 Pull Request Review 事件
|
|
32
|
+
*/
|
|
33
|
+
export declare function parsePullRequestReviewEvent(payload: any): ParsedEvent | null;
|
|
34
|
+
/**
|
|
35
|
+
* 解析 Pull Request Review Comment 事件
|
|
36
|
+
*/
|
|
37
|
+
export declare function parsePullRequestReviewCommentEvent(payload: any): ParsedEvent | null;
|
|
26
38
|
/**
|
|
27
39
|
* 解析 Star 事件
|
|
28
40
|
* 需求 4.5: 提取操作者和当前 star 数量
|
|
29
41
|
*/
|
|
30
42
|
export declare function parseStarEvent(payload: any): ParsedEvent | null;
|
|
43
|
+
/**
|
|
44
|
+
* 解析 Fork 事件
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseForkEvent(payload: any): ParsedEvent | null;
|
|
47
|
+
/**
|
|
48
|
+
* 解析 Create 事件
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseCreateEvent(payload: any): ParsedEvent | null;
|
|
51
|
+
/**
|
|
52
|
+
* 解析 Delete 事件
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseDeleteEvent(payload: any): ParsedEvent | null;
|
|
55
|
+
/**
|
|
56
|
+
* 解析 Workflow Run 事件
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseWorkflowRunEvent(payload: any): ParsedEvent | null;
|
|
31
59
|
/**
|
|
32
60
|
* 统一的事件解析入口函数
|
|
33
61
|
* @param eventName X-GitHub-Event 头的值
|
package/lib/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 需求: 4.1-4.7
|
|
4
4
|
*/
|
|
5
5
|
/** 支持的事件类型 */
|
|
6
|
-
export type EventType = 'issues' | 'release' | 'push' | '
|
|
6
|
+
export type EventType = 'issues' | 'issue_comment' | 'pull_request' | 'pull_request_review' | 'pull_request_review_comment' | 'release' | 'push' | 'star' | 'fork' | 'create' | 'delete' | 'workflow_run';
|
|
7
7
|
/** 提交信息 */
|
|
8
8
|
export interface CommitInfo {
|
|
9
9
|
sha: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-github-webhook-pusher",
|
|
3
3
|
"description": "GitHub Webhook 事件推送插件",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"ClozyA <aoxuan233@gmail.com>"
|
|
7
7
|
],
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "vitest --run",
|
|
22
|
-
"test:watch": "vitest"
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"build:ci": "tsc -p tsconfig.github.json",
|
|
25
|
+
"prepublishOnly": "npm run build:ci"
|
|
23
26
|
},
|
|
24
27
|
"keywords": [
|
|
25
28
|
"chatbot",
|
package/readme.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/koishi-plugin-github-webhook-pusher)
|
|
4
4
|
|
|
5
|
-
Koishi 机器人框架的 GitHub Webhook 推送插件,支持将 GitHub
|
|
5
|
+
Koishi 机器人框架的 GitHub Webhook 推送插件,支持将 GitHub 仓库事件推送到 QQ 群聊或私聊。
|
|
6
6
|
|
|
7
7
|
## 功能特性
|
|
8
8
|
|
|
9
9
|
- 🔐 **安全验证** - 支持 HMAC SHA256 签名验证,确保 Webhook 请求来源可信
|
|
10
10
|
- 📋 **信任仓库管理** - 管理员可控制哪些仓库的事件可以被处理
|
|
11
11
|
- 🔔 **灵活订阅** - 用户可自由订阅感兴趣的仓库和事件类型
|
|
12
|
-
- 📨 **多事件支持** - 支持 Issues、
|
|
12
|
+
- 📨 **多事件支持** - 支持 Issues、Issue Comment、Pull Request、Review、Release、Push、Star、Fork 等常用事件
|
|
13
13
|
- 🚀 **并发推送** - 支持并发推送到多个订阅目标,可配置并发数
|
|
14
14
|
- 💾 **数据持久化** - 订阅和信任仓库数据持久化存储
|
|
15
15
|
|
|
@@ -74,10 +74,17 @@ https://your-koishi-server.com/github/webhook
|
|
|
74
74
|
- **Secret**: 与插件配置中的 `secret` 保持一致
|
|
75
75
|
- **Which events would you like to trigger this webhook?**: 选择需要的事件
|
|
76
76
|
- Issues
|
|
77
|
+
- Issue comments
|
|
78
|
+
- Pull requests
|
|
79
|
+
- Pull request reviews
|
|
80
|
+
- Pull request review comments
|
|
77
81
|
- Releases
|
|
78
82
|
- Pushes
|
|
79
|
-
- Pull requests
|
|
80
83
|
- Stars (Watch)
|
|
84
|
+
- Forks
|
|
85
|
+
- Create
|
|
86
|
+
- Delete
|
|
87
|
+
- Workflow runs
|
|
81
88
|
4. 点击 **Add webhook** 保存
|
|
82
89
|
|
|
83
90
|
### 3. 验证配置
|
|
@@ -108,8 +115,9 @@ https://your-koishi-server.com/github/webhook
|
|
|
108
115
|
| `gh.sub <repo>` | 订阅仓库 | `gh.sub koishijs/koishi` |
|
|
109
116
|
| `gh.unsub <repo>` | 取消订阅 | `gh.unsub koishijs/koishi` |
|
|
110
117
|
| `gh.list` | 列出当前会话的所有订阅 | `gh.list` |
|
|
111
|
-
| `gh.events
|
|
112
|
-
| `gh.
|
|
118
|
+
| `gh.events [repo]` | 查看订阅的事件类型(无 repo 时列出所有可用事件) | `gh.events koishijs/koishi` |
|
|
119
|
+
| `gh.on <repo> [...events]` | 快捷启用订阅事件 | `gh.on koishijs/koishi issues pull_request` |
|
|
120
|
+
| `gh.off <repo> [...events]` | 快捷禁用订阅事件 | `gh.off koishijs/koishi issues pull_request` |
|
|
113
121
|
|
|
114
122
|
### 工具命令
|
|
115
123
|
|
|
@@ -123,10 +131,17 @@ https://your-koishi-server.com/github/webhook
|
|
|
123
131
|
| 事件类型 | 显示名称 | Emoji | 说明 |
|
|
124
132
|
|----------|----------|-------|------|
|
|
125
133
|
| `issues` | Issue | 📌 | Issue 的创建、关闭、重新打开、编辑 |
|
|
126
|
-
| `
|
|
127
|
-
| `push` | Commit | ⬆️ | 代码推送(最多显示 5 条提交) |
|
|
134
|
+
| `issue_comment` | Issue Comment | 💬 | Issue 评论的创建、编辑、删除 |
|
|
128
135
|
| `pull_request` | PR | 🔀 | Pull Request 的创建、关闭、合并 |
|
|
136
|
+
| `pull_request_review` | PR Review | 🧪 | PR Review 的提交、编辑、撤销 |
|
|
137
|
+
| `pull_request_review_comment` | PR Review Comment | 💬 | PR Review 评论的创建、编辑、删除 |
|
|
138
|
+
| `release` | Release | 🚀 | 版本发布 |
|
|
139
|
+
| `push` | Commit | ⬆️ | 代码推送(最多显示 3 条提交) |
|
|
129
140
|
| `star` | Star | ⭐ | Star 操作 |
|
|
141
|
+
| `fork` | Fork | 🍴 | Fork 操作 |
|
|
142
|
+
| `create` | Create | ✨ | 分支/标签创建 |
|
|
143
|
+
| `delete` | Delete | 🗑️ | 分支/标签删除 |
|
|
144
|
+
| `workflow_run` | Workflow | 🧩 | Workflow 运行 |
|
|
130
145
|
|
|
131
146
|
## 消息格式示例
|
|
132
147
|
|
|
@@ -178,10 +193,9 @@ https://github.com/owner/repo/releases/tag/v1.0.0
|
|
|
178
193
|
|
|
179
194
|
### Q: 如何修改订阅的事件类型?
|
|
180
195
|
|
|
181
|
-
**A:** 使用 `gh.
|
|
182
|
-
-
|
|
183
|
-
-
|
|
184
|
-
- 混合操作:`gh.events owner/repo +issues -star`
|
|
196
|
+
**A:** 使用 `gh.on` / `gh.off` 命令:
|
|
197
|
+
- 启用事件:`gh.on owner/repo issues release`
|
|
198
|
+
- 禁用事件:`gh.off owner/repo star push`
|
|
185
199
|
|
|
186
200
|
### Q: 如何查看当前订阅了哪些仓库?
|
|
187
201
|
|
package/lib/index.js
DELETED
|
@@ -1,1016 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name2 in all)
|
|
9
|
-
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
10
|
-
};
|
|
11
|
-
var __copyProps = (to, from, except, desc) => {
|
|
12
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
-
for (let key of __getOwnPropNames(from))
|
|
14
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
-
|
|
21
|
-
// src/index.ts
|
|
22
|
-
var src_exports = {};
|
|
23
|
-
__export(src_exports, {
|
|
24
|
-
Config: () => Config,
|
|
25
|
-
apply: () => apply,
|
|
26
|
-
inject: () => inject,
|
|
27
|
-
name: () => name
|
|
28
|
-
});
|
|
29
|
-
module.exports = __toCommonJS(src_exports);
|
|
30
|
-
var import_koishi5 = require("koishi");
|
|
31
|
-
|
|
32
|
-
// src/config.ts
|
|
33
|
-
var import_koishi = require("koishi");
|
|
34
|
-
|
|
35
|
-
// src/types.ts
|
|
36
|
-
var EVENT_DISPLAY_MAP = {
|
|
37
|
-
issues: { name: "Issue", emoji: "📌" },
|
|
38
|
-
release: { name: "Release", emoji: "🚀" },
|
|
39
|
-
push: { name: "Commit", emoji: "⬆️" },
|
|
40
|
-
pull_request: { name: "PR", emoji: "🔀" },
|
|
41
|
-
star: { name: "Star", emoji: "⭐" }
|
|
42
|
-
};
|
|
43
|
-
function getDisplayType(type) {
|
|
44
|
-
return EVENT_DISPLAY_MAP[type].name;
|
|
45
|
-
}
|
|
46
|
-
__name(getDisplayType, "getDisplayType");
|
|
47
|
-
function getEventEmoji(type) {
|
|
48
|
-
return EVENT_DISPLAY_MAP[type].emoji;
|
|
49
|
-
}
|
|
50
|
-
__name(getEventEmoji, "getEventEmoji");
|
|
51
|
-
|
|
52
|
-
// src/config.ts
|
|
53
|
-
var EVENT_TYPES = ["issues", "release", "push", "pull_request", "star"];
|
|
54
|
-
var Config = import_koishi.Schema.object({
|
|
55
|
-
path: import_koishi.Schema.string().default("/github/webhook").description("Webhook 接收路径"),
|
|
56
|
-
secret: import_koishi.Schema.string().required().description("GitHub Webhook Secret"),
|
|
57
|
-
baseUrl: import_koishi.Schema.string().description("显示用基础 URL"),
|
|
58
|
-
defaultEvents: import_koishi.Schema.array(import_koishi.Schema.union(EVENT_TYPES.map((e) => import_koishi.Schema.const(e)))).default(["issues", "release", "push"]).description("默认订阅事件"),
|
|
59
|
-
debug: import_koishi.Schema.boolean().default(false).description("调试模式"),
|
|
60
|
-
allowUntrusted: import_koishi.Schema.boolean().default(false).description("允许非信任仓库"),
|
|
61
|
-
concurrency: import_koishi.Schema.number().default(5).description("推送并发数")
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// src/database.ts
|
|
65
|
-
function extendDatabase(ctx) {
|
|
66
|
-
ctx.model.extend("github_trusted_repos", {
|
|
67
|
-
id: { type: "unsigned", length: 10 },
|
|
68
|
-
repo: { type: "string", length: 255 },
|
|
69
|
-
enabled: "boolean",
|
|
70
|
-
createdAt: "timestamp",
|
|
71
|
-
updatedAt: "timestamp"
|
|
72
|
-
}, {
|
|
73
|
-
primary: "id",
|
|
74
|
-
autoInc: true,
|
|
75
|
-
unique: ["repo"]
|
|
76
|
-
});
|
|
77
|
-
ctx.model.extend("github_subscriptions", {
|
|
78
|
-
id: { type: "unsigned", length: 10 },
|
|
79
|
-
platform: "string",
|
|
80
|
-
channelId: "string",
|
|
81
|
-
guildId: "string",
|
|
82
|
-
userId: "string",
|
|
83
|
-
repo: "string",
|
|
84
|
-
events: "json",
|
|
85
|
-
enabled: "boolean",
|
|
86
|
-
createdAt: "timestamp",
|
|
87
|
-
updatedAt: "timestamp"
|
|
88
|
-
}, {
|
|
89
|
-
primary: "id",
|
|
90
|
-
autoInc: true
|
|
91
|
-
});
|
|
92
|
-
ctx.model.extend("github_deliveries", {
|
|
93
|
-
deliveryId: "string",
|
|
94
|
-
repo: "string",
|
|
95
|
-
event: "string",
|
|
96
|
-
receivedAt: "timestamp"
|
|
97
|
-
}, {
|
|
98
|
-
primary: "deliveryId"
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
__name(extendDatabase, "extendDatabase");
|
|
102
|
-
|
|
103
|
-
// src/webhook.ts
|
|
104
|
-
var import_koishi4 = require("koishi");
|
|
105
|
-
|
|
106
|
-
// src/signature.ts
|
|
107
|
-
var import_crypto = require("crypto");
|
|
108
|
-
function createSignature(payload, secret) {
|
|
109
|
-
const hmac = (0, import_crypto.createHmac)("sha256", secret);
|
|
110
|
-
hmac.update(payload, "utf8");
|
|
111
|
-
return `sha256=${hmac.digest("hex")}`;
|
|
112
|
-
}
|
|
113
|
-
__name(createSignature, "createSignature");
|
|
114
|
-
function verifySignature(payload, signature, secret) {
|
|
115
|
-
if (!signature || !signature.startsWith("sha256=")) {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
const expected = createSignature(payload, secret);
|
|
119
|
-
const sigBuffer = Buffer.from(signature);
|
|
120
|
-
const expectedBuffer = Buffer.from(expected);
|
|
121
|
-
if (sigBuffer.length !== expectedBuffer.length) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
return (0, import_crypto.timingSafeEqual)(sigBuffer, expectedBuffer);
|
|
125
|
-
}
|
|
126
|
-
__name(verifySignature, "verifySignature");
|
|
127
|
-
|
|
128
|
-
// src/parser.ts
|
|
129
|
-
var MAX_COMMITS = 3;
|
|
130
|
-
function parseIssuesEvent(payload) {
|
|
131
|
-
const { action, issue, repository, sender } = payload;
|
|
132
|
-
if (!["opened", "closed", "reopened", "edited"].includes(action)) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
type: "issues",
|
|
137
|
-
displayType: getDisplayType("issues"),
|
|
138
|
-
repo: repository.full_name,
|
|
139
|
-
actor: sender.login,
|
|
140
|
-
action,
|
|
141
|
-
title: issue.title,
|
|
142
|
-
number: issue.number,
|
|
143
|
-
url: issue.html_url,
|
|
144
|
-
body: issue.body
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
__name(parseIssuesEvent, "parseIssuesEvent");
|
|
148
|
-
function parseReleaseEvent(payload) {
|
|
149
|
-
const { action, release, repository, sender } = payload;
|
|
150
|
-
if (!["published", "created"].includes(action)) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
type: "release",
|
|
155
|
-
displayType: getDisplayType("release"),
|
|
156
|
-
repo: repository.full_name,
|
|
157
|
-
actor: sender.login,
|
|
158
|
-
action,
|
|
159
|
-
title: release.name || release.tag_name,
|
|
160
|
-
tagName: release.tag_name,
|
|
161
|
-
url: release.html_url,
|
|
162
|
-
body: release.body
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
__name(parseReleaseEvent, "parseReleaseEvent");
|
|
166
|
-
function parsePushEvent(payload) {
|
|
167
|
-
const { ref, commits, repository, sender, compare } = payload;
|
|
168
|
-
const branch = ref?.replace("refs/heads/", "") || "";
|
|
169
|
-
const allCommits = (commits || []).map((commit) => ({
|
|
170
|
-
sha: commit.id?.substring(0, 7) || "",
|
|
171
|
-
message: commit.message?.split("\n")[0] || "",
|
|
172
|
-
// 只取第一行
|
|
173
|
-
author: commit.author?.name || commit.author?.username || "",
|
|
174
|
-
url: commit.url || ""
|
|
175
|
-
}));
|
|
176
|
-
const displayCommits = allCommits.slice(0, MAX_COMMITS);
|
|
177
|
-
return {
|
|
178
|
-
type: "push",
|
|
179
|
-
displayType: getDisplayType("push"),
|
|
180
|
-
// 显示为 "Commit"
|
|
181
|
-
repo: repository.full_name,
|
|
182
|
-
actor: sender.login,
|
|
183
|
-
ref: branch,
|
|
184
|
-
commits: displayCommits,
|
|
185
|
-
totalCommits: allCommits.length,
|
|
186
|
-
url: compare || `https://github.com/${repository.full_name}`
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
__name(parsePushEvent, "parsePushEvent");
|
|
190
|
-
function parsePullRequestEvent(payload) {
|
|
191
|
-
const { action, pull_request, repository, sender } = payload;
|
|
192
|
-
const validActions = ["opened", "closed", "reopened", "edited"];
|
|
193
|
-
if (!validActions.includes(action)) {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
const actualAction = action === "closed" && pull_request.merged ? "merged" : action;
|
|
197
|
-
return {
|
|
198
|
-
type: "pull_request",
|
|
199
|
-
displayType: getDisplayType("pull_request"),
|
|
200
|
-
repo: repository.full_name,
|
|
201
|
-
actor: sender.login,
|
|
202
|
-
action: actualAction,
|
|
203
|
-
title: pull_request.title,
|
|
204
|
-
number: pull_request.number,
|
|
205
|
-
url: pull_request.html_url,
|
|
206
|
-
body: pull_request.body
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
__name(parsePullRequestEvent, "parsePullRequestEvent");
|
|
210
|
-
function parseStarEvent(payload) {
|
|
211
|
-
const { action, repository, sender } = payload;
|
|
212
|
-
if (!["created", "deleted"].includes(action)) {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
type: "star",
|
|
217
|
-
displayType: getDisplayType("star"),
|
|
218
|
-
repo: repository.full_name,
|
|
219
|
-
actor: sender.login,
|
|
220
|
-
action,
|
|
221
|
-
starCount: repository.stargazers_count,
|
|
222
|
-
url: repository.html_url
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
__name(parseStarEvent, "parseStarEvent");
|
|
226
|
-
function parseEvent(eventName, payload) {
|
|
227
|
-
switch (eventName) {
|
|
228
|
-
case "issues":
|
|
229
|
-
return parseIssuesEvent(payload);
|
|
230
|
-
case "release":
|
|
231
|
-
return parseReleaseEvent(payload);
|
|
232
|
-
case "push":
|
|
233
|
-
return parsePushEvent(payload);
|
|
234
|
-
case "pull_request":
|
|
235
|
-
return parsePullRequestEvent(payload);
|
|
236
|
-
case "star":
|
|
237
|
-
return parseStarEvent(payload);
|
|
238
|
-
default:
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
__name(parseEvent, "parseEvent");
|
|
243
|
-
|
|
244
|
-
// src/pusher.ts
|
|
245
|
-
var import_koishi3 = require("koishi");
|
|
246
|
-
|
|
247
|
-
// src/repository/subscription.ts
|
|
248
|
-
async function createSubscription(ctx, session, repo, defaultEvents) {
|
|
249
|
-
const now = /* @__PURE__ */ new Date();
|
|
250
|
-
const existing = await ctx.database.get("github_subscriptions", {
|
|
251
|
-
platform: session.platform,
|
|
252
|
-
channelId: session.channelId,
|
|
253
|
-
repo
|
|
254
|
-
});
|
|
255
|
-
if (existing.length > 0) {
|
|
256
|
-
return existing[0];
|
|
257
|
-
}
|
|
258
|
-
await ctx.database.create("github_subscriptions", {
|
|
259
|
-
platform: session.platform,
|
|
260
|
-
channelId: session.channelId,
|
|
261
|
-
guildId: session.guildId || "",
|
|
262
|
-
userId: session.userId || "",
|
|
263
|
-
repo,
|
|
264
|
-
events: defaultEvents,
|
|
265
|
-
enabled: true,
|
|
266
|
-
createdAt: now,
|
|
267
|
-
updatedAt: now
|
|
268
|
-
});
|
|
269
|
-
const [created] = await ctx.database.get("github_subscriptions", {
|
|
270
|
-
platform: session.platform,
|
|
271
|
-
channelId: session.channelId,
|
|
272
|
-
repo
|
|
273
|
-
});
|
|
274
|
-
return created;
|
|
275
|
-
}
|
|
276
|
-
__name(createSubscription, "createSubscription");
|
|
277
|
-
async function removeSubscription(ctx, session, repo) {
|
|
278
|
-
const result = await ctx.database.remove("github_subscriptions", {
|
|
279
|
-
platform: session.platform,
|
|
280
|
-
channelId: session.channelId,
|
|
281
|
-
repo
|
|
282
|
-
});
|
|
283
|
-
return (result.removed ?? 0) > 0;
|
|
284
|
-
}
|
|
285
|
-
__name(removeSubscription, "removeSubscription");
|
|
286
|
-
async function listSubscriptions(ctx, session) {
|
|
287
|
-
return ctx.database.get("github_subscriptions", {
|
|
288
|
-
platform: session.platform,
|
|
289
|
-
channelId: session.channelId
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
__name(listSubscriptions, "listSubscriptions");
|
|
293
|
-
async function getSubscription(ctx, session, repo) {
|
|
294
|
-
const subs = await ctx.database.get("github_subscriptions", {
|
|
295
|
-
platform: session.platform,
|
|
296
|
-
channelId: session.channelId,
|
|
297
|
-
repo
|
|
298
|
-
});
|
|
299
|
-
return subs.length > 0 ? subs[0] : null;
|
|
300
|
-
}
|
|
301
|
-
__name(getSubscription, "getSubscription");
|
|
302
|
-
async function updateEvents(ctx, session, repo, events) {
|
|
303
|
-
const result = await ctx.database.set("github_subscriptions", {
|
|
304
|
-
platform: session.platform,
|
|
305
|
-
channelId: session.channelId,
|
|
306
|
-
repo
|
|
307
|
-
}, {
|
|
308
|
-
events,
|
|
309
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
310
|
-
});
|
|
311
|
-
return (result.modified ?? 0) > 0;
|
|
312
|
-
}
|
|
313
|
-
__name(updateEvents, "updateEvents");
|
|
314
|
-
async function queryTargets(ctx, repo, eventType) {
|
|
315
|
-
const subscriptions = await ctx.database.get("github_subscriptions", {
|
|
316
|
-
repo,
|
|
317
|
-
enabled: true
|
|
318
|
-
});
|
|
319
|
-
return subscriptions.filter((sub) => sub.events.includes(eventType)).map((sub) => ({
|
|
320
|
-
platform: sub.platform,
|
|
321
|
-
channelId: sub.channelId,
|
|
322
|
-
guildId: sub.guildId || void 0,
|
|
323
|
-
userId: sub.userId || void 0
|
|
324
|
-
}));
|
|
325
|
-
}
|
|
326
|
-
__name(queryTargets, "queryTargets");
|
|
327
|
-
|
|
328
|
-
// src/message.ts
|
|
329
|
-
var import_koishi2 = require("koishi");
|
|
330
|
-
function buildMessage(event) {
|
|
331
|
-
switch (event.type) {
|
|
332
|
-
case "issues":
|
|
333
|
-
return buildIssuesMessage(event);
|
|
334
|
-
case "release":
|
|
335
|
-
return buildReleaseMessage(event);
|
|
336
|
-
case "push":
|
|
337
|
-
return buildPushMessage(event);
|
|
338
|
-
case "pull_request":
|
|
339
|
-
return buildPullRequestMessage(event);
|
|
340
|
-
case "star":
|
|
341
|
-
return buildStarMessage(event);
|
|
342
|
-
default:
|
|
343
|
-
return buildGenericMessage(event);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
__name(buildMessage, "buildMessage");
|
|
347
|
-
function buildIssuesMessage(event) {
|
|
348
|
-
const emoji = getEventEmoji(event.type);
|
|
349
|
-
const actionText = getActionText(event.action);
|
|
350
|
-
const lines = [
|
|
351
|
-
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
352
|
-
`${event.actor} ${actionText} #${event.number}: ${event.title}`,
|
|
353
|
-
event.url
|
|
354
|
-
];
|
|
355
|
-
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
356
|
-
}
|
|
357
|
-
__name(buildIssuesMessage, "buildIssuesMessage");
|
|
358
|
-
function buildReleaseMessage(event) {
|
|
359
|
-
const emoji = getEventEmoji(event.type);
|
|
360
|
-
const actionText = getActionText(event.action);
|
|
361
|
-
const lines = [
|
|
362
|
-
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
363
|
-
`${event.actor} ${actionText} ${event.tagName || event.title}`,
|
|
364
|
-
event.url
|
|
365
|
-
];
|
|
366
|
-
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
367
|
-
}
|
|
368
|
-
__name(buildReleaseMessage, "buildReleaseMessage");
|
|
369
|
-
function buildPushMessage(event) {
|
|
370
|
-
const emoji = getEventEmoji(event.type);
|
|
371
|
-
const commits = event.commits || [];
|
|
372
|
-
const totalCommits = event.totalCommits || commits.length;
|
|
373
|
-
const lines = [
|
|
374
|
-
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
375
|
-
`${event.actor} 推送了 ${totalCommits} 个提交到 ${event.ref}`,
|
|
376
|
-
""
|
|
377
|
-
];
|
|
378
|
-
for (const commit of commits) {
|
|
379
|
-
lines.push(`• ${commit.sha} - ${commit.message}`);
|
|
380
|
-
}
|
|
381
|
-
if (totalCommits > commits.length) {
|
|
382
|
-
lines.push("");
|
|
383
|
-
lines.push(`还有 ${totalCommits - commits.length} 条提交...`);
|
|
384
|
-
}
|
|
385
|
-
lines.push(event.url);
|
|
386
|
-
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
387
|
-
}
|
|
388
|
-
__name(buildPushMessage, "buildPushMessage");
|
|
389
|
-
function buildPullRequestMessage(event) {
|
|
390
|
-
const emoji = getEventEmoji(event.type);
|
|
391
|
-
const actionText = getActionText(event.action);
|
|
392
|
-
const lines = [
|
|
393
|
-
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
394
|
-
`${event.actor} ${actionText} #${event.number}: ${event.title}`,
|
|
395
|
-
event.url
|
|
396
|
-
];
|
|
397
|
-
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
398
|
-
}
|
|
399
|
-
__name(buildPullRequestMessage, "buildPullRequestMessage");
|
|
400
|
-
function buildStarMessage(event) {
|
|
401
|
-
const emoji = getEventEmoji(event.type);
|
|
402
|
-
const actionText = event.action === "created" ? "starred" : "unstarred";
|
|
403
|
-
const lines = [
|
|
404
|
-
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
405
|
-
`${event.actor} ${actionText} (⭐ ${event.starCount})`,
|
|
406
|
-
event.url
|
|
407
|
-
];
|
|
408
|
-
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
409
|
-
}
|
|
410
|
-
__name(buildStarMessage, "buildStarMessage");
|
|
411
|
-
function buildGenericMessage(event) {
|
|
412
|
-
const emoji = getEventEmoji(event.type);
|
|
413
|
-
const lines = [
|
|
414
|
-
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
415
|
-
`${event.actor} ${event.action || "triggered"}`,
|
|
416
|
-
event.url
|
|
417
|
-
];
|
|
418
|
-
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
419
|
-
}
|
|
420
|
-
__name(buildGenericMessage, "buildGenericMessage");
|
|
421
|
-
function getActionText(action) {
|
|
422
|
-
const actionMap = {
|
|
423
|
-
opened: "opened",
|
|
424
|
-
closed: "closed",
|
|
425
|
-
reopened: "reopened",
|
|
426
|
-
edited: "edited",
|
|
427
|
-
merged: "merged",
|
|
428
|
-
published: "published",
|
|
429
|
-
created: "created",
|
|
430
|
-
deleted: "deleted"
|
|
431
|
-
};
|
|
432
|
-
return actionMap[action || ""] || action || "";
|
|
433
|
-
}
|
|
434
|
-
__name(getActionText, "getActionText");
|
|
435
|
-
|
|
436
|
-
// src/pusher.ts
|
|
437
|
-
var logger = new import_koishi3.Logger("github-webhook-pusher");
|
|
438
|
-
async function queryTargets2(ctx, repo, eventType) {
|
|
439
|
-
return queryTargets(ctx, repo, eventType);
|
|
440
|
-
}
|
|
441
|
-
__name(queryTargets2, "queryTargets");
|
|
442
|
-
async function sendMessage(ctx, target, message) {
|
|
443
|
-
try {
|
|
444
|
-
const bot = ctx.bots.find((b) => b.platform === target.platform);
|
|
445
|
-
if (!bot) {
|
|
446
|
-
throw new Error(`未找到平台 ${target.platform} 的 bot`);
|
|
447
|
-
}
|
|
448
|
-
await bot.sendMessage(target.channelId, message, target.guildId);
|
|
449
|
-
return { target, success: true };
|
|
450
|
-
} catch (error) {
|
|
451
|
-
return {
|
|
452
|
-
target,
|
|
453
|
-
success: false,
|
|
454
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
__name(sendMessage, "sendMessage");
|
|
459
|
-
async function pushWithConcurrency(ctx, targets, message, concurrency) {
|
|
460
|
-
if (targets.length === 0) {
|
|
461
|
-
return [];
|
|
462
|
-
}
|
|
463
|
-
const results = [];
|
|
464
|
-
const queue = [...targets];
|
|
465
|
-
const workers = Array(Math.min(concurrency, queue.length)).fill(null).map(async () => {
|
|
466
|
-
while (queue.length > 0) {
|
|
467
|
-
const target = queue.shift();
|
|
468
|
-
if (!target) break;
|
|
469
|
-
const result = await sendMessage(ctx, target, message);
|
|
470
|
-
results.push(result);
|
|
471
|
-
if (!result.success && result.error) {
|
|
472
|
-
logger.error(
|
|
473
|
-
`推送失败 [${target.platform}:${target.channelId}]: ${result.error.message}`
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
await Promise.all(workers);
|
|
479
|
-
return results;
|
|
480
|
-
}
|
|
481
|
-
__name(pushWithConcurrency, "pushWithConcurrency");
|
|
482
|
-
async function pushMessage(ctx, event, concurrency = 5) {
|
|
483
|
-
const targets = await queryTargets2(ctx, event.repo, event.type);
|
|
484
|
-
if (targets.length === 0) {
|
|
485
|
-
logger.debug(`仓库 ${event.repo} 的 ${event.type} 事件没有订阅目标`);
|
|
486
|
-
return [];
|
|
487
|
-
}
|
|
488
|
-
logger.info(`准备推送 ${event.type} 事件到 ${targets.length} 个目标`);
|
|
489
|
-
const message = buildMessage(event);
|
|
490
|
-
const results = await pushWithConcurrency(ctx, targets, message, concurrency);
|
|
491
|
-
const successCount = results.filter((r) => r.success).length;
|
|
492
|
-
const failCount = results.filter((r) => !r.success).length;
|
|
493
|
-
logger.info(`推送完成: 成功 ${successCount}, 失败 ${failCount}`);
|
|
494
|
-
return results;
|
|
495
|
-
}
|
|
496
|
-
__name(pushMessage, "pushMessage");
|
|
497
|
-
async function pushEvent(ctx, event, concurrency = 5) {
|
|
498
|
-
const results = await pushMessage(ctx, event, concurrency);
|
|
499
|
-
return {
|
|
500
|
-
pushed: results.filter((r) => r.success).length,
|
|
501
|
-
failed: results.filter((r) => !r.success).length,
|
|
502
|
-
results
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
__name(pushEvent, "pushEvent");
|
|
506
|
-
|
|
507
|
-
// src/repository/trust.ts
|
|
508
|
-
var REPO_NAME_REGEX = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
509
|
-
function isValidRepoFormat(repo) {
|
|
510
|
-
if (!repo || typeof repo !== "string") {
|
|
511
|
-
return false;
|
|
512
|
-
}
|
|
513
|
-
return REPO_NAME_REGEX.test(repo);
|
|
514
|
-
}
|
|
515
|
-
__name(isValidRepoFormat, "isValidRepoFormat");
|
|
516
|
-
async function addTrustedRepo(ctx, repo) {
|
|
517
|
-
if (!isValidRepoFormat(repo)) {
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
const now = /* @__PURE__ */ new Date();
|
|
521
|
-
const existing = await ctx.database.get("github_trusted_repos", { repo });
|
|
522
|
-
if (existing.length > 0) {
|
|
523
|
-
return existing[0];
|
|
524
|
-
}
|
|
525
|
-
await ctx.database.create("github_trusted_repos", {
|
|
526
|
-
repo,
|
|
527
|
-
enabled: true,
|
|
528
|
-
createdAt: now,
|
|
529
|
-
updatedAt: now
|
|
530
|
-
});
|
|
531
|
-
const [created] = await ctx.database.get("github_trusted_repos", { repo });
|
|
532
|
-
return created;
|
|
533
|
-
}
|
|
534
|
-
__name(addTrustedRepo, "addTrustedRepo");
|
|
535
|
-
async function removeTrustedRepo(ctx, repo) {
|
|
536
|
-
const result = await ctx.database.remove("github_trusted_repos", { repo });
|
|
537
|
-
return (result.removed ?? 0) > 0;
|
|
538
|
-
}
|
|
539
|
-
__name(removeTrustedRepo, "removeTrustedRepo");
|
|
540
|
-
async function listTrustedRepos(ctx) {
|
|
541
|
-
return ctx.database.get("github_trusted_repos", {});
|
|
542
|
-
}
|
|
543
|
-
__name(listTrustedRepos, "listTrustedRepos");
|
|
544
|
-
async function isTrusted(ctx, repo) {
|
|
545
|
-
const repos = await ctx.database.get("github_trusted_repos", { repo, enabled: true });
|
|
546
|
-
return repos.length > 0;
|
|
547
|
-
}
|
|
548
|
-
__name(isTrusted, "isTrusted");
|
|
549
|
-
async function isInTrustList(ctx, repo) {
|
|
550
|
-
const repos = await ctx.database.get("github_trusted_repos", { repo });
|
|
551
|
-
return repos.length > 0;
|
|
552
|
-
}
|
|
553
|
-
__name(isInTrustList, "isInTrustList");
|
|
554
|
-
async function enableRepo(ctx, repo) {
|
|
555
|
-
const result = await ctx.database.set("github_trusted_repos", { repo }, {
|
|
556
|
-
enabled: true,
|
|
557
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
558
|
-
});
|
|
559
|
-
return (result.modified ?? 0) > 0;
|
|
560
|
-
}
|
|
561
|
-
__name(enableRepo, "enableRepo");
|
|
562
|
-
async function disableRepo(ctx, repo) {
|
|
563
|
-
const result = await ctx.database.set("github_trusted_repos", { repo }, {
|
|
564
|
-
enabled: false,
|
|
565
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
566
|
-
});
|
|
567
|
-
return (result.modified ?? 0) > 0;
|
|
568
|
-
}
|
|
569
|
-
__name(disableRepo, "disableRepo");
|
|
570
|
-
|
|
571
|
-
// src/repository/delivery.ts
|
|
572
|
-
async function recordDelivery(ctx, deliveryId, repo, event) {
|
|
573
|
-
const now = /* @__PURE__ */ new Date();
|
|
574
|
-
await ctx.database.create("github_deliveries", {
|
|
575
|
-
deliveryId,
|
|
576
|
-
repo,
|
|
577
|
-
event,
|
|
578
|
-
receivedAt: now
|
|
579
|
-
});
|
|
580
|
-
const [created] = await ctx.database.get("github_deliveries", { deliveryId });
|
|
581
|
-
return created;
|
|
582
|
-
}
|
|
583
|
-
__name(recordDelivery, "recordDelivery");
|
|
584
|
-
async function isDelivered(ctx, deliveryId) {
|
|
585
|
-
const deliveries = await ctx.database.get("github_deliveries", { deliveryId });
|
|
586
|
-
return deliveries.length > 0;
|
|
587
|
-
}
|
|
588
|
-
__name(isDelivered, "isDelivered");
|
|
589
|
-
|
|
590
|
-
// src/webhook.ts
|
|
591
|
-
var logger2 = new import_koishi4.Logger("github-webhook");
|
|
592
|
-
function registerWebhook(ctx, config) {
|
|
593
|
-
const path = config.path || "/github/webhook";
|
|
594
|
-
logger2.info(`注册 Webhook 处理器: ${path}`);
|
|
595
|
-
ctx.server.post(path, async (koaCtx) => {
|
|
596
|
-
const startTime = Date.now();
|
|
597
|
-
try {
|
|
598
|
-
const signature = koaCtx.get("X-Hub-Signature-256");
|
|
599
|
-
const eventName = koaCtx.get("X-GitHub-Event");
|
|
600
|
-
const deliveryId = koaCtx.get("X-GitHub-Delivery");
|
|
601
|
-
const rawBody = await getRawBody(koaCtx);
|
|
602
|
-
if (config.debug) {
|
|
603
|
-
logger2.debug(`收到 Webhook 请求: event=${eventName}, delivery=${deliveryId}`);
|
|
604
|
-
}
|
|
605
|
-
if (config.secret) {
|
|
606
|
-
if (!signature) {
|
|
607
|
-
logger2.warn("请求缺少 X-Hub-Signature-256 头");
|
|
608
|
-
koaCtx.status = 401;
|
|
609
|
-
koaCtx.body = { error: "Missing signature" };
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
if (!verifySignature(rawBody, signature, config.secret)) {
|
|
613
|
-
logger2.warn("签名验证失败");
|
|
614
|
-
koaCtx.status = 401;
|
|
615
|
-
koaCtx.body = {
|
|
616
|
-
error: "Invalid signature. Ensure Content-Type is application/json, and the signed payload matches the raw request body. 签名无效:请确认 Content-Type 为 application/json,且签名内容与实际请求体完全一致。"
|
|
617
|
-
};
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
let payload;
|
|
622
|
-
try {
|
|
623
|
-
payload = JSON.parse(rawBody);
|
|
624
|
-
} catch (e) {
|
|
625
|
-
logger2.warn("无法解析 JSON 负载");
|
|
626
|
-
koaCtx.status = 400;
|
|
627
|
-
koaCtx.body = { error: "Invalid JSON payload" };
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
const repo = payload.repository?.full_name;
|
|
631
|
-
if (!repo) {
|
|
632
|
-
logger2.warn("负载中缺少仓库信息");
|
|
633
|
-
koaCtx.status = 400;
|
|
634
|
-
koaCtx.body = { error: "Missing repository information" };
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
logger2.info(`收到 Webhook: [${repo}] ${eventName} (${deliveryId})`);
|
|
638
|
-
if (deliveryId) {
|
|
639
|
-
const isDuplicate = await isDelivered(ctx, deliveryId);
|
|
640
|
-
if (isDuplicate) {
|
|
641
|
-
logger2.info(`跳过重复请求: ${deliveryId}`);
|
|
642
|
-
koaCtx.status = 200;
|
|
643
|
-
koaCtx.body = { status: "duplicate" };
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
if (!config.allowUntrusted) {
|
|
648
|
-
const trusted = await isTrusted(ctx, repo);
|
|
649
|
-
if (!trusted) {
|
|
650
|
-
logger2.info(`忽略非信任仓库事件: ${repo}`);
|
|
651
|
-
koaCtx.status = 200;
|
|
652
|
-
koaCtx.body = { status: "ignored", reason: "untrusted" };
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
const event = parseEvent(eventName, payload);
|
|
657
|
-
if (!event) {
|
|
658
|
-
logger2.info(`不支持的事件类型: ${eventName}`);
|
|
659
|
-
koaCtx.status = 200;
|
|
660
|
-
koaCtx.body = { status: "ignored", reason: "unsupported" };
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
if (deliveryId) {
|
|
664
|
-
await recordDelivery(ctx, deliveryId, repo, eventName);
|
|
665
|
-
}
|
|
666
|
-
const result = await pushEvent(ctx, event, config.concurrency);
|
|
667
|
-
const elapsed = Date.now() - startTime;
|
|
668
|
-
logger2.info(`处理完成: 推送 ${result.pushed} 成功, ${result.failed} 失败 (${elapsed}ms)`);
|
|
669
|
-
koaCtx.status = 200;
|
|
670
|
-
koaCtx.body = { status: "ok", pushed: result.pushed };
|
|
671
|
-
} catch (error) {
|
|
672
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
673
|
-
logger2.error(`处理 Webhook 时发生错误: ${errorMessage}`);
|
|
674
|
-
if (config.debug && error instanceof Error) {
|
|
675
|
-
logger2.error(error.stack || "");
|
|
676
|
-
}
|
|
677
|
-
koaCtx.status = 500;
|
|
678
|
-
koaCtx.body = { status: "error", error: "Internal server error" };
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
__name(registerWebhook, "registerWebhook");
|
|
683
|
-
async function getRawBody(koaCtx) {
|
|
684
|
-
if (koaCtx.request.body) {
|
|
685
|
-
return JSON.stringify(koaCtx.request.body);
|
|
686
|
-
}
|
|
687
|
-
return new Promise((resolve, reject) => {
|
|
688
|
-
let data = "";
|
|
689
|
-
koaCtx.req.on("data", (chunk) => {
|
|
690
|
-
data += chunk.toString();
|
|
691
|
-
});
|
|
692
|
-
koaCtx.req.on("end", () => {
|
|
693
|
-
resolve(data);
|
|
694
|
-
});
|
|
695
|
-
koaCtx.req.on("error", reject);
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
__name(getRawBody, "getRawBody");
|
|
699
|
-
|
|
700
|
-
// src/commands/trust.ts
|
|
701
|
-
var ADMIN_AUTHORITY = 3;
|
|
702
|
-
function registerTrustCommands(ctx) {
|
|
703
|
-
const trust = ctx.command("gh.trust", "管理信任的 GitHub 仓库").usage("gh.trust <add|remove|list|enable|disable> [repo]");
|
|
704
|
-
trust.subcommand(".add <repo:string>", "添加信任仓库").usage("gh.trust.add owner/repo").example("gh.trust.add koishijs/koishi").action(async ({ session }, repo) => {
|
|
705
|
-
const user = session?.user;
|
|
706
|
-
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
707
|
-
return "❌ 权限不足,需要管理员权限";
|
|
708
|
-
}
|
|
709
|
-
if (!repo) {
|
|
710
|
-
return "❌ 请指定仓库名,格式: owner/repo";
|
|
711
|
-
}
|
|
712
|
-
if (!isValidRepoFormat(repo)) {
|
|
713
|
-
return "❌ 仓库格式错误,请使用 owner/repo 格式";
|
|
714
|
-
}
|
|
715
|
-
const result = await addTrustedRepo(ctx, repo);
|
|
716
|
-
if (result) {
|
|
717
|
-
return `✅ 已添加信任仓库: ${repo}`;
|
|
718
|
-
}
|
|
719
|
-
return "❌ 添加失败";
|
|
720
|
-
});
|
|
721
|
-
trust.subcommand(".remove <repo:string>", "移除信任仓库").usage("gh.trust.remove owner/repo").example("gh.trust.remove koishijs/koishi").action(async ({ session }, repo) => {
|
|
722
|
-
const user = session?.user;
|
|
723
|
-
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
724
|
-
return "❌ 权限不足,需要管理员权限";
|
|
725
|
-
}
|
|
726
|
-
if (!repo) {
|
|
727
|
-
return "❌ 请指定仓库名";
|
|
728
|
-
}
|
|
729
|
-
const success = await removeTrustedRepo(ctx, repo);
|
|
730
|
-
if (success) {
|
|
731
|
-
return `✅ 已移除信任仓库: ${repo}`;
|
|
732
|
-
}
|
|
733
|
-
return `❌ 仓库 ${repo} 不在信任列表中`;
|
|
734
|
-
});
|
|
735
|
-
trust.subcommand(".list", "列出所有信任仓库").usage("gh.trust.list").action(async ({ session }) => {
|
|
736
|
-
const user = session?.user;
|
|
737
|
-
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
738
|
-
return "❌ 权限不足,需要管理员权限";
|
|
739
|
-
}
|
|
740
|
-
const repos = await listTrustedRepos(ctx);
|
|
741
|
-
if (repos.length === 0) {
|
|
742
|
-
return "📋 信任仓库列表为空";
|
|
743
|
-
}
|
|
744
|
-
const lines = ["📋 信任仓库列表:"];
|
|
745
|
-
for (const repo of repos) {
|
|
746
|
-
const status = repo.enabled ? "✅" : "⏸️";
|
|
747
|
-
lines.push(`${status} ${repo.repo}`);
|
|
748
|
-
}
|
|
749
|
-
return lines.join("\n");
|
|
750
|
-
});
|
|
751
|
-
trust.subcommand(".enable <repo:string>", "启用信任仓库").usage("gh.trust.enable owner/repo").example("gh.trust.enable koishijs/koishi").action(async ({ session }, repo) => {
|
|
752
|
-
const user = session?.user;
|
|
753
|
-
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
754
|
-
return "❌ 权限不足,需要管理员权限";
|
|
755
|
-
}
|
|
756
|
-
if (!repo) {
|
|
757
|
-
return "❌ 请指定仓库名";
|
|
758
|
-
}
|
|
759
|
-
const success = await enableRepo(ctx, repo);
|
|
760
|
-
if (success) {
|
|
761
|
-
return `✅ 已启用仓库: ${repo}`;
|
|
762
|
-
}
|
|
763
|
-
return `❌ 仓库 ${repo} 不在信任列表中`;
|
|
764
|
-
});
|
|
765
|
-
trust.subcommand(".disable <repo:string>", "禁用信任仓库").usage("gh.trust.disable owner/repo").example("gh.trust.disable koishijs/koishi").action(async ({ session }, repo) => {
|
|
766
|
-
const user = session?.user;
|
|
767
|
-
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
768
|
-
return "❌ 权限不足,需要管理员权限";
|
|
769
|
-
}
|
|
770
|
-
if (!repo) {
|
|
771
|
-
return "❌ 请指定仓库名";
|
|
772
|
-
}
|
|
773
|
-
const success = await disableRepo(ctx, repo);
|
|
774
|
-
if (success) {
|
|
775
|
-
return `⏸️ 已禁用仓库: ${repo}`;
|
|
776
|
-
}
|
|
777
|
-
return `❌ 仓库 ${repo} 不在信任列表中`;
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
__name(registerTrustCommands, "registerTrustCommands");
|
|
781
|
-
|
|
782
|
-
// src/commands/subscription.ts
|
|
783
|
-
var ALL_EVENT_TYPES = ["issues", "release", "push", "pull_request", "star"];
|
|
784
|
-
function getSessionIdentifier(session) {
|
|
785
|
-
return {
|
|
786
|
-
platform: session.platform,
|
|
787
|
-
channelId: session.channelId,
|
|
788
|
-
guildId: session.guildId,
|
|
789
|
-
userId: session.userId
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
__name(getSessionIdentifier, "getSessionIdentifier");
|
|
793
|
-
function parseEventChanges(changes, currentEvents) {
|
|
794
|
-
const events = new Set(currentEvents);
|
|
795
|
-
for (const change of changes) {
|
|
796
|
-
if (!change) continue;
|
|
797
|
-
const prefix = change[0];
|
|
798
|
-
const eventName = change.slice(1);
|
|
799
|
-
if (!ALL_EVENT_TYPES.includes(eventName)) {
|
|
800
|
-
continue;
|
|
801
|
-
}
|
|
802
|
-
if (prefix === "+") {
|
|
803
|
-
events.add(eventName);
|
|
804
|
-
} else if (prefix === "-") {
|
|
805
|
-
events.delete(eventName);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
return Array.from(events);
|
|
809
|
-
}
|
|
810
|
-
__name(parseEventChanges, "parseEventChanges");
|
|
811
|
-
function registerSubscriptionCommands(ctx, config) {
|
|
812
|
-
ctx.command("gh.sub <repo:string>", "订阅 GitHub 仓库事件").usage("gh.sub owner/repo").example("gh.sub koishijs/koishi").action(async ({ session }, repo) => {
|
|
813
|
-
if (!session) return "❌ 无法获取会话信息";
|
|
814
|
-
if (!repo) {
|
|
815
|
-
return "❌ 请指定仓库名,格式: owner/repo";
|
|
816
|
-
}
|
|
817
|
-
const trusted = await isInTrustList(ctx, repo);
|
|
818
|
-
if (!trusted) {
|
|
819
|
-
return "❌ 该仓库不在信任列表中";
|
|
820
|
-
}
|
|
821
|
-
const sessionId = getSessionIdentifier(session);
|
|
822
|
-
const subscription = await createSubscription(ctx, sessionId, repo, config.defaultEvents);
|
|
823
|
-
if (subscription) {
|
|
824
|
-
const eventList = subscription.events.join(", ");
|
|
825
|
-
return `✅ 已订阅仓库: ${repo}
|
|
826
|
-
📋 订阅事件: ${eventList}`;
|
|
827
|
-
}
|
|
828
|
-
return "❌ 订阅失败";
|
|
829
|
-
});
|
|
830
|
-
ctx.command("gh.unsub <repo:string>", "取消订阅 GitHub 仓库").usage("gh.unsub owner/repo").example("gh.unsub koishijs/koishi").action(async ({ session }, repo) => {
|
|
831
|
-
if (!session) return "❌ 无法获取会话信息";
|
|
832
|
-
if (!repo) {
|
|
833
|
-
return "❌ 请指定仓库名";
|
|
834
|
-
}
|
|
835
|
-
const sessionId = getSessionIdentifier(session);
|
|
836
|
-
const success = await removeSubscription(ctx, sessionId, repo);
|
|
837
|
-
if (success) {
|
|
838
|
-
return `✅ 已取消订阅: ${repo}`;
|
|
839
|
-
}
|
|
840
|
-
return `❌ 未找到仓库 ${repo} 的订阅`;
|
|
841
|
-
});
|
|
842
|
-
ctx.command("gh.list", "列出当前会话的所有订阅").usage("gh.list").action(async ({ session }) => {
|
|
843
|
-
if (!session) return "❌ 无法获取会话信息";
|
|
844
|
-
const sessionId = getSessionIdentifier(session);
|
|
845
|
-
const subscriptions = await listSubscriptions(ctx, sessionId);
|
|
846
|
-
if (subscriptions.length === 0) {
|
|
847
|
-
return "📋 当前会话没有订阅任何仓库";
|
|
848
|
-
}
|
|
849
|
-
const lines = ["📋 订阅列表:"];
|
|
850
|
-
for (const sub of subscriptions) {
|
|
851
|
-
const status = sub.enabled ? "✅" : "⏸️";
|
|
852
|
-
const events = sub.events.join(", ");
|
|
853
|
-
lines.push(`${status} ${sub.repo}`);
|
|
854
|
-
lines.push(` 事件: ${events}`);
|
|
855
|
-
}
|
|
856
|
-
return lines.join("\n");
|
|
857
|
-
});
|
|
858
|
-
ctx.command("gh.events <repo:string> [...changes:string]", "查看或修改订阅的事件类型").usage("gh.events owner/repo [+event] [-event]").example("gh.events koishijs/koishi").example("gh.events koishijs/koishi +issues -star +release").action(async ({ session }, repo, ...changes) => {
|
|
859
|
-
if (!session) return "❌ 无法获取会话信息";
|
|
860
|
-
if (!repo) {
|
|
861
|
-
const lines = ["📋 可用事件类型:"];
|
|
862
|
-
for (const [type, info] of Object.entries(EVENT_DISPLAY_MAP)) {
|
|
863
|
-
lines.push(`${info.emoji} ${type} - ${info.name}`);
|
|
864
|
-
}
|
|
865
|
-
return lines.join("\n");
|
|
866
|
-
}
|
|
867
|
-
const sessionId = getSessionIdentifier(session);
|
|
868
|
-
const subscription = await getSubscription(ctx, sessionId, repo);
|
|
869
|
-
if (!subscription) {
|
|
870
|
-
return `❌ 未找到仓库 ${repo} 的订阅`;
|
|
871
|
-
}
|
|
872
|
-
if (!changes || changes.length === 0) {
|
|
873
|
-
const events = subscription.events.join(", ");
|
|
874
|
-
return `📋 ${repo} 订阅的事件:
|
|
875
|
-
${events}`;
|
|
876
|
-
}
|
|
877
|
-
const newEvents = parseEventChanges(changes, subscription.events);
|
|
878
|
-
const success = await updateEvents(ctx, sessionId, repo, newEvents);
|
|
879
|
-
if (success) {
|
|
880
|
-
const eventList = newEvents.join(", ");
|
|
881
|
-
return `✅ 已更新 ${repo} 的订阅事件:
|
|
882
|
-
${eventList}`;
|
|
883
|
-
}
|
|
884
|
-
return "❌ 更新失败";
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
__name(registerSubscriptionCommands, "registerSubscriptionCommands");
|
|
888
|
-
|
|
889
|
-
// src/commands/utils.ts
|
|
890
|
-
var PLUGIN_NAME = "github-webhook-pusher";
|
|
891
|
-
var ADMIN_AUTHORITY2 = 3;
|
|
892
|
-
var ALL_EVENT_TYPES2 = ["issues", "release", "push", "pull_request", "star"];
|
|
893
|
-
function generateTestEvent(repo, eventType) {
|
|
894
|
-
const baseEvent = {
|
|
895
|
-
type: eventType,
|
|
896
|
-
displayType: getDisplayType(eventType),
|
|
897
|
-
repo,
|
|
898
|
-
actor: "test-user",
|
|
899
|
-
url: `https://github.com/${repo}`
|
|
900
|
-
};
|
|
901
|
-
switch (eventType) {
|
|
902
|
-
case "issues":
|
|
903
|
-
return {
|
|
904
|
-
...baseEvent,
|
|
905
|
-
action: "opened",
|
|
906
|
-
title: "测试 Issue 标题",
|
|
907
|
-
number: 123
|
|
908
|
-
};
|
|
909
|
-
case "release":
|
|
910
|
-
return {
|
|
911
|
-
...baseEvent,
|
|
912
|
-
action: "published",
|
|
913
|
-
title: "v1.0.0",
|
|
914
|
-
tagName: "v1.0.0"
|
|
915
|
-
};
|
|
916
|
-
case "push":
|
|
917
|
-
return {
|
|
918
|
-
...baseEvent,
|
|
919
|
-
ref: "main",
|
|
920
|
-
commits: [
|
|
921
|
-
{ sha: "abc1234", message: "测试提交 1", author: "test-user", url: "" },
|
|
922
|
-
{ sha: "def5678", message: "测试提交 2", author: "test-user", url: "" }
|
|
923
|
-
],
|
|
924
|
-
totalCommits: 2
|
|
925
|
-
};
|
|
926
|
-
case "pull_request":
|
|
927
|
-
return {
|
|
928
|
-
...baseEvent,
|
|
929
|
-
action: "opened",
|
|
930
|
-
title: "测试 PR 标题",
|
|
931
|
-
number: 456
|
|
932
|
-
};
|
|
933
|
-
case "star":
|
|
934
|
-
return {
|
|
935
|
-
...baseEvent,
|
|
936
|
-
action: "created",
|
|
937
|
-
starCount: 1234
|
|
938
|
-
};
|
|
939
|
-
default:
|
|
940
|
-
return baseEvent;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
__name(generateTestEvent, "generateTestEvent");
|
|
944
|
-
function registerUtilCommands(ctx, config) {
|
|
945
|
-
ctx.command("gh.ping", "查看插件状态").usage("gh.ping").action(async () => {
|
|
946
|
-
const repos = await listTrustedRepos(ctx);
|
|
947
|
-
const enabledCount = repos.filter((r) => r.enabled).length;
|
|
948
|
-
const lines = [
|
|
949
|
-
"🏓 GitHub Webhook 推送插件",
|
|
950
|
-
`📦 插件名称: ${PLUGIN_NAME}`,
|
|
951
|
-
`🔗 Webhook 路径: ${config.path}`,
|
|
952
|
-
`📋 信任仓库: ${repos.length} 个 (${enabledCount} 个已启用)`,
|
|
953
|
-
`🔧 调试模式: ${config.debug ? "开启" : "关闭"}`
|
|
954
|
-
];
|
|
955
|
-
return lines.join("\n");
|
|
956
|
-
});
|
|
957
|
-
ctx.command("gh.test <repo:string> <event:string>", "生成测试消息").usage("gh.test owner/repo event").example("gh.test koishijs/koishi issues").example("gh.test koishijs/koishi push").action(async ({ session }, repo, event) => {
|
|
958
|
-
const user = session?.user;
|
|
959
|
-
if ((user?.authority ?? 0) < ADMIN_AUTHORITY2) {
|
|
960
|
-
return "❌ 权限不足,需要管理员权限";
|
|
961
|
-
}
|
|
962
|
-
if (!repo) {
|
|
963
|
-
return "❌ 请指定仓库名,格式: owner/repo";
|
|
964
|
-
}
|
|
965
|
-
if (!event) {
|
|
966
|
-
const eventList = ALL_EVENT_TYPES2.map((e) => {
|
|
967
|
-
const info = EVENT_DISPLAY_MAP[e];
|
|
968
|
-
return `${info.emoji} ${e}`;
|
|
969
|
-
}).join(", ");
|
|
970
|
-
return `❌ 请指定事件类型
|
|
971
|
-
可用类型: ${eventList}`;
|
|
972
|
-
}
|
|
973
|
-
if (!ALL_EVENT_TYPES2.includes(event)) {
|
|
974
|
-
const eventList = ALL_EVENT_TYPES2.join(", ");
|
|
975
|
-
return `❌ 不支持的事件类型: ${event}
|
|
976
|
-
可用类型: ${eventList}`;
|
|
977
|
-
}
|
|
978
|
-
const testEvent = generateTestEvent(repo, event);
|
|
979
|
-
const message = buildMessage(testEvent);
|
|
980
|
-
await session?.send(message);
|
|
981
|
-
return;
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
__name(registerUtilCommands, "registerUtilCommands");
|
|
985
|
-
|
|
986
|
-
// src/index.ts
|
|
987
|
-
var name = "github-webhook-pusher";
|
|
988
|
-
var inject = ["server", "database"];
|
|
989
|
-
var logger3 = new import_koishi5.Logger("github-webhook-pusher");
|
|
990
|
-
function apply(ctx, config) {
|
|
991
|
-
logger3.info(`正在加载 GitHub Webhook 插件...`);
|
|
992
|
-
extendDatabase(ctx);
|
|
993
|
-
logger3.debug("数据库模型已注册");
|
|
994
|
-
registerWebhook(ctx, config);
|
|
995
|
-
logger3.debug(`Webhook 处理器已注册: ${config.path}`);
|
|
996
|
-
registerTrustCommands(ctx);
|
|
997
|
-
logger3.debug("信任仓库管理命令已注册");
|
|
998
|
-
registerSubscriptionCommands(ctx, config);
|
|
999
|
-
logger3.debug("订阅管理命令已注册");
|
|
1000
|
-
registerUtilCommands(ctx, config);
|
|
1001
|
-
logger3.debug("工具命令已注册");
|
|
1002
|
-
logger3.info(`GitHub Webhook 插件已加载`);
|
|
1003
|
-
logger3.info(`Webhook 路径: ${config.path}`);
|
|
1004
|
-
logger3.info(`默认订阅事件: ${config.defaultEvents.join(", ")}`);
|
|
1005
|
-
if (config.debug) {
|
|
1006
|
-
logger3.info("调试模式已启用");
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
__name(apply, "apply");
|
|
1010
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
1011
|
-
0 && (module.exports = {
|
|
1012
|
-
Config,
|
|
1013
|
-
apply,
|
|
1014
|
-
inject,
|
|
1015
|
-
name
|
|
1016
|
-
});
|