koishi-plugin-githubsth 1.0.3 → 1.0.5-alpha.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.
@@ -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
- console.log('Applying githubsth commands...');
42
- // Register parent command to show help
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
- console.log('githubsth.subscribe loaded');
47
+ ctx.plugin(render, config);
53
48
  }
@@ -0,0 +1,3 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from '../config';
3
+ export declare function apply(ctx: Context, config: Config): void;
@@ -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
@@ -10,6 +10,7 @@ function apply(ctx) {
10
10
  events: 'list',
11
11
  }, {
12
12
  autoInc: true,
13
+ unique: ['repo', 'channelId', 'platform'],
13
14
  });
14
15
  ctx.model.extend('github_trusted_repo', {
15
16
  id: 'unsigned',
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, h } from 'koishi';
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): h | null;
12
- formatIssue(payload: any): h;
13
- formatPullRequest(payload: any): h;
14
- formatStar(payload: any): h | null;
15
- formatFork(payload: any): h;
16
- formatRelease(payload: any): h | null;
17
- formatDiscussion(payload: any): h;
18
- formatWorkflowRun(payload: any): h | null;
19
- formatIssueComment(payload: any): h | null;
20
- formatPullRequestReview(payload: any): h | null;
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 (0, koishi_1.h)('message', [(0, koishi_1.h)('text', { content: `${lines.join('\n')}\n` })]);
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,28 @@ 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 normalizeRenderedImage;
34
+ private buildImageHtml;
35
+ private escapeHtml;
17
36
  private extractRepoName;
18
37
  private patchPayloadForEvent;
19
38
  private buildEventDedupKey;
@@ -22,4 +41,5 @@ export declare class Notifier extends Service {
22
41
  private sendMessage;
23
42
  private sendWithRetry;
24
43
  private sleep;
44
+ private getPreviewPayload;
25
45
  }
