@dsh-plugin/dsh-synapse 0.1.0-dev.41044228975 → 0.1.0

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.
Files changed (74) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +0 -14
  3. package/package.json +1 -1
  4. package/vendor/gugle-chat-bot/LICENSE +661 -661
  5. package/vendor/gugle-chat-bot/package.json +57 -57
  6. package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -9345
  7. package/vendor/gugle-chat-bot/src/api.json +4284 -4284
  8. package/vendor/gugle-chat-bot/src/command/index.ts +141 -141
  9. package/vendor/gugle-chat-bot/src/config/features.ts +547 -547
  10. package/vendor/gugle-chat-bot/src/config/index.ts +184 -184
  11. package/vendor/gugle-chat-bot/src/config/manager.ts +356 -356
  12. package/vendor/gugle-chat-bot/src/constants/index.ts +13 -13
  13. package/vendor/gugle-chat-bot/src/custom.ts +548 -548
  14. package/vendor/gugle-chat-bot/src/event/index.ts +118 -118
  15. package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -414
  16. package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -39
  17. package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -37
  18. package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -65
  19. package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -97
  20. package/vendor/gugle-chat-bot/src/features/bili/template/video.html +56 -56
  21. package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -1128
  22. package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -105
  23. package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -355
  24. package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -96
  25. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -14
  26. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -144
  27. package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -93
  28. package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -161
  29. package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -132
  30. package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -144
  31. package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -160
  32. package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -310
  33. package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -302
  34. package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -118
  35. package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -125
  36. package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -53
  37. package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -468
  38. package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +468 -468
  39. package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -412
  40. package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -184
  41. package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -210
  42. package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -67
  43. package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -86
  44. package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -1533
  45. package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -319
  46. package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -181
  47. package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -118
  48. package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -331
  49. package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -406
  50. package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -30
  51. package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -21
  52. package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -81
  53. package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -120
  54. package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -86
  55. package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -327
  56. package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -156
  57. package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -127
  58. package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -71
  59. package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -117
  60. package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +451 -451
  61. package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -24
  62. package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +77 -77
  63. package/vendor/gugle-chat-bot/src/features/welcome/index.ts +57 -57
  64. package/vendor/gugle-chat-bot/src/github-ip.json +5814 -5814
  65. package/vendor/gugle-chat-bot/src/image/index.ts +278 -278
  66. package/vendor/gugle-chat-bot/src/index.ts +695 -695
  67. package/vendor/gugle-chat-bot/src/logger/index.ts +54 -54
  68. package/vendor/gugle-chat-bot/src/result.json +359 -359
  69. package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -825
  70. package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -146
  71. package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -446
  72. package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -254
  73. package/vendor/gugle-chat-bot/tsconfig.json +46 -46
  74. package/scripts/dump-npm-latest.mjs +0 -30
