koishi-plugin-githubsth 1.0.3 → 1.0.4
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/commands/index.js +4 -9
- package/lib/commands/render.d.ts +3 -0
- package/lib/commands/render.js +55 -0
- package/lib/config.d.ts +8 -0
- package/lib/config.js +16 -0
- package/lib/database.js +1 -0
- package/lib/index.js +1 -1
- package/lib/services/formatter.d.ts +11 -11
- package/lib/services/formatter.js +1 -5
- package/lib/services/notifier.d.ts +21 -2
- package/lib/services/notifier.js +213 -47
- package/package.json +1 -1
package/lib/commands/index.js
CHANGED
|
@@ -37,17 +37,12 @@ exports.apply = apply;
|
|
|
37
37
|
const repo = __importStar(require("./repo"));
|
|
38
38
|
const admin = __importStar(require("./admin"));
|
|
39
39
|
const subscribe = __importStar(require("./subscribe"));
|
|
40
|
+
const render = __importStar(require("./render"));
|
|
40
41
|
function apply(ctx, config) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ctx.command('githubsth', 'GitHub 推送通知')
|
|
44
|
-
.action(({ session }) => {
|
|
45
|
-
session?.execute('help githubsth');
|
|
46
|
-
});
|
|
42
|
+
ctx.command('githubsth', 'GitHub 订阅通知')
|
|
43
|
+
.action(({ session }) => session?.execute('help githubsth'));
|
|
47
44
|
ctx.plugin(repo, config);
|
|
48
|
-
console.log('githubsth.repo loaded');
|
|
49
45
|
ctx.plugin(admin, config);
|
|
50
|
-
console.log('githubsth.admin loaded');
|
|
51
46
|
ctx.plugin(subscribe, config);
|
|
52
|
-
|
|
47
|
+
ctx.plugin(render, config);
|
|
53
48
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apply = apply;
|
|
4
|
+
const modes = ['text', 'image', 'auto'];
|
|
5
|
+
const themes = ['compact', 'card', 'terminal'];
|
|
6
|
+
const events = ['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'star', 'fork', 'release', 'discussion', 'workflow_run'];
|
|
7
|
+
function apply(ctx, config) {
|
|
8
|
+
ctx.command('githubsth.render', '通知渲染设置(文本/图片)', { authority: 3 })
|
|
9
|
+
.alias('gh.render');
|
|
10
|
+
ctx.command('githubsth.render.status', '查看渲染状态', { authority: 3 })
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const status = ctx.githubsthNotifier.getRenderStatus();
|
|
13
|
+
return [
|
|
14
|
+
`mode: ${status.mode} (configured: ${status.configuredMode})`,
|
|
15
|
+
`fallback: ${status.fallback}`,
|
|
16
|
+
`theme: ${status.theme}`,
|
|
17
|
+
`width: ${status.width}`,
|
|
18
|
+
`timeout: ${status.timeoutMs}ms`,
|
|
19
|
+
`puppeteer: ${status.hasPuppeteer ? 'ready' : 'missing'}`,
|
|
20
|
+
].join('\n');
|
|
21
|
+
});
|
|
22
|
+
ctx.command('githubsth.render.mode <mode:string>', '切换渲染模式(text/image/auto)', { authority: 3 })
|
|
23
|
+
.action(async (_, mode) => {
|
|
24
|
+
if (!mode || !modes.includes(mode)) {
|
|
25
|
+
return `无效模式。可选:${modes.join(', ')}`;
|
|
26
|
+
}
|
|
27
|
+
ctx.githubsthNotifier.setRenderMode(mode);
|
|
28
|
+
return `已切换运行时渲染模式为 ${mode}(重启后恢复配置值 ${config.renderMode})。`;
|
|
29
|
+
});
|
|
30
|
+
ctx.command('githubsth.render.theme <theme:string>', '切换图片主题(compact/card/terminal)', { authority: 3 })
|
|
31
|
+
.action(async (_, theme) => {
|
|
32
|
+
if (!theme || !themes.includes(theme)) {
|
|
33
|
+
return `无效主题。可选:${themes.join(', ')}`;
|
|
34
|
+
}
|
|
35
|
+
config.renderTheme = theme;
|
|
36
|
+
return `已切换图片主题为 ${theme}(当前进程生效)。`;
|
|
37
|
+
});
|
|
38
|
+
ctx.command('githubsth.render.width <width:number>', '设置图片宽度', { authority: 3 })
|
|
39
|
+
.action(async (_, width) => {
|
|
40
|
+
if (!width || Number.isNaN(width))
|
|
41
|
+
return '请提供有效的数字宽度。';
|
|
42
|
+
const normalized = Math.max(480, Math.min(1600, Math.floor(width)));
|
|
43
|
+
config.renderWidth = normalized;
|
|
44
|
+
return `已设置图片宽度为 ${normalized}px(当前进程生效)。`;
|
|
45
|
+
});
|
|
46
|
+
ctx.command('githubsth.render.preview [event:string]', '预览通知渲染', { authority: 3 })
|
|
47
|
+
.action(async ({ session }, event) => {
|
|
48
|
+
const selected = event && events.includes(event) ? event : 'issue_comment';
|
|
49
|
+
if (event && !events.includes(event)) {
|
|
50
|
+
await session?.send(`未知事件 ${event},已改用默认事件 issue_comment。`);
|
|
51
|
+
}
|
|
52
|
+
const preview = await ctx.githubsthNotifier.renderPreview(selected);
|
|
53
|
+
return preview || '预览失败:请检查 puppeteer 或渲染配置。';
|
|
54
|
+
});
|
|
55
|
+
}
|
package/lib/config.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export interface Rule {
|
|
|
5
5
|
platform?: string;
|
|
6
6
|
events: string[];
|
|
7
7
|
}
|
|
8
|
+
export type RenderMode = 'text' | 'image' | 'auto';
|
|
9
|
+
export type RenderFallback = 'text' | 'drop';
|
|
10
|
+
export type RenderTheme = 'compact' | 'card' | 'terminal';
|
|
8
11
|
export interface Config {
|
|
9
12
|
defaultOwner?: string;
|
|
10
13
|
defaultRepo?: string;
|
|
@@ -16,6 +19,11 @@ export interface Config {
|
|
|
16
19
|
sendRetryCount: number;
|
|
17
20
|
sendRetryBaseDelayMs: number;
|
|
18
21
|
formatterLocale: 'zh-CN' | 'en-US';
|
|
22
|
+
renderMode: RenderMode;
|
|
23
|
+
renderFallback: RenderFallback;
|
|
24
|
+
renderTheme: RenderTheme;
|
|
25
|
+
renderWidth: number;
|
|
26
|
+
renderTimeoutMs: number;
|
|
19
27
|
rules?: Rule[];
|
|
20
28
|
}
|
|
21
29
|
export declare const Config: Schema<Config>;
|
package/lib/config.js
CHANGED
|
@@ -15,6 +15,22 @@ exports.Config = koishi_1.Schema.object({
|
|
|
15
15
|
koishi_1.Schema.const('zh-CN').description('中文'),
|
|
16
16
|
koishi_1.Schema.const('en-US').description('English'),
|
|
17
17
|
]).default('zh-CN').description('通知文本语言。'),
|
|
18
|
+
renderMode: koishi_1.Schema.union([
|
|
19
|
+
koishi_1.Schema.const('auto').description('自动:优先图片,失败回退'),
|
|
20
|
+
koishi_1.Schema.const('image').description('仅图片'),
|
|
21
|
+
koishi_1.Schema.const('text').description('仅文本'),
|
|
22
|
+
]).default('auto').description('通知渲染模式。'),
|
|
23
|
+
renderFallback: koishi_1.Schema.union([
|
|
24
|
+
koishi_1.Schema.const('text').description('图片失败回退文本'),
|
|
25
|
+
koishi_1.Schema.const('drop').description('图片失败则丢弃'),
|
|
26
|
+
]).default('text').description('图片渲染失败时的回退策略。'),
|
|
27
|
+
renderTheme: koishi_1.Schema.union([
|
|
28
|
+
koishi_1.Schema.const('compact').description('紧凑样式'),
|
|
29
|
+
koishi_1.Schema.const('card').description('卡片样式'),
|
|
30
|
+
koishi_1.Schema.const('terminal').description('终端样式'),
|
|
31
|
+
]).default('compact').description('图片通知主题。'),
|
|
32
|
+
renderWidth: koishi_1.Schema.number().min(480).max(1600).default(840).description('图片宽度(像素)。'),
|
|
33
|
+
renderTimeoutMs: koishi_1.Schema.number().min(1000).max(60000).default(12000).description('单次图片渲染超时(毫秒)。'),
|
|
18
34
|
defaultEvents: koishi_1.Schema.array(koishi_1.Schema.string())
|
|
19
35
|
.default(['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'release', 'star', 'fork'])
|
|
20
36
|
.description('未显式指定时使用的默认订阅事件列表。'),
|
package/lib/database.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -50,7 +50,7 @@ const formatter_1 = require("./services/formatter");
|
|
|
50
50
|
exports.name = 'githubsth';
|
|
51
51
|
exports.inject = {
|
|
52
52
|
required: ['database'],
|
|
53
|
-
optional: ['github'],
|
|
53
|
+
optional: ['github', 'puppeteer'],
|
|
54
54
|
};
|
|
55
55
|
__exportStar(require("./config"), exports);
|
|
56
56
|
function apply(ctx, config) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Service
|
|
1
|
+
import { Context, Service } from 'koishi';
|
|
2
2
|
import type { Config } from '../config';
|
|
3
3
|
declare module 'koishi' {
|
|
4
4
|
interface Context {
|
|
@@ -8,16 +8,16 @@ declare module 'koishi' {
|
|
|
8
8
|
export declare class Formatter extends Service {
|
|
9
9
|
private readonly locale;
|
|
10
10
|
constructor(ctx: Context, config?: Partial<Config>);
|
|
11
|
-
formatPush(payload: any):
|
|
12
|
-
formatIssue(payload: any):
|
|
13
|
-
formatPullRequest(payload: any):
|
|
14
|
-
formatStar(payload: any):
|
|
15
|
-
formatFork(payload: any):
|
|
16
|
-
formatRelease(payload: any):
|
|
17
|
-
formatDiscussion(payload: any):
|
|
18
|
-
formatWorkflowRun(payload: any):
|
|
19
|
-
formatIssueComment(payload: any):
|
|
20
|
-
formatPullRequestReview(payload: any):
|
|
11
|
+
formatPush(payload: any): string | null;
|
|
12
|
+
formatIssue(payload: any): string;
|
|
13
|
+
formatPullRequest(payload: any): string;
|
|
14
|
+
formatStar(payload: any): string | null;
|
|
15
|
+
formatFork(payload: any): string;
|
|
16
|
+
formatRelease(payload: any): string | null;
|
|
17
|
+
formatDiscussion(payload: any): string;
|
|
18
|
+
formatWorkflowRun(payload: any): string | null;
|
|
19
|
+
formatIssueComment(payload: any): string | null;
|
|
20
|
+
formatPullRequestReview(payload: any): string | null;
|
|
21
21
|
private summarizeCommentBody;
|
|
22
22
|
private mapAction;
|
|
23
23
|
private t;
|
|
@@ -10,7 +10,6 @@ const I18N = {
|
|
|
10
10
|
unknownState: '未知状态',
|
|
11
11
|
unknownBranch: '未知分支',
|
|
12
12
|
unknownResult: '未知结果',
|
|
13
|
-
noDescription: '无描述',
|
|
14
13
|
emptyComment: '(空评论)',
|
|
15
14
|
hiddenMachinePayload: '检测到自动化系统签名/编码载荷,已省略原始内容。',
|
|
16
15
|
pushTitle: '🚀 [代码推送]',
|
|
@@ -71,7 +70,6 @@ const I18N = {
|
|
|
71
70
|
unknownState: 'Unknown state',
|
|
72
71
|
unknownBranch: 'Unknown branch',
|
|
73
72
|
unknownResult: 'Unknown result',
|
|
74
|
-
noDescription: 'No description',
|
|
75
73
|
emptyComment: '(empty comment)',
|
|
76
74
|
hiddenMachinePayload: 'Detected automated signature/encoded payload. Raw content hidden.',
|
|
77
75
|
pushTitle: '🚀 [Push Event]',
|
|
@@ -280,11 +278,9 @@ class Formatter extends koishi_1.Service {
|
|
|
280
278
|
const content = String(raw || '').replace(/\r/g, '').trim();
|
|
281
279
|
if (!content)
|
|
282
280
|
return this.t('emptyComment');
|
|
283
|
-
// Typical Vercel/GitHub app machine signature payload: [vc]: #<token>:<payload>
|
|
284
281
|
if (/^\[vc\]:\s*#[A-Za-z0-9+/=_:-]+/i.test(content)) {
|
|
285
282
|
return this.t('hiddenMachinePayload');
|
|
286
283
|
}
|
|
287
|
-
// Very long high-entropy lines are usually bot payloads/signatures.
|
|
288
284
|
const oneLine = content.replace(/\n+/g, ' ');
|
|
289
285
|
const looksEncoded = oneLine.length > 120 && !/\s/.test(oneLine.slice(0, 80));
|
|
290
286
|
if (looksEncoded)
|
|
@@ -301,7 +297,7 @@ class Formatter extends koishi_1.Service {
|
|
|
301
297
|
return template.replace(/\{(\d+)\}/g, (_, idx) => String(params[Number(idx)] ?? ''));
|
|
302
298
|
}
|
|
303
299
|
render(lines) {
|
|
304
|
-
return
|
|
300
|
+
return `${lines.join('\n')}\n`;
|
|
305
301
|
}
|
|
306
302
|
}
|
|
307
303
|
exports.Formatter = Formatter;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Context, Service } from 'koishi';
|
|
2
|
-
import { Config } from '../config';
|
|
1
|
+
import { Context, Service, h } from 'koishi';
|
|
2
|
+
import { Config, RenderMode } from '../config';
|
|
3
3
|
declare module 'koishi' {
|
|
4
4
|
interface Context {
|
|
5
5
|
githubsthNotifier: Notifier;
|
|
@@ -11,9 +11,27 @@ export declare class Notifier extends Service {
|
|
|
11
11
|
private readonly recentEventKeys;
|
|
12
12
|
private readonly memoryDedupWindowMs;
|
|
13
13
|
private dedupWriteCounter;
|
|
14
|
+
private runtimeRenderMode;
|
|
14
15
|
constructor(ctx: Context, config: Config);
|
|
16
|
+
setRenderMode(mode: RenderMode): void;
|
|
17
|
+
getRenderMode(): RenderMode;
|
|
18
|
+
getRenderStatus(): {
|
|
19
|
+
mode: RenderMode;
|
|
20
|
+
configuredMode: RenderMode;
|
|
21
|
+
fallback: import("../config").RenderFallback;
|
|
22
|
+
theme: import("../config").RenderTheme;
|
|
23
|
+
width: number;
|
|
24
|
+
timeoutMs: number;
|
|
25
|
+
hasPuppeteer: boolean;
|
|
26
|
+
};
|
|
27
|
+
renderPreview(event?: string): Promise<string | h | null>;
|
|
15
28
|
private registerListeners;
|
|
16
29
|
private handleEvent;
|
|
30
|
+
private formatByEvent;
|
|
31
|
+
private prepareOutboundMessage;
|
|
32
|
+
private renderTextAsImage;
|
|
33
|
+
private buildImageHtml;
|
|
34
|
+
private escapeHtml;
|
|
17
35
|
private extractRepoName;
|
|
18
36
|
private patchPayloadForEvent;
|
|
19
37
|
private buildEventDedupKey;
|
|
@@ -22,4 +40,5 @@ export declare class Notifier extends Service {
|
|
|
22
40
|
private sendMessage;
|
|
23
41
|
private sendWithRetry;
|
|
24
42
|
private sleep;
|
|
43
|
+
private getPreviewPayload;
|
|
25
44
|
}
|
package/lib/services/notifier.js
CHANGED
|
@@ -10,9 +10,33 @@ class Notifier extends koishi_1.Service {
|
|
|
10
10
|
this.recentEventKeys = new Map();
|
|
11
11
|
this.memoryDedupWindowMs = 5000;
|
|
12
12
|
this.dedupWriteCounter = 0;
|
|
13
|
+
this.runtimeRenderMode = null;
|
|
13
14
|
this.ctx.logger('githubsth').info('Notifier service initialized');
|
|
14
15
|
this.registerListeners();
|
|
15
16
|
}
|
|
17
|
+
setRenderMode(mode) {
|
|
18
|
+
this.runtimeRenderMode = mode;
|
|
19
|
+
}
|
|
20
|
+
getRenderMode() {
|
|
21
|
+
return this.runtimeRenderMode || this.config.renderMode;
|
|
22
|
+
}
|
|
23
|
+
getRenderStatus() {
|
|
24
|
+
const puppeteer = this.ctx.puppeteer;
|
|
25
|
+
return {
|
|
26
|
+
mode: this.getRenderMode(),
|
|
27
|
+
configuredMode: this.config.renderMode,
|
|
28
|
+
fallback: this.config.renderFallback,
|
|
29
|
+
theme: this.config.renderTheme,
|
|
30
|
+
width: this.config.renderWidth,
|
|
31
|
+
timeoutMs: this.config.renderTimeoutMs,
|
|
32
|
+
hasPuppeteer: Boolean(puppeteer?.render),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async renderPreview(event = 'issue_comment') {
|
|
36
|
+
const payload = this.getPreviewPayload(event);
|
|
37
|
+
const text = this.formatByEvent(event, payload) || this.formatByEvent('issue_comment', this.getPreviewPayload('issue_comment')) || 'Preview unavailable.';
|
|
38
|
+
return this.prepareOutboundMessage(text);
|
|
39
|
+
}
|
|
16
40
|
registerListeners() {
|
|
17
41
|
const bind = (name, event) => {
|
|
18
42
|
this.ctx.on(name, (payload) => this.handleEvent(event, payload));
|
|
@@ -76,9 +100,6 @@ class Notifier extends koishi_1.Service {
|
|
|
76
100
|
}
|
|
77
101
|
}
|
|
78
102
|
async handleEvent(event, payload) {
|
|
79
|
-
if (this.config.debug) {
|
|
80
|
-
this.ctx.logger('githubsth').info(`Received event: ${event}`);
|
|
81
|
-
}
|
|
82
103
|
const realPayload = payload.payload || payload;
|
|
83
104
|
if (payload.actor && !realPayload.sender) {
|
|
84
105
|
const actorLogin = payload.actor.login || payload.actor.name || 'GitHub';
|
|
@@ -128,10 +149,6 @@ class Notifier extends koishi_1.Service {
|
|
|
128
149
|
this.ctx.logger('githubsth').warn('Cannot query rules: repoName is missing');
|
|
129
150
|
return;
|
|
130
151
|
}
|
|
131
|
-
if (this.config.debug) {
|
|
132
|
-
this.ctx.logger('githubsth').info(`Processing event ${event} for ${repoName}`);
|
|
133
|
-
this.ctx.logger('notifier').debug(JSON.stringify(realPayload, null, 2));
|
|
134
|
-
}
|
|
135
152
|
const repoNames = [repoName];
|
|
136
153
|
if (repoName !== repoName.toLowerCase())
|
|
137
154
|
repoNames.push(repoName.toLowerCase());
|
|
@@ -144,50 +161,151 @@ class Notifier extends koishi_1.Service {
|
|
|
144
161
|
const matchedRules = allRules.filter((rule) => rule.events.includes('*') || rule.events.includes(event));
|
|
145
162
|
if (!matchedRules.length)
|
|
146
163
|
return;
|
|
147
|
-
|
|
164
|
+
// De-duplicate same delivery target to avoid double push caused by duplicated rules.
|
|
165
|
+
const uniqueRules = Array.from(new Map(matchedRules.map((rule) => [`${repoName}|${rule.channelId}`, rule])).values());
|
|
166
|
+
const textMessage = this.formatByEvent(event, realPayload);
|
|
167
|
+
if (!textMessage)
|
|
168
|
+
return;
|
|
169
|
+
const outbound = await this.prepareOutboundMessage(textMessage);
|
|
170
|
+
if (!outbound) {
|
|
171
|
+
if (this.config.debug)
|
|
172
|
+
this.ctx.logger('githubsth').warn(`Drop message because render failed and fallback=drop (${event}, ${repoName})`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const rule of uniqueRules) {
|
|
176
|
+
await this.sendMessage(rule, outbound);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
formatByEvent(event, payload) {
|
|
180
|
+
switch (event) {
|
|
181
|
+
case 'push':
|
|
182
|
+
return this.ctx.githubsthFormatter.formatPush(payload);
|
|
183
|
+
case 'issues':
|
|
184
|
+
return this.ctx.githubsthFormatter.formatIssue(payload);
|
|
185
|
+
case 'pull_request':
|
|
186
|
+
return this.ctx.githubsthFormatter.formatPullRequest(payload);
|
|
187
|
+
case 'star':
|
|
188
|
+
return this.ctx.githubsthFormatter.formatStar(payload);
|
|
189
|
+
case 'fork':
|
|
190
|
+
return this.ctx.githubsthFormatter.formatFork(payload);
|
|
191
|
+
case 'release':
|
|
192
|
+
return this.ctx.githubsthFormatter.formatRelease(payload);
|
|
193
|
+
case 'discussion':
|
|
194
|
+
return this.ctx.githubsthFormatter.formatDiscussion(payload);
|
|
195
|
+
case 'workflow_run':
|
|
196
|
+
return this.ctx.githubsthFormatter.formatWorkflowRun(payload);
|
|
197
|
+
case 'issue_comment':
|
|
198
|
+
return this.ctx.githubsthFormatter.formatIssueComment(payload);
|
|
199
|
+
case 'pull_request_review':
|
|
200
|
+
return this.ctx.githubsthFormatter.formatPullRequestReview(payload);
|
|
201
|
+
default:
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async prepareOutboundMessage(textMessage) {
|
|
206
|
+
const mode = this.getRenderMode();
|
|
207
|
+
if (mode === 'text')
|
|
208
|
+
return textMessage;
|
|
209
|
+
const image = await this.renderTextAsImage(textMessage);
|
|
210
|
+
if (image)
|
|
211
|
+
return image;
|
|
212
|
+
if (mode === 'image' && this.config.renderFallback === 'drop')
|
|
213
|
+
return null;
|
|
214
|
+
return textMessage;
|
|
215
|
+
}
|
|
216
|
+
async renderTextAsImage(textMessage) {
|
|
217
|
+
const puppeteer = this.ctx.puppeteer;
|
|
218
|
+
if (!puppeteer || typeof puppeteer.render !== 'function')
|
|
219
|
+
return null;
|
|
148
220
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
case 'star':
|
|
160
|
-
message = this.ctx.githubsthFormatter.formatStar(realPayload);
|
|
161
|
-
break;
|
|
162
|
-
case 'fork':
|
|
163
|
-
message = this.ctx.githubsthFormatter.formatFork(realPayload);
|
|
164
|
-
break;
|
|
165
|
-
case 'release':
|
|
166
|
-
message = this.ctx.githubsthFormatter.formatRelease(realPayload);
|
|
167
|
-
break;
|
|
168
|
-
case 'discussion':
|
|
169
|
-
message = this.ctx.githubsthFormatter.formatDiscussion(realPayload);
|
|
170
|
-
break;
|
|
171
|
-
case 'workflow_run':
|
|
172
|
-
message = this.ctx.githubsthFormatter.formatWorkflowRun(realPayload);
|
|
173
|
-
break;
|
|
174
|
-
case 'issue_comment':
|
|
175
|
-
message = this.ctx.githubsthFormatter.formatIssueComment(realPayload);
|
|
176
|
-
break;
|
|
177
|
-
case 'pull_request_review':
|
|
178
|
-
message = this.ctx.githubsthFormatter.formatPullRequestReview(realPayload);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
221
|
+
const html = this.buildImageHtml(textMessage);
|
|
222
|
+
const task = puppeteer.render(html);
|
|
223
|
+
const timeout = this.config.renderTimeoutMs || 12000;
|
|
224
|
+
const buffer = await Promise.race([
|
|
225
|
+
task,
|
|
226
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeout)),
|
|
227
|
+
]);
|
|
228
|
+
if (!buffer)
|
|
229
|
+
return null;
|
|
230
|
+
return koishi_1.h.image(buffer, 'image/png');
|
|
181
231
|
}
|
|
182
232
|
catch (error) {
|
|
183
|
-
this.ctx.logger('githubsth').
|
|
184
|
-
return;
|
|
233
|
+
this.ctx.logger('githubsth').warn('Image render failed:', error);
|
|
234
|
+
return null;
|
|
185
235
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
236
|
+
}
|
|
237
|
+
buildImageHtml(textMessage) {
|
|
238
|
+
const escaped = this.escapeHtml(textMessage).replace(/\n/g, '<br/>');
|
|
239
|
+
const width = this.config.renderWidth || 840;
|
|
240
|
+
let bg = 'linear-gradient(135deg, #1f2937, #111827)';
|
|
241
|
+
let card = 'rgba(17, 24, 39, 0.92)';
|
|
242
|
+
let accent = '#22d3ee';
|
|
243
|
+
let font = "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif";
|
|
244
|
+
if (this.config.renderTheme === 'card') {
|
|
245
|
+
bg = 'linear-gradient(140deg, #0f172a, #1e293b)';
|
|
246
|
+
card = 'rgba(15, 23, 42, 0.9)';
|
|
247
|
+
accent = '#60a5fa';
|
|
248
|
+
}
|
|
249
|
+
else if (this.config.renderTheme === 'terminal') {
|
|
250
|
+
bg = '#0b1020';
|
|
251
|
+
card = '#0b1220';
|
|
252
|
+
accent = '#34d399';
|
|
253
|
+
font = "'Consolas', 'Courier New', monospace";
|
|
190
254
|
}
|
|
255
|
+
return `<!doctype html>
|
|
256
|
+
<html>
|
|
257
|
+
<head>
|
|
258
|
+
<meta charset="utf-8" />
|
|
259
|
+
<style>
|
|
260
|
+
* { box-sizing: border-box; }
|
|
261
|
+
body {
|
|
262
|
+
margin: 0;
|
|
263
|
+
width: ${width}px;
|
|
264
|
+
background: ${bg};
|
|
265
|
+
color: #e5e7eb;
|
|
266
|
+
font-family: ${font};
|
|
267
|
+
}
|
|
268
|
+
.wrap {
|
|
269
|
+
padding: 20px;
|
|
270
|
+
}
|
|
271
|
+
.card {
|
|
272
|
+
border-radius: 14px;
|
|
273
|
+
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
274
|
+
background: ${card};
|
|
275
|
+
overflow: hidden;
|
|
276
|
+
}
|
|
277
|
+
.head {
|
|
278
|
+
padding: 12px 16px;
|
|
279
|
+
border-bottom: 1px solid rgba(148, 163, 184, 0.24);
|
|
280
|
+
font-weight: 700;
|
|
281
|
+
color: ${accent};
|
|
282
|
+
letter-spacing: .3px;
|
|
283
|
+
}
|
|
284
|
+
.content {
|
|
285
|
+
padding: 14px 16px;
|
|
286
|
+
line-height: 1.55;
|
|
287
|
+
word-break: break-word;
|
|
288
|
+
font-size: 14px;
|
|
289
|
+
}
|
|
290
|
+
</style>
|
|
291
|
+
</head>
|
|
292
|
+
<body>
|
|
293
|
+
<div class="wrap">
|
|
294
|
+
<div class="card">
|
|
295
|
+
<div class="head">GitHub Notification</div>
|
|
296
|
+
<div class="content">${escaped}</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</body>
|
|
300
|
+
</html>`;
|
|
301
|
+
}
|
|
302
|
+
escapeHtml(input) {
|
|
303
|
+
return String(input)
|
|
304
|
+
.replace(/&/g, '&')
|
|
305
|
+
.replace(/</g, '<')
|
|
306
|
+
.replace(/>/g, '>')
|
|
307
|
+
.replace(/"/g, '"')
|
|
308
|
+
.replace(/'/g, ''');
|
|
191
309
|
}
|
|
192
310
|
extractRepoName(payload, realPayload, event) {
|
|
193
311
|
let repoName = realPayload.repository?.full_name;
|
|
@@ -329,7 +447,6 @@ class Notifier extends koishi_1.Service {
|
|
|
329
447
|
if (recent && now - recent <= this.memoryDedupWindowMs)
|
|
330
448
|
return false;
|
|
331
449
|
this.recentEventKeys.set(dedupKey, now);
|
|
332
|
-
// durable dedup
|
|
333
450
|
const exists = await this.ctx.database.get('github_event_dedup', { dedupKey });
|
|
334
451
|
if (exists.length > 0)
|
|
335
452
|
return false;
|
|
@@ -407,6 +524,55 @@ class Notifier extends koishi_1.Service {
|
|
|
407
524
|
sleep(ms) {
|
|
408
525
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
409
526
|
}
|
|
527
|
+
getPreviewPayload(event) {
|
|
528
|
+
const baseRepo = { full_name: 'acmuhan/JackalClientDocs', stargazers_count: 128 };
|
|
529
|
+
const baseUser = { login: 'acmuhan' };
|
|
530
|
+
const payload = {
|
|
531
|
+
action: 'created',
|
|
532
|
+
repository: baseRepo,
|
|
533
|
+
sender: baseUser,
|
|
534
|
+
issue: {
|
|
535
|
+
number: 29,
|
|
536
|
+
title: 'Delete demobot',
|
|
537
|
+
html_url: 'https://github.com/acmuhan/JackalClientDocs/issues/29',
|
|
538
|
+
pull_request: { html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29' },
|
|
539
|
+
},
|
|
540
|
+
comment: {
|
|
541
|
+
body: '[vc]: #gOsUN...=eyJpc01vbm9yZXBvIjp0cnVl...'
|
|
542
|
+
},
|
|
543
|
+
pull_request: {
|
|
544
|
+
number: 29,
|
|
545
|
+
title: 'Delete demobot',
|
|
546
|
+
state: 'open',
|
|
547
|
+
html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29',
|
|
548
|
+
},
|
|
549
|
+
workflow_run: {
|
|
550
|
+
name: 'Deploy',
|
|
551
|
+
conclusion: 'success',
|
|
552
|
+
head_branch: 'main',
|
|
553
|
+
html_url: 'https://github.com/acmuhan/JackalClientDocs/actions/runs/1',
|
|
554
|
+
},
|
|
555
|
+
commits: [
|
|
556
|
+
{ id: 'ea5eaddca38f25ce013ee50d70addb49c8d28844', message: 'Delete demobot', author: { name: 'MuHan' } },
|
|
557
|
+
],
|
|
558
|
+
ref: 'refs/heads/main',
|
|
559
|
+
compare: 'https://github.com/acmuhan/JackalClientDocs/compare/old...new',
|
|
560
|
+
pusher: { name: 'acmuhan' },
|
|
561
|
+
release: { tag_name: 'v1.2.0', name: 'Spring Patch', html_url: 'https://github.com/acmuhan/JackalClientDocs/releases/tag/v1.2.0' },
|
|
562
|
+
forkee: { full_name: 'acmuhan/JackalClientDocs-fork' },
|
|
563
|
+
discussion: { number: 7, title: 'Roadmap', html_url: 'https://github.com/acmuhan/JackalClientDocs/discussions/7' },
|
|
564
|
+
review: { state: 'approved', html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29#pullrequestreview-1' },
|
|
565
|
+
};
|
|
566
|
+
if (event === 'star')
|
|
567
|
+
payload.action = 'created';
|
|
568
|
+
if (event === 'workflow_run')
|
|
569
|
+
payload.action = 'completed';
|
|
570
|
+
if (event === 'pull_request_review')
|
|
571
|
+
payload.action = 'submitted';
|
|
572
|
+
if (event === 'release')
|
|
573
|
+
payload.action = 'published';
|
|
574
|
+
return payload;
|
|
575
|
+
}
|
|
410
576
|
}
|
|
411
577
|
exports.Notifier = Notifier;
|
|
412
578
|
Notifier.inject = ['githubsthFormatter', 'database'];
|
package/package.json
CHANGED