@@ -10,9 +10,34 @@ 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
+ const preview = await this.prepareOutboundMessage(text);
39
+ return preview?.message || null;
40
+ }
16
41
  registerListeners() {
17
42
  const bind = (name, event) => {
18
43
  this.ctx.on(name, (payload) => this.handleEvent(event, payload));
@@ -76,9 +101,6 @@ class Notifier extends koishi_1.Service {
76
101
  }
77
102
  }
78
103
  async handleEvent(event, payload) {
79
- if (this.config.debug) {
80
- this.ctx.logger('githubsth').info(`Received event: ${event}`);
81
- }
82
104
  const realPayload = payload.payload || payload;
83
105
  if (payload.actor && !realPayload.sender) {
84
106
  const actorLogin = payload.actor.login || payload.actor.name || 'GitHub';
@@ -128,10 +150,6 @@ class Notifier extends koishi_1.Service {
128
150
  this.ctx.logger('githubsth').warn('Cannot query rules: repoName is missing');
129
151
  return;
130
152
  }
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
153
  const repoNames = [repoName];
136
154
  if (repoName !== repoName.toLowerCase())
137
155
  repoNames.push(repoName.toLowerCase());
@@ -144,50 +162,170 @@ class Notifier extends koishi_1.Service {
144
162
  const matchedRules = allRules.filter((rule) => rule.events.includes('*') || rule.events.includes(event));
145
163
  if (!matchedRules.length)
146
164
  return;
147
- let message = null;
165
+ // De-duplicate same delivery target to avoid double push caused by duplicated rules.
166
+ const uniqueRules = Array.from(new Map(matchedRules.map((rule) => [`${repoName}|${rule.channelId}`, rule])).values());
167
+ const textMessage = this.formatByEvent(event, realPayload);
168
+ if (!textMessage)
169
+ return;
170
+ const outbound = await this.prepareOutboundMessage(textMessage);
171
+ if (!outbound) {
172
+ if (this.config.debug)
173
+ this.ctx.logger('githubsth').warn(`Drop message because render failed and fallback=drop (${event}, ${repoName})`);
174
+ return;
175
+ }
176
+ for (const rule of uniqueRules) {
177
+ await this.sendMessage(rule, outbound);
178
+ }
179
+ }
180
+ formatByEvent(event, payload) {
181
+ switch (event) {
182
+ case 'push':
183
+ return this.ctx.githubsthFormatter.formatPush(payload);
184
+ case 'issues':
185
+ return this.ctx.githubsthFormatter.formatIssue(payload);
186
+ case 'pull_request':
187
+ return this.ctx.githubsthFormatter.formatPullRequest(payload);
188
+ case 'star':
189
+ return this.ctx.githubsthFormatter.formatStar(payload);
190
+ case 'fork':
191
+ return this.ctx.githubsthFormatter.formatFork(payload);
192
+ case 'release':
193
+ return this.ctx.githubsthFormatter.formatRelease(payload);
194
+ case 'discussion':
195
+ return this.ctx.githubsthFormatter.formatDiscussion(payload);
196
+ case 'workflow_run':
197
+ return this.ctx.githubsthFormatter.formatWorkflowRun(payload);
198
+ case 'issue_comment':
199
+ return this.ctx.githubsthFormatter.formatIssueComment(payload);
200
+ case 'pull_request_review':
201
+ return this.ctx.githubsthFormatter.formatPullRequestReview(payload);
202
+ default:
203
+ return null;
204
+ }
205
+ }
206
+ async prepareOutboundMessage(textMessage) {
207
+ const mode = this.getRenderMode();
208
+ if (mode === 'text') {
209
+ return { message: textMessage, text: textMessage, isImage: false };
210
+ }
211
+ const imageMessage = await this.renderTextAsImage(textMessage);
212
+ if (imageMessage) {
213
+ return { message: imageMessage, text: textMessage, isImage: true };
214
+ }
215
+ if (mode === 'image' && this.config.renderFallback === 'drop')
216
+ return null;
217
+ return { message: textMessage, text: textMessage, isImage: false };
218
+ }
219
+ async renderTextAsImage(textMessage) {
220
+ const puppeteer = this.ctx.puppeteer;
221
+ if (!puppeteer || typeof puppeteer.render !== 'function')
222
+ return null;
148
223
  try {
149
- switch (event) {
150
- case 'push':
151
- message = this.ctx.githubsthFormatter.formatPush(realPayload);
152
- break;
153
- case 'issues':
154
- message = this.ctx.githubsthFormatter.formatIssue(realPayload);
155
- break;
156
- case 'pull_request':
157
- message = this.ctx.githubsthFormatter.formatPullRequest(realPayload);
158
- break;
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
- }
224
+ const html = this.buildImageHtml(textMessage);
225
+ const task = puppeteer.render(html);
226
+ const timeout = this.config.renderTimeoutMs || 12000;
227
+ const rendered = await Promise.race([
228
+ task,
229
+ new Promise((resolve) => setTimeout(() => resolve(null), timeout)),
230
+ ]);
231
+ if (!rendered)
232
+ return null;
233
+ return this.normalizeRenderedImage(rendered);
181
234
  }
182
235
  catch (error) {
183
- this.ctx.logger('githubsth').error(`Error formatting event ${event}:`, error);
184
- return;
236
+ this.ctx.logger('githubsth').warn('Image render failed:', error);
237
+ return null;
185
238
  }
186
- if (!message)
187
- return;
188
- for (const rule of matchedRules) {
189
- await this.sendMessage(rule, message);
239
+ }
240
+ normalizeRenderedImage(rendered) {
241
+ if (!rendered)
242
+ return null;
243
+ if (typeof rendered === 'string') {
244
+ const trimmed = rendered.trim();
245
+ if (trimmed.startsWith('<img'))
246
+ return trimmed;
247
+ if (trimmed.startsWith('data:image/'))
248
+ return koishi_1.h.image(trimmed);
249
+ return null;
190
250
  }
251
+ if (Buffer.isBuffer(rendered))
252
+ return koishi_1.h.image(rendered, 'image/png');
253
+ if (rendered instanceof Uint8Array)
254
+ return koishi_1.h.image(Buffer.from(rendered), 'image/png');
255
+ return null;
256
+ }
257
+ buildImageHtml(textMessage) {
258
+ const escaped = this.escapeHtml(textMessage).replace(/\n/g, '<br/>');
259
+ const width = this.config.renderWidth || 840;
260
+ let bg = 'linear-gradient(135deg, #1f2937, #111827)';
261
+ let card = 'rgba(17, 24, 39, 0.92)';
262
+ let accent = '#22d3ee';
263
+ let font = "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif";
264
+ if (this.config.renderTheme === 'card') {
265
+ bg = 'linear-gradient(140deg, #0f172a, #1e293b)';
266
+ card = 'rgba(15, 23, 42, 0.9)';
267
+ accent = '#60a5fa';
268
+ }
269
+ else if (this.config.renderTheme === 'terminal') {
270
+ bg = '#0b1020';
271
+ card = '#0b1220';
272
+ accent = '#34d399';
273
+ font = "'Consolas', 'Courier New', monospace";
274
+ }
275
+ return `<!doctype html>
276
+ <html>
277
+ <head>
278
+ <meta charset="utf-8" />
279
+ <style>
280
+ * { box-sizing: border-box; }
281
+ body {
282
+ margin: 0;
283
+ width: ${width}px;
284
+ background: ${bg};
285
+ color: #e5e7eb;
286
+ font-family: ${font};
287
+ }
288
+ .wrap {
289
+ padding: 20px;
290
+ }
291
+ .card {
292
+ border-radius: 14px;
293
+ border: 1px solid rgba(148, 163, 184, 0.28);
294
+ background: ${card};
295
+ overflow: hidden;
296
+ }
297
+ .head {
298
+ padding: 12px 16px;
299
+ border-bottom: 1px solid rgba(148, 163, 184, 0.24);
300
+ font-weight: 700;
301
+ color: ${accent};
302
+ letter-spacing: .3px;
303
+ }
304
+ .content {
305
+ padding: 14px 16px;
306
+ line-height: 1.55;
307
+ word-break: break-word;
308
+ font-size: 14px;
309
+ }
310
+ </style>
311
+ </head>
312
+ <body>
313
+ <div class="wrap">
314
+ <div class="card">
315
+ <div class="head">GitHub Notification</div>
316
+ <div class="content">${escaped}</div>
317
+ </div>
318
+ </div>
319
+ </body>
320
+ </html>`;
321
+ }
322
+ escapeHtml(input) {
323
+ return String(input)
324
+ .replace(/&/g, '&amp;')
325
+ .replace(/</g, '&lt;')
326
+ .replace(/>/g, '&gt;')
327
+ .replace(/"/g, '&quot;')
328
+ .replace(/'/g, '&#39;');
191
329
  }
192
330
  extractRepoName(payload, realPayload, event) {
193
331
  let repoName = realPayload.repository?.full_name;
@@ -329,7 +467,6 @@ class Notifier extends koishi_1.Service {
329
467
  if (recent && now - recent <= this.memoryDedupWindowMs)
330
468
  return false;
331
469
  this.recentEventKeys.set(dedupKey, now);
332
- // durable dedup
333
470
  const exists = await this.ctx.database.get('github_event_dedup', { dedupKey });
334
471
  if (exists.length > 0)
335
472
  return false;
@@ -365,22 +502,31 @@ class Notifier extends koishi_1.Service {
365
502
  }
366
503
  }
367
504
  }
368
- async sendMessage(rule, message) {
505
+ async sendMessage(rule, outbound) {
369
506
  const bots = this.ctx.bots.filter((bot) => !rule.platform || bot.platform === rule.platform);
370
507
  if (!bots.length)
371
508
  return;
372
509
  for (const bot of bots) {
373
510
  try {
374
- await this.sendWithRetry(bot, rule.channelId, message);
375
- if (this.config.debug) {
511
+ await this.sendWithRetry(bot, rule.channelId, outbound.message);
512
+ if (this.config.debug)
376
513
  this.ctx.logger('notifier').info(`Sent message to ${rule.channelId} via ${bot.platform}:${bot.selfId}`);
377
- }
378
514
  return;
379
515
  }
380
516
  catch (error) {
381
- if (this.config.debug) {
382
- this.ctx.logger('notifier').warn(`Bot ${bot.sid} failed to send message with retries: ${error}`);
517
+ if (outbound.isImage && this.config.renderFallback === 'text') {
518
+ try {
519
+ await this.sendWithRetry(bot, rule.channelId, outbound.text);
520
+ this.ctx.logger('notifier').warn(`Image failed on ${bot.platform}:${bot.selfId}, fallback to text succeeded.`);
521
+ return;
522
+ }
523
+ catch (fallbackError) {
524
+ if (this.config.debug)
525
+ this.ctx.logger('notifier').warn(`Fallback text send failed: ${fallbackError}`);
526
+ }
383
527
  }
528
+ if (this.config.debug)
529
+ this.ctx.logger('notifier').warn(`Bot ${bot.sid} failed to send message with retries: ${error}`);
384
530
  }
385
531
  }
386
532
  this.ctx.logger('notifier').warn(`Failed to send message to ${rule.channelId}`);
@@ -407,6 +553,55 @@ class Notifier extends koishi_1.Service {
407
553
  sleep(ms) {
408
554
  return new Promise((resolve) => setTimeout(resolve, ms));
409
555
  }
556
+ getPreviewPayload(event) {
557
+ const baseRepo = { full_name: 'acmuhan/JackalClientDocs', stargazers_count: 128 };
558
+ const baseUser = { login: 'acmuhan' };
559
+ const payload = {
560
+ action: 'created',
561
+ repository: baseRepo,
562
+ sender: baseUser,
563
+ issue: {
564
+ number: 29,
565
+ title: 'Delete demobot',
566
+ html_url: 'https://github.com/acmuhan/JackalClientDocs/issues/29',
567
+ pull_request: { html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29' },
568
+ },
569
+ comment: {
570
+ body: '[vc]: #gOsUN...=eyJpc01vbm9yZXBvIjp0cnVl...'
571
+ },
572
+ pull_request: {
573
+ number: 29,
574
+ title: 'Delete demobot',
575
+ state: 'open',
576
+ html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29',
577
+ },
578
+ workflow_run: {
579
+ name: 'Deploy',
580
+ conclusion: 'success',
581
+ head_branch: 'main',
582
+ html_url: 'https://github.com/acmuhan/JackalClientDocs/actions/runs/1',
583
+ },
584
+ commits: [
585
+ { id: 'ea5eaddca38f25ce013ee50d70addb49c8d28844', message: 'Delete demobot', author: { name: 'MuHan' } },
586
+ ],
587
+ ref: 'refs/heads/main',
588
+ compare: 'https://github.com/acmuhan/JackalClientDocs/compare/old...new',
589
+ pusher: { name: 'acmuhan' },
590
+ release: { tag_name: 'v1.2.0', name: 'Spring Patch', html_url: 'https://github.com/acmuhan/JackalClientDocs/releases/tag/v1.2.0' },
591
+ forkee: { full_name: 'acmuhan/JackalClientDocs-fork' },
592
+ discussion: { number: 7, title: 'Roadmap', html_url: 'https://github.com/acmuhan/JackalClientDocs/discussions/7' },
593
+ review: { state: 'approved', html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29#pullrequestreview-1' },
594
+ };
595
+ if (event === 'star')
596
+ payload.action = 'created';
597
+ if (event === 'workflow_run')
598
+ payload.action = 'completed';
599
+ if (event === 'pull_request_review')
600
+ payload.action = 'submitted';
601
+ if (event === 'release')
602
+ payload.action = 'published';
603
+ return payload;
604
+ }
410
605
  }
411
606
  exports.Notifier = Notifier;
412
607
  Notifier.inject = ['githubsthFormatter', 'database'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-githubsth",
3
- "version": "1.0.3",
3
+ "version": "1.0.5-alpha.0",
4
4
  "description": "Github Subscriptions Notifications, push notifications for GitHub subscriptions For koishi",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",