@@ -1,302 +1,302 @@
1
- import { EventManager } from 'gugle-event';
2
- import { Logger } from 'winston';
3
- import http from 'node:http';
4
- import { QQBot } from '@/index';
5
- import { AllIssueEvent, AllPullRequestEvent, AllReleaseEvent, Issue, PullRequest } from '@/type/github';
6
- import { GroupMessageWSMSG, Message, SentMessage, TextMessage } from '@/type';
7
- import { GitHubImage } from '@/features/github/image';
8
- import { botConfig } from '@/config';
9
- import { getGithubSubscribers, isGithubEnabledGroup } from '@/config/features';
10
- import { GitHubBindingManager } from '@/features/github/binding';
11
- import { fetchGithubApi } from '@/features/github/api';
12
- import { shouldPushRelease } from '@/features/github/image/impl/release';
13
- import axios, { AxiosInstance } from 'axios';
14
-
15
- /** 一条 Issue/PR 引用(`owner/repo#123`,repo 省略时为默认仓库) */
16
- export interface IssueReference {
17
- repository: string;
18
- number: number;
19
- }
20
-
21
- export class Github {
22
- private readonly bot: QQBot;
23
- private readonly logger: Logger;
24
- private readonly eventManager: EventManager;
25
- private httpServer: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
26
-
27
- public static readonly axiosInstance: AxiosInstance = axios.create({
28
- timeout: 15000,
29
- baseURL: botConfig.httpUrl,
30
- headers: {
31
- 'Content-Type': 'application/json',
32
- 'User-Agent': botConfig.userAgent
33
- }
34
- });
35
-
36
- /**
37
- * 请求 GitHub API(经 ghapi.anvilcraft.dev 反代),供 processMessage 使用。
38
- * @param apiPath GitHub API 路径(不含域名),如 /repos/owner/repo/issues/123
39
- */
40
- public static fetchGithubApi<T>(bot: QQBot, apiPath: string): Promise<T> {
41
- return fetchGithubApi<T>(apiPath, bot.logger);
42
- }
43
-
44
- public constructor(bot: QQBot) {
45
- this.bot = bot;
46
- this.logger = bot.logger!;
47
- this.eventManager = new EventManager();
48
- this.eventManager.listen('github-issues', this.listenIssueEvent.bind(this));
49
- this.eventManager.listen('github-pull_request', this.listenPullRequestEvent.bind(this));
50
- this.eventManager.listen('github-release', this.listenReleaseEvent.bind(this));
51
- this.httpServer = http.createServer((req, res) => {
52
- if (req.method === 'POST') {
53
- let body = '';
54
- req.on('data', chunk => {
55
- body += chunk.toString(); // 将数据块拼接成字符串
56
- });
57
- req.on('end', () => {
58
- try {
59
- const event = (req.headers['x-github-event'] as string) || '';
60
- body = JSON.parse(body);
61
- bot.logger?.debug(`Receive Github Event: github-${event}`);
62
- this.post(`github-${event}`, bot, body);
63
- res.statusCode = 202;
64
- res.setHeader('Content-Type', 'text/plain');
65
- res.end('Accepted');
66
- } catch (e) {
67
- bot.logger?.error(e);
68
- }
69
- });
70
- return;
71
- }
72
- res.statusCode = 202;
73
- res.setHeader('Content-Type', 'text/plain');
74
- res.end('Accepted');
75
- });
76
- }
77
-
78
- private listenIssueEvent(bot: QQBot, msg: AllIssueEvent) {
79
- let promise: Promise<string> | undefined = undefined;
80
- if (msg.action === 'opened' || msg.action === 'reopened') {
81
- promise = GitHubImage.issuesOpened(msg, this.logger);
82
- } else if (msg.action === 'closed') {
83
- promise = GitHubImage.issuesClosed(msg, this.logger);
84
- }
85
- const repository = msg.repository.full_name;
86
- this.sendGeneratedImage(bot, repository, 'issue', promise);
87
- }
88
-
89
- private listenPullRequestEvent(bot: QQBot, msg: AllPullRequestEvent) {
90
- let promise: Promise<string> | undefined = undefined;
91
- if (msg.action === 'opened' || msg.action === 'reopened') {
92
- promise = GitHubImage.prOpened(msg, this.logger);
93
- } else if (msg.action === 'closed') {
94
- promise = GitHubImage.prClosed(msg, this.logger);
95
- }
96
- const repository = msg.repository.full_name;
97
- this.sendGeneratedImage(bot, repository, 'pull request', promise);
98
- }
99
-
100
- /**
101
- * 处理仓库发布 release 的 webhook 事件。
102
- *
103
- * GitHub 对同一次发布会投递多个 action(实测同一 release id 会在数秒内先后收到
104
- * created + released、published + created + prereleased 等组合,且组合不固定),
105
- * 因此按 action 过滤并不能避免重复,这里以 release id 在时间窗口内去重。
106
- */
107
- private listenReleaseEvent(bot: QQBot, msg: AllReleaseEvent) {
108
- const decision = shouldPushRelease(msg);
109
- if (!decision.push) {
110
- this.logger?.debug(`Skip release event ${msg.release?.id} (${msg.action}): ${decision.reason}`);
111
- return;
112
- }
113
- const repository = msg.repository.full_name;
114
- this.sendGeneratedImage(bot, repository, 'release', GitHubImage.releasePublished(msg, this.logger));
115
- }
116
-
117
- /**
118
- * 把生成的图片推送给订阅了该仓库的群(仓库级路由)。
119
- * 只发给 github.json 中 repository → 订阅群列表对应的群。
120
- */
121
- private sendGeneratedImage(bot: QQBot, repository: string, type: string, result: Promise<string> | undefined = undefined) {
122
- const subscribers = getGithubSubscribers(repository);
123
- if (subscribers.length === 0) {
124
- this.logger?.debug(`No subscribers for repository ${repository}, skip ${type} message...`);
125
- return;
126
- }
127
- result
128
- ?.then(base64 => {
129
- const msg: SentMessage = [
130
- {
131
- type: 'image',
132
- data: {
133
- file: `data:image/png;base64, ${base64}`
134
- }
135
- }
136
- ];
137
- subscribers.forEach(group => {
138
- // fromWebhook:标记为 webhook 推送,互通桥据此不转发到 need_cmd/need_reply 频道
139
- bot.sendGroupMsg(group, msg, { fromWebhook: true });
140
- });
141
- this.logger?.debug(`Sent process ${type} message of ${repository} to ${subscribers.length} group(s)...`);
142
- })
143
- .catch(e => {
144
- this.logger?.error(e);
145
- });
146
- }
147
-
148
- public post(event: string, ...args: any): Promise<any[]> {
149
- return this.eventManager.post(event, ...args);
150
- }
151
-
152
- public start(port: number): void {
153
- const self = this;
154
- this.httpServer.listen(port, () => {
155
- self.logger?.info(`http server listen on port ${port}`);
156
- });
157
- }
158
-
159
- /**
160
- * 判断仓库是否允许被访问:owner 已绑定 GitHub 账号,或命中允许列表配置。
161
- * 消息查询与订阅共用该判定,保证「能查」与「能订阅」口径一致。
162
- */
163
- public static isAllowedRepository(repository: string): boolean {
164
- const [owner] = repository.split('/');
165
- if (owner && GitHubBindingManager.isBoundUsername(owner)) return true;
166
-
167
- return botConfig.githubAllowedRepositories.some(pattern => {
168
- if (pattern.endsWith('/*')) return repository.startsWith(`${pattern.slice(0, -2)}/`);
169
- return repository === pattern;
170
- });
171
- }
172
-
173
- /** 单条消息最多解析的 Issue/PR 引用数(防止刷屏,超出部分忽略) */
174
- public static readonly MAX_REFERENCES_PER_MESSAGE = 10;
175
-
176
- /**
177
- * 解析文本中的全部 Issue/PR 引用(`owner/repo#123` 或 `#123`)。
178
- * 按出现顺序去重(`repository#number`),最多返回 MAX_REFERENCES_PER_MESSAGE 条。
179
- */
180
- public static parseReferences(text: string, defaultRepository: string = 'Anvil-Dev/AnvilCraft'): IssueReference[] {
181
- const pattern = /(?:([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+))?#(\d+)/g;
182
- const references: IssueReference[] = [];
183
- const seen = new Set<string>();
184
- let match: RegExpExecArray | null;
185
- while ((match = pattern.exec(text)) !== null) {
186
- const repository = match[1] || defaultRepository;
187
- const number = Number.parseInt(match[2]!, 10);
188
- const key = `${repository}#${number}`;
189
- if (seen.has(key)) continue;
190
- seen.add(key);
191
- references.push({ repository, number });
192
- if (references.length >= Github.MAX_REFERENCES_PER_MESSAGE) break;
193
- }
194
- return references;
195
- }
196
-
197
- /**
198
- * 渲染单个 Issue/PR 引用为 QQ 消息段:成功为图片段,失败为文本说明段。
199
- * 仓库不在允许列表时返回提示文本(不发请求)。
200
- */
201
- public static async renderReference(bot: QQBot, reference: IssueReference): Promise<Message> {
202
- const { repository, number } = reference;
203
- if (!this.isAllowedRepository(repository)) {
204
- return {
205
- type: 'text',
206
- data: {
207
- text: `仓库 ${repository} 不在允许访问的仓库列表中`
208
- }
209
- };
210
- }
211
-
212
- const issueApiPath = `/repos/${repository}/issues/${number}`;
213
- const pullApiPath = `/repos/${repository}/pulls/${number}`;
214
-
215
- // 经 ghapi 反代获取 issue 数据
216
- let issueData: Issue & { pull_request?: unknown };
217
- try {
218
- issueData = await Github.fetchGithubApi<Issue & { pull_request?: unknown }>(bot, issueApiPath);
219
- } catch (e) {
220
- bot.logger?.error(e);
221
- return {
222
- type: 'text',
223
- data: {
224
- text: `请求失败,原因:${(e as Error)?.message ?? e}`
225
- }
226
- };
227
- }
228
-
229
- try {
230
- if (issueData.pull_request) {
231
- // 如果是 PR,经 ghapi 反代获取 PR 数据
232
- const prData = await Github.fetchGithubApi<PullRequest>(bot, pullApiPath);
233
- const imageData = await GitHubImage.prHandler(prData, bot.logger);
234
- return {
235
- type: 'image',
236
- data: {
237
- file: `data:image/png;base64, ${imageData}`
238
- }
239
- };
240
- }
241
- // 处理 issue
242
- const imageData = await GitHubImage.issuesHandler(issueData as Issue, bot.logger);
243
- return {
244
- type: 'image',
245
- data: {
246
- file: `data:image/png;base64, ${imageData}`
247
- }
248
- };
249
- } catch (e) {
250
- bot.logger?.error(e);
251
- return {
252
- type: 'text',
253
- data: {
254
- text: `图片处理失败,原因:${(e as Error)?.message ?? e}`
255
- }
256
- };
257
- }
258
- }
259
-
260
- /**
261
- * QQ 群消息入口:解析消息中的全部 Issue/PR 引用并逐个渲染。
262
- * 第一个引用的结果并入 sentMessage(随调用方的回复消息一起发出);
263
- * 其余引用各自作为独立群消息发送(用户要求多编号时分多条消息发送)。
264
- * 渲染串行执行以保证消息顺序与编号顺序一致。
265
- */
266
- public static async processMessage(bot: QQBot, msg: GroupMessageWSMSG, sentMessage: Message[]): Promise<void> {
267
- if (!isGithubEnabledGroup(msg.group_id)) return;
268
-
269
- const receivedMessage: TextMessage[] = [];
270
- msg.message.forEach(message => {
271
- if (message.type != 'text') return;
272
- receivedMessage.push(message);
273
- });
274
-
275
- const strMsg = receivedMessage.map(msg => msg.data.text).join(' ');
276
- const references = Github.parseReferences(strMsg);
277
- if (references.length === 0) return;
278
-
279
- for (let index = 0; index < references.length; index++) {
280
- const rendered = await Github.renderReference(bot, references[index]!);
281
- if (index === 0) {
282
- sentMessage.push(rendered);
283
- } else {
284
- await bot.sendGroupMsg(msg.group_id, [rendered]);
285
- }
286
- }
287
- }
288
-
289
- /**
290
- * Discord 侧入口:解析文本中的全部 Issue/PR 引用并渲染为消息段列表(由调用方发送)。
291
- * groupId 为 Discord 频道桥接的 QQ 群号,用于复用「该群是否启用 github 功能」的判定。
292
- */
293
- public static async processDiscordMessage(bot: QQBot, text: string, groupId?: number): Promise<Message[]> {
294
- if (groupId !== undefined && !isGithubEnabledGroup(groupId)) return [];
295
- const references = Github.parseReferences(text);
296
- const results: Message[] = [];
297
- for (const reference of references) {
298
- results.push(await Github.renderReference(bot, reference));
299
- }
300
- return results;
301
- }
302
- }
1
+ import { EventManager } from 'gugle-event';
2
+ import { Logger } from 'winston';
3
+ import http from 'node:http';
4
+ import { QQBot } from '@/index';
5
+ import { AllIssueEvent, AllPullRequestEvent, AllReleaseEvent, Issue, PullRequest } from '@/type/github';
6
+ import { GroupMessageWSMSG, Message, SentMessage, TextMessage } from '@/type';
7
+ import { GitHubImage } from '@/features/github/image';
8
+ import { botConfig } from '@/config';
9
+ import { getGithubSubscribers, isGithubEnabledGroup } from '@/config/features';
10
+ import { GitHubBindingManager } from '@/features/github/binding';
11
+ import { fetchGithubApi } from '@/features/github/api';
12
+ import { shouldPushRelease } from '@/features/github/image/impl/release';
13
+ import axios, { AxiosInstance } from 'axios';
14
+
15
+ /** 一条 Issue/PR 引用(`owner/repo#123`,repo 省略时为默认仓库) */
16
+ export interface IssueReference {
17
+ repository: string;
18
+ number: number;
19
+ }
20
+
21
+ export class Github {
22
+ private readonly bot: QQBot;
23
+ private readonly logger: Logger;
24
+ private readonly eventManager: EventManager;
25
+ private httpServer: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
26
+
27
+ public static readonly axiosInstance: AxiosInstance = axios.create({
28
+ timeout: 15000,
29
+ baseURL: botConfig.httpUrl,
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ 'User-Agent': botConfig.userAgent
33
+ }
34
+ });
35
+
36
+ /**
37
+ * 请求 GitHub API(经 ghapi.anvilcraft.dev 反代),供 processMessage 使用。
38
+ * @param apiPath GitHub API 路径(不含域名),如 /repos/owner/repo/issues/123
39
+ */
40
+ public static fetchGithubApi<T>(bot: QQBot, apiPath: string): Promise<T> {
41
+ return fetchGithubApi<T>(apiPath, bot.logger);
42
+ }
43
+
44
+ public constructor(bot: QQBot) {
45
+ this.bot = bot;
46
+ this.logger = bot.logger!;
47
+ this.eventManager = new EventManager();
48
+ this.eventManager.listen('github-issues', this.listenIssueEvent.bind(this));
49
+ this.eventManager.listen('github-pull_request', this.listenPullRequestEvent.bind(this));
50
+ this.eventManager.listen('github-release', this.listenReleaseEvent.bind(this));
51
+ this.httpServer = http.createServer((req, res) => {
52
+ if (req.method === 'POST') {
53
+ let body = '';
54
+ req.on('data', chunk => {
55
+ body += chunk.toString(); // 将数据块拼接成字符串
56
+ });
57
+ req.on('end', () => {
58
+ try {
59
+ const event = (req.headers['x-github-event'] as string) || '';
60
+ body = JSON.parse(body);
61
+ bot.logger?.debug(`Receive Github Event: github-${event}`);
62
+ this.post(`github-${event}`, bot, body);
63
+ res.statusCode = 202;
64
+ res.setHeader('Content-Type', 'text/plain');
65
+ res.end('Accepted');
66
+ } catch (e) {
67
+ bot.logger?.error(e);
68
+ }
69
+ });
70
+ return;
71
+ }
72
+ res.statusCode = 202;
73
+ res.setHeader('Content-Type', 'text/plain');
74
+ res.end('Accepted');
75
+ });
76
+ }
77
+
78
+ private listenIssueEvent(bot: QQBot, msg: AllIssueEvent) {
79
+ let promise: Promise<string> | undefined = undefined;
80
+ if (msg.action === 'opened' || msg.action === 'reopened') {
81
+ promise = GitHubImage.issuesOpened(msg, this.logger);
82
+ } else if (msg.action === 'closed') {
83
+ promise = GitHubImage.issuesClosed(msg, this.logger);
84
+ }
85
+ const repository = msg.repository.full_name;
86
+ this.sendGeneratedImage(bot, repository, 'issue', promise);
87
+ }
88
+
89
+ private listenPullRequestEvent(bot: QQBot, msg: AllPullRequestEvent) {
90
+ let promise: Promise<string> | undefined = undefined;
91
+ if (msg.action === 'opened' || msg.action === 'reopened') {
92
+ promise = GitHubImage.prOpened(msg, this.logger);
93
+ } else if (msg.action === 'closed') {
94
+ promise = GitHubImage.prClosed(msg, this.logger);
95
+ }
96
+ const repository = msg.repository.full_name;
97
+ this.sendGeneratedImage(bot, repository, 'pull request', promise);
98
+ }
99
+
100
+ /**
101
+ * 处理仓库发布 release 的 webhook 事件。
102
+ *
103
+ * GitHub 对同一次发布会投递多个 action(实测同一 release id 会在数秒内先后收到
104
+ * created + released、published + created + prereleased 等组合,且组合不固定),
105
+ * 因此按 action 过滤并不能避免重复,这里以 release id 在时间窗口内去重。
106
+ */
107
+ private listenReleaseEvent(bot: QQBot, msg: AllReleaseEvent) {
108
+ const decision = shouldPushRelease(msg);
109
+ if (!decision.push) {
110
+ this.logger?.debug(`Skip release event ${msg.release?.id} (${msg.action}): ${decision.reason}`);
111
+ return;
112
+ }
113
+ const repository = msg.repository.full_name;
114
+ this.sendGeneratedImage(bot, repository, 'release', GitHubImage.releasePublished(msg, this.logger));
115
+ }
116
+
117
+ /**
118
+ * 把生成的图片推送给订阅了该仓库的群(仓库级路由)。
119
+ * 只发给 github.json 中 repository → 订阅群列表对应的群。
120
+ */
121
+ private sendGeneratedImage(bot: QQBot, repository: string, type: string, result: Promise<string> | undefined = undefined) {
122
+ const subscribers = getGithubSubscribers(repository);
123
+ if (subscribers.length === 0) {
124
+ this.logger?.debug(`No subscribers for repository ${repository}, skip ${type} message...`);
125
+ return;
126
+ }
127
+ result
128
+ ?.then(base64 => {
129
+ const msg: SentMessage = [
130
+ {
131
+ type: 'image',
132
+ data: {
133
+ file: `data:image/png;base64, ${base64}`
134
+ }
135
+ }
136
+ ];
137
+ subscribers.forEach(group => {
138
+ // fromWebhook:标记为 webhook 推送,互通桥据此不转发到 need_cmd/need_reply 频道
139
+ bot.sendGroupMsg(group, msg, { fromWebhook: true });
140
+ });
141
+ this.logger?.debug(`Sent process ${type} message of ${repository} to ${subscribers.length} group(s)...`);
142
+ })
143
+ .catch(e => {
144
+ this.logger?.error(e);
145
+ });
146
+ }
147
+
148
+ public post(event: string, ...args: any): Promise<any[]> {
149
+ return this.eventManager.post(event, ...args);
150
+ }
151
+
152
+ public start(port: number): void {
153
+ const self = this;
154
+ this.httpServer.listen(port, () => {
155
+ self.logger?.info(`http server listen on port ${port}`);
156
+ });
157
+ }
158
+
159
+ /**
160
+ * 判断仓库是否允许被访问:owner 已绑定 GitHub 账号,或命中允许列表配置。
161
+ * 消息查询与订阅共用该判定,保证「能查」与「能订阅」口径一致。
162
+ */
163
+ public static isAllowedRepository(repository: string): boolean {
164
+ const [owner] = repository.split('/');
165
+ if (owner && GitHubBindingManager.isBoundUsername(owner)) return true;
166
+
167
+ return botConfig.githubAllowedRepositories.some(pattern => {
168
+ if (pattern.endsWith('/*')) return repository.startsWith(`${pattern.slice(0, -2)}/`);
169
+ return repository === pattern;
170
+ });
171
+ }
172
+
173
+ /** 单条消息最多解析的 Issue/PR 引用数(防止刷屏,超出部分忽略) */
174
+ public static readonly MAX_REFERENCES_PER_MESSAGE = 10;
175
+
176
+ /**
177
+ * 解析文本中的全部 Issue/PR 引用(`owner/repo#123` 或 `#123`)。
178
+ * 按出现顺序去重(`repository#number`),最多返回 MAX_REFERENCES_PER_MESSAGE 条。
179
+ */
180
+ public static parseReferences(text: string, defaultRepository: string = 'Anvil-Dev/AnvilCraft'): IssueReference[] {
181
+ const pattern = /(?:([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+))?#(\d+)/g;
182
+ const references: IssueReference[] = [];
183
+ const seen = new Set<string>();
184
+ let match: RegExpExecArray | null;
185
+ while ((match = pattern.exec(text)) !== null) {
186
+ const repository = match[1] || defaultRepository;
187
+ const number = Number.parseInt(match[2]!, 10);
188
+ const key = `${repository}#${number}`;
189
+ if (seen.has(key)) continue;
190
+ seen.add(key);
191
+ references.push({ repository, number });
192
+ if (references.length >= Github.MAX_REFERENCES_PER_MESSAGE) break;
193
+ }
194
+ return references;
195
+ }
196
+
197
+ /**
198
+ * 渲染单个 Issue/PR 引用为 QQ 消息段:成功为图片段,失败为文本说明段。
199
+ * 仓库不在允许列表时返回提示文本(不发请求)。
200
+ */
201
+ public static async renderReference(bot: QQBot, reference: IssueReference): Promise<Message> {
202
+ const { repository, number } = reference;
203
+ if (!this.isAllowedRepository(repository)) {
204
+ return {
205
+ type: 'text',
206
+ data: {
207
+ text: `仓库 ${repository} 不在允许访问的仓库列表中`
208
+ }
209
+ };
210
+ }
211
+
212
+ const issueApiPath = `/repos/${repository}/issues/${number}`;
213
+ const pullApiPath = `/repos/${repository}/pulls/${number}`;
214
+
215
+ // 经 ghapi 反代获取 issue 数据
216
+ let issueData: Issue & { pull_request?: unknown };
217
+ try {
218
+ issueData = await Github.fetchGithubApi<Issue & { pull_request?: unknown }>(bot, issueApiPath);
219
+ } catch (e) {
220
+ bot.logger?.error(e);
221
+ return {
222
+ type: 'text',
223
+ data: {
224
+ text: `请求失败,原因:${(e as Error)?.message ?? e}`
225
+ }
226
+ };
227
+ }
228
+
229
+ try {
230
+ if (issueData.pull_request) {
231
+ // 如果是 PR,经 ghapi 反代获取 PR 数据
232
+ const prData = await Github.fetchGithubApi<PullRequest>(bot, pullApiPath);
233
+ const imageData = await GitHubImage.prHandler(prData, bot.logger);
234
+ return {
235
+ type: 'image',
236
+ data: {
237
+ file: `data:image/png;base64, ${imageData}`
238
+ }
239
+ };
240
+ }
241
+ // 处理 issue
242
+ const imageData = await GitHubImage.issuesHandler(issueData as Issue, bot.logger);
243
+ return {
244
+ type: 'image',
245
+ data: {
246
+ file: `data:image/png;base64, ${imageData}`
247
+ }
248
+ };
249
+ } catch (e) {
250
+ bot.logger?.error(e);
251
+ return {
252
+ type: 'text',
253
+ data: {
254
+ text: `图片处理失败,原因:${(e as Error)?.message ?? e}`
255
+ }
256
+ };
257
+ }
258
+ }
259
+
260
+ /**
261
+ * QQ 群消息入口:解析消息中的全部 Issue/PR 引用并逐个渲染。
262
+ * 第一个引用的结果并入 sentMessage(随调用方的回复消息一起发出);
263
+ * 其余引用各自作为独立群消息发送(用户要求多编号时分多条消息发送)。
264
+ * 渲染串行执行以保证消息顺序与编号顺序一致。
265
+ */
266
+ public static async processMessage(bot: QQBot, msg: GroupMessageWSMSG, sentMessage: Message[]): Promise<void> {
267
+ if (!isGithubEnabledGroup(msg.group_id)) return;
268
+
269
+ const receivedMessage: TextMessage[] = [];
270
+ msg.message.forEach(message => {
271
+ if (message.type != 'text') return;
272
+ receivedMessage.push(message);
273
+ });
274
+
275
+ const strMsg = receivedMessage.map(msg => msg.data.text).join(' ');
276
+ const references = Github.parseReferences(strMsg);
277
+ if (references.length === 0) return;
278
+
279
+ for (let index = 0; index < references.length; index++) {
280
+ const rendered = await Github.renderReference(bot, references[index]!);
281
+ if (index === 0) {
282
+ sentMessage.push(rendered);
283
+ } else {
284
+ await bot.sendGroupMsg(msg.group_id, [rendered]);
285
+ }
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Discord 侧入口:解析文本中的全部 Issue/PR 引用并渲染为消息段列表(由调用方发送)。
291
+ * groupId 为 Discord 频道桥接的 QQ 群号,用于复用「该群是否启用 github 功能」的判定。
292
+ */
293
+ public static async processDiscordMessage(bot: QQBot, text: string, groupId?: number): Promise<Message[]> {
294
+ if (groupId !== undefined && !isGithubEnabledGroup(groupId)) return [];
295
+ const references = Github.parseReferences(text);
296
+ const results: Message[] = [];
297
+ for (const reference of references) {
298
+ results.push(await Github.renderReference(bot, reference));
299
+ }
300
+ return results;
301
+ }
302
+ }