koishi-plugin-githubsth 1.0.4 → 1.0.5-alpha.1

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.
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.apply = apply;
4
4
  const modes = ['text', 'image', 'auto'];
5
- const themes = ['compact', 'card', 'terminal'];
6
5
  const events = ['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'star', 'fork', 'release', 'discussion', 'workflow_run'];
7
6
  function apply(ctx, config) {
8
7
  ctx.command('githubsth.render', '通知渲染设置(文本/图片)', { authority: 3 })
@@ -13,7 +12,7 @@ function apply(ctx, config) {
13
12
  return [
14
13
  `mode: ${status.mode} (configured: ${status.configuredMode})`,
15
14
  `fallback: ${status.fallback}`,
16
- `theme: ${status.theme}`,
15
+ `theme(default): ${status.theme}`,
17
16
  `width: ${status.width}`,
18
17
  `timeout: ${status.timeoutMs}ms`,
19
18
  `puppeteer: ${status.hasPuppeteer ? 'ready' : 'missing'}`,
@@ -27,13 +26,14 @@ function apply(ctx, config) {
27
26
  ctx.githubsthNotifier.setRenderMode(mode);
28
27
  return `已切换运行时渲染模式为 ${mode}(重启后恢复配置值 ${config.renderMode})。`;
29
28
  });
30
- ctx.command('githubsth.render.theme <theme:string>', '切换图片主题(compact/card/terminal)', { authority: 3 })
29
+ ctx.command('githubsth.render.theme <theme:string>', '设置全局默认主题', { authority: 3 })
31
30
  .action(async (_, theme) => {
32
- if (!theme || !themes.includes(theme)) {
33
- return `无效主题。可选:${themes.join(', ')}`;
31
+ const normalized = ctx.githubsthNotifier.normalizeTheme(theme);
32
+ if (!normalized) {
33
+ return `无效主题。请使用 githubsth.render.themes 查看可用主题。`;
34
34
  }
35
- config.renderTheme = theme;
36
- return `已切换图片主题为 ${theme}(当前进程生效)。`;
35
+ config.renderTheme = normalized;
36
+ return `已设置全局默认主题为 ${normalized}(当前进程生效)。`;
37
37
  });
38
38
  ctx.command('githubsth.render.width <width:number>', '设置图片宽度', { authority: 3 })
39
39
  .action(async (_, width) => {
@@ -43,13 +43,72 @@ function apply(ctx, config) {
43
43
  config.renderWidth = normalized;
44
44
  return `已设置图片宽度为 ${normalized}px(当前进程生效)。`;
45
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';
46
+ ctx.command('githubsth.render.themes', '查看主题列表', { authority: 3 })
47
+ .action(async () => {
48
+ const themes = ctx.githubsthNotifier.listThemes();
49
+ return `可用主题:\n- ${themes.join('\n- ')}`;
50
+ });
51
+ ctx.command('githubsth.render.preview [event:string] [theme:string]', '预览通知渲染', { authority: 3 })
52
+ .action(async ({ session }, event, theme) => {
53
+ const selectedEvent = event && events.includes(event) ? event : 'issue_comment';
49
54
  if (event && !events.includes(event)) {
50
55
  await session?.send(`未知事件 ${event},已改用默认事件 issue_comment。`);
51
56
  }
52
- const preview = await ctx.githubsthNotifier.renderPreview(selected);
57
+ const normalizedTheme = theme ? ctx.githubsthNotifier.normalizeTheme(theme) : null;
58
+ if (theme && !normalizedTheme) {
59
+ await session?.send(`未知主题 ${theme},将使用默认主题。`);
60
+ }
61
+ const preview = await ctx.githubsthNotifier.renderPreview(selectedEvent, normalizedTheme || undefined);
53
62
  return preview || '预览失败:请检查 puppeteer 或渲染配置。';
54
63
  });
64
+ ctx.command('githubsth.render.repo-theme <repo:string> <theme:string>', '为当前频道某订阅设置单独主题', { authority: 3 })
65
+ .action(async ({ session }, repo, theme) => {
66
+ if (!repo)
67
+ return '请提供仓库(owner/repo)。';
68
+ if (!session?.channelId)
69
+ return '请在群聊/频道中执行该命令。';
70
+ const normalized = ctx.githubsthNotifier.normalizeTheme(theme);
71
+ if (!normalized)
72
+ return '主题不存在,请先执行 githubsth.render.themes。';
73
+ const target = await ctx.database.get('github_subscription', {
74
+ repo,
75
+ channelId: session.channelId,
76
+ platform: session.platform || 'unknown',
77
+ });
78
+ if (!target.length)
79
+ return '当前频道没有该仓库订阅。';
80
+ await ctx.database.set('github_subscription', { id: target[0].id }, { renderTheme: normalized });
81
+ return `已为 ${repo} 设置专属主题:${normalized}`;
82
+ });
83
+ ctx.command('githubsth.render.repo-theme.clear <repo:string>', '清除当前频道某订阅的专属主题', { authority: 3 })
84
+ .action(async ({ session }, repo) => {
85
+ if (!repo)
86
+ return '请提供仓库(owner/repo)。';
87
+ if (!session?.channelId)
88
+ return '请在群聊/频道中执行该命令。';
89
+ const target = await ctx.database.get('github_subscription', {
90
+ repo,
91
+ channelId: session.channelId,
92
+ platform: session.platform || 'unknown',
93
+ });
94
+ if (!target.length)
95
+ return '当前频道没有该仓库订阅。';
96
+ await ctx.database.set('github_subscription', { id: target[0].id }, { renderTheme: null });
97
+ return `已清除 ${repo} 的专属主题,回退到全局主题。`;
98
+ });
99
+ ctx.command('githubsth.render.repo-theme.list [repo:string]', '查看当前频道订阅的专属主题', { authority: 3 })
100
+ .action(async ({ session }, repo) => {
101
+ if (!session?.channelId)
102
+ return '请在群聊/频道中执行该命令。';
103
+ const query = {
104
+ channelId: session.channelId,
105
+ platform: session.platform || 'unknown',
106
+ };
107
+ if (repo)
108
+ query.repo = repo;
109
+ const subs = await ctx.database.get('github_subscription', query);
110
+ if (!subs.length)
111
+ return '当前频道没有匹配订阅。';
112
+ return subs.map((sub) => `${sub.repo} => ${sub.renderTheme || '(default)'}`).join('\n');
113
+ });
55
114
  }
@@ -3,101 +3,73 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.apply = apply;
4
4
  function apply(ctx, config) {
5
5
  const logger = ctx.logger('githubsth');
6
- const repoRegex = /^[\w-]+\/[\w-\.]+$/;
6
+ const repoRegex = /^[\w-]+\/[\w-.]+$/;
7
7
  const validEvents = [
8
8
  'push', 'issues', 'issue_comment', 'pull_request',
9
9
  'pull_request_review', 'star', 'fork', 'release',
10
- 'discussion', 'workflow_run'
10
+ 'discussion', 'workflow_run',
11
11
  ];
12
12
  const defaultConfigEvents = config.defaultEvents || ['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'release', 'star', 'fork'];
13
- ctx.command('githubsth.subscribe <repo> [events:text]', '订阅 GitHub 仓库')
13
+ ctx.command('githubsth.subscribe <repo> [events:text]', '订阅 GitHub 仓库事件')
14
14
  .alias('gh.sub')
15
15
  .usage(`
16
- 订阅 GitHub 仓库通知。
17
- 如果不指定事件,默认订阅: ${defaultConfigEvents.join(', ')}
16
+ 订阅 GitHub 仓库通知。若不指定事件,默认订阅:${defaultConfigEvents.join(', ')}
18
17
 
19
- 可选事件:
20
- - push: 代码推送
21
- - issues: Issue 创建/关闭/重开
22
- - issue_comment: Issue 评论
23
- - pull_request: PR 创建/关闭/重开
24
- - pull_request_review: PR 审查
25
- - star: 标星
26
- - fork: 仓库 Fork
27
- - release: 发布新版本
28
- - discussion: 讨论区更新
29
- - workflow_run: Workflow 运行
30
-
31
- 示例:
32
- gh.sub koishijs/koishi
33
- gh.sub koishijs/koishi push,issues,star
18
+ 示例:
19
+ - gh.sub koishijs/koishi
20
+ - gh.sub koishijs/koishi push,issues,star
34
21
  `)
35
22
  .action(async ({ session }, repo, eventsStr) => {
36
23
  if (!repo)
37
- return '请指定仓库名称 (owner/repo)。';
24
+ return '请指定仓库(owner/repo)。';
38
25
  if (!repoRegex.test(repo))
39
- return '仓库名称格式不正确 (应为 owner/repo)。';
26
+ return '仓库格式错误,应为 owner/repo。';
40
27
  if (!session?.channelId)
41
- return '请在群组中使用此命令。';
42
- // Check trusted repo
28
+ return '请在群聊/频道中执行该命令。';
43
29
  const trusted = await ctx.database.get('github_trusted_repo', { repo, enabled: true });
44
- if (trusted.length === 0) {
45
- return '该仓库不在信任列表中,无法订阅。请联系管理员添加。';
46
- }
47
- // Parse events
30
+ if (trusted.length === 0)
31
+ return '该仓库不在信任列表中,请先由管理员添加。';
48
32
  let events;
49
33
  if (eventsStr) {
50
- // Split by comma, Chinese comma, or whitespace
51
- events = eventsStr.split(/[,,\s]+/).map(e => e.trim()).filter(Boolean);
52
- // Normalize events (kebab-case to snake_case)
53
- events = events.map(e => e.replace(/-/g, '_'));
54
- // Validate events
55
- const invalidEvents = events.filter(e => !validEvents.includes(e) && e !== '*');
56
- if (invalidEvents.length > 0) {
57
- return `无效的事件类型: ${invalidEvents.join(', ')}。\n可选事件: ${validEvents.join(', ')}`;
34
+ events = eventsStr.split(/[,,\s]+/).map((e) => e.trim()).filter(Boolean).map((e) => e.replace(/-/g, '_'));
35
+ const invalidEvents = events.filter((e) => !validEvents.includes(e) && e !== '*');
36
+ if (invalidEvents.length) {
37
+ return `无效事件:${invalidEvents.join(', ')}\n可选事件:${validEvents.join(', ')}`;
58
38
  }
59
39
  }
60
40
  else {
61
- // Default events
62
41
  events = [...defaultConfigEvents];
63
42
  }
64
43
  try {
65
- // Check if subscription exists
66
44
  const existing = await ctx.database.get('github_subscription', {
67
45
  repo,
68
46
  channelId: session.channelId,
69
47
  platform: session.platform || 'unknown',
70
48
  });
71
49
  if (existing.length > 0) {
72
- // Update existing subscription
73
- await ctx.database.set('github_subscription', { id: existing[0].id }, {
74
- events,
75
- });
76
- return `已更新 ${repo} 的订阅,当前监听事件: ${events.join(', ')}。`;
77
- }
78
- else {
79
- // Create new subscription
80
- await ctx.database.create('github_subscription', {
81
- repo,
82
- channelId: session.channelId,
83
- platform: session.platform || 'unknown',
84
- events,
85
- });
86
- return `已订阅 ${repo} 的 ${events.join(', ')} 事件。`;
50
+ await ctx.database.set('github_subscription', { id: existing[0].id }, { events });
51
+ return `已更新订阅:${repo}\n事件:${events.join(', ')}`;
87
52
  }
53
+ await ctx.database.create('github_subscription', {
54
+ repo,
55
+ channelId: session.channelId,
56
+ platform: session.platform || 'unknown',
57
+ events,
58
+ });
59
+ return `已订阅 ${repo}\n事件:${events.join(', ')}`;
88
60
  }
89
- catch (e) {
90
- logger.warn(e);
91
- return '订阅失败。';
61
+ catch (error) {
62
+ logger.warn(error);
63
+ return '订阅失败,请稍后重试。';
92
64
  }
93
65
  });
94
66
  ctx.command('githubsth.unsubscribe <repo>', '取消订阅 GitHub 仓库')
95
67
  .alias('gh.unsub')
96
68
  .action(async ({ session }, repo) => {
97
69
  if (!repo)
98
- return '请指定仓库名称 (owner/repo)。';
70
+ return '请指定仓库(owner/repo)。';
99
71
  if (!session?.channelId)
100
- return '请在群组中使用此命令。';
72
+ return '请在群聊/频道中执行该命令。';
101
73
  const result = await ctx.database.remove('github_subscription', {
102
74
  repo,
103
75
  channelId: session.channelId,
@@ -105,19 +77,19 @@ gh.sub koishijs/koishi push,issues,star
105
77
  });
106
78
  if (result.matched === 0)
107
79
  return '未找到该订阅。';
108
- return `已取消订阅 ${repo}。`;
80
+ return `已取消订阅 ${repo}`;
109
81
  });
110
- ctx.command('githubsth.list', '查看当前频道的订阅')
82
+ ctx.command('githubsth.list', '查看当前频道订阅')
111
83
  .alias('gh.list')
112
84
  .action(async ({ session }) => {
113
85
  if (!session?.channelId)
114
- return '请在群组中使用此命令。';
86
+ return '请在群聊/频道中执行该命令。';
115
87
  const subs = await ctx.database.get('github_subscription', {
116
88
  channelId: session.channelId,
117
89
  platform: session.platform || 'unknown',
118
90
  });
119
91
  if (subs.length === 0)
120
92
  return '当前频道没有订阅。';
121
- return subs.map(s => `${s.repo} [${s.events.join(', ')}]`).join('\n');
93
+ return subs.map((sub) => `${sub.repo} [${sub.events.join(', ')}] theme=${sub.renderTheme || '(default)'}`).join('\n');
122
94
  });
123
95
  }
package/lib/config.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Schema } from 'koishi';
2
+ export type RenderMode = 'text' | 'image' | 'auto';
3
+ export type RenderFallback = 'text' | 'drop';
4
+ export type RenderTheme = 'github-light' | 'github-dark' | 'aurora' | 'sunset' | 'matrix' | 'compact' | 'card' | 'terminal';
2
5
  export interface Rule {
3
6
  repo: string;
4
7
  channelId: string;
5
8
  platform?: string;
6
9
  events: string[];
10
+ renderTheme?: RenderTheme;
7
11
  }
8
- export type RenderMode = 'text' | 'image' | 'auto';
9
- export type RenderFallback = 'text' | 'drop';
10
- export type RenderTheme = 'compact' | 'card' | 'terminal';
11
12
  export interface Config {
12
13
  defaultOwner?: string;
13
14
  defaultRepo?: string;
package/lib/config.js CHANGED
@@ -25,11 +25,16 @@ exports.Config = koishi_1.Schema.object({
25
25
  koishi_1.Schema.const('drop').description('图片失败则丢弃'),
26
26
  ]).default('text').description('图片渲染失败时的回退策略。'),
27
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('图片宽度(像素)。'),
28
+ koishi_1.Schema.const('github-light').description('GitHub Light'),
29
+ koishi_1.Schema.const('github-dark').description('GitHub Dark'),
30
+ koishi_1.Schema.const('aurora').description('Aurora'),
31
+ koishi_1.Schema.const('sunset').description('Sunset'),
32
+ koishi_1.Schema.const('matrix').description('Matrix'),
33
+ koishi_1.Schema.const('compact').description('兼容: compact'),
34
+ koishi_1.Schema.const('card').description('兼容: card'),
35
+ koishi_1.Schema.const('terminal').description('兼容: terminal'),
36
+ ]).default('github-dark').description('图片通知主题。'),
37
+ renderWidth: koishi_1.Schema.number().min(480).max(1600).default(860).description('图片宽度(像素)。'),
33
38
  renderTimeoutMs: koishi_1.Schema.number().min(1000).max(60000).default(12000).description('单次图片渲染超时(毫秒)。'),
34
39
  defaultEvents: koishi_1.Schema.array(koishi_1.Schema.string())
35
40
  .default(['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'release', 'star', 'fork'])
@@ -39,5 +44,15 @@ exports.Config = koishi_1.Schema.object({
39
44
  channelId: koishi_1.Schema.string().required(),
40
45
  platform: koishi_1.Schema.string(),
41
46
  events: koishi_1.Schema.array(koishi_1.Schema.string()).default(['push', 'issues', 'pull_request', 'issue_comment', 'pull_request_review']),
47
+ renderTheme: koishi_1.Schema.union([
48
+ koishi_1.Schema.const('github-light'),
49
+ koishi_1.Schema.const('github-dark'),
50
+ koishi_1.Schema.const('aurora'),
51
+ koishi_1.Schema.const('sunset'),
52
+ koishi_1.Schema.const('matrix'),
53
+ koishi_1.Schema.const('compact'),
54
+ koishi_1.Schema.const('card'),
55
+ koishi_1.Schema.const('terminal'),
56
+ ]),
42
57
  })).hidden().description('已废弃,仅保留兼容。建议改用数据库订阅管理。'),
43
58
  });
package/lib/database.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Context } from 'koishi';
2
+ import type { RenderTheme } from './config';
2
3
  declare module 'koishi' {
3
4
  interface Tables {
4
5
  github_subscription: GithubSubscription;
@@ -12,6 +13,7 @@ export interface GithubSubscription {
12
13
  channelId: string;
13
14
  platform: string;
14
15
  events: string[];
16
+ renderTheme?: RenderTheme;
15
17
  }
16
18
  export interface GithubTrustedRepo {
17
19
  id: number;
package/lib/database.js CHANGED
@@ -8,6 +8,7 @@ function apply(ctx) {
8
8
  channelId: 'string',
9
9
  platform: 'string',
10
10
  events: 'list',
11
+ renderTheme: 'string',
11
12
  }, {
12
13
  autoInc: true,
13
14
  unique: ['repo', 'channelId', 'platform'],
@@ -1,5 +1,5 @@
1
- import { Context, Service, h } from 'koishi';
2
- import { Config, RenderMode } from '../config';
1
+ import { Context, Service } from 'koishi';
2
+ import { Config, RenderMode, RenderTheme } from '../config';
3
3
  declare module 'koishi' {
4
4
  interface Context {
5
5
  githubsthNotifier: Notifier;
@@ -13,24 +13,32 @@ export declare class Notifier extends Service {
13
13
  private dedupWriteCounter;
14
14
  private runtimeRenderMode;
15
15
  constructor(ctx: Context, config: Config);
16
+ listThemes(): RenderTheme[];
17
+ normalizeTheme(theme?: string | null): RenderTheme | null;
16
18
  setRenderMode(mode: RenderMode): void;
17
19
  getRenderMode(): RenderMode;
18
20
  getRenderStatus(): {
19
21
  mode: RenderMode;
20
22
  configuredMode: RenderMode;
21
23
  fallback: import("../config").RenderFallback;
22
- theme: import("../config").RenderTheme;
24
+ theme: RenderTheme;
23
25
  width: number;
24
26
  timeoutMs: number;
25
27
  hasPuppeteer: boolean;
26
28
  };
27
- renderPreview(event?: string): Promise<string | h | null>;
29
+ renderPreview(event?: string, theme?: RenderTheme | null): Promise<any>;
28
30
  private registerListeners;
29
31
  private handleEvent;
32
+ private resolveRuleTheme;
30
33
  private formatByEvent;
31
34
  private prepareOutboundMessage;
32
35
  private renderTextAsImage;
36
+ private normalizeRenderedImage;
33
37
  private buildImageHtml;
38
+ private getEventTitle;
39
+ private buildStatusPills;
40
+ private buildCommitBlock;
41
+ private pickCommitIcon;
34
42
  private escapeHtml;
35
43
  private extractRepoName;
36
44
  private patchPayloadForEvent;
@@ -1,7 +1,118 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Notifier = void 0;
4
+ const node_buffer_1 = require("node:buffer");
4
5
  const koishi_1 = require("koishi");
6
+ const THEME_PRESETS = {
7
+ 'github-light': {
8
+ key: 'github-light',
9
+ title: 'GitHub Light',
10
+ background: 'linear-gradient(145deg, #f6f8fa, #eef2f7)',
11
+ card: '#ffffff',
12
+ border: '#d0d7de',
13
+ text: '#24292f',
14
+ muted: '#57606a',
15
+ accent: '#0969da',
16
+ pillText: '#0969da',
17
+ pillBg: '#ddf4ff',
18
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
19
+ },
20
+ 'github-dark': {
21
+ key: 'github-dark',
22
+ title: 'GitHub Dark',
23
+ background: 'linear-gradient(145deg, #0d1117, #161b22)',
24
+ card: '#161b22',
25
+ border: '#30363d',
26
+ text: '#c9d1d9',
27
+ muted: '#8b949e',
28
+ accent: '#58a6ff',
29
+ pillText: '#58a6ff',
30
+ pillBg: 'rgba(56,139,253,0.15)',
31
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
32
+ },
33
+ aurora: {
34
+ key: 'aurora',
35
+ title: 'Aurora',
36
+ background: 'linear-gradient(155deg, #07203f, #2b5876)',
37
+ card: 'rgba(12, 24, 42, 0.88)',
38
+ border: 'rgba(143, 211, 244, 0.35)',
39
+ text: '#e8f5ff',
40
+ muted: '#b7d7ef',
41
+ accent: '#7dd3fc',
42
+ pillText: '#7dd3fc',
43
+ pillBg: 'rgba(125,211,252,0.16)',
44
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
45
+ },
46
+ sunset: {
47
+ key: 'sunset',
48
+ title: 'Sunset',
49
+ background: 'linear-gradient(155deg, #3f2b96, #a83279)',
50
+ card: 'rgba(46, 16, 57, 0.88)',
51
+ border: 'rgba(255, 183, 197, 0.34)',
52
+ text: '#fff1f6',
53
+ muted: '#ffd2e4',
54
+ accent: '#fda4af',
55
+ pillText: '#fecdd3',
56
+ pillBg: 'rgba(253,164,175,0.18)',
57
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
58
+ },
59
+ matrix: {
60
+ key: 'matrix',
61
+ title: 'Matrix',
62
+ background: '#07140d',
63
+ card: '#0a1e13',
64
+ border: '#1f5136',
65
+ text: '#b8f7cc',
66
+ muted: '#7fd79f',
67
+ accent: '#34d399',
68
+ pillText: '#34d399',
69
+ pillBg: 'rgba(52,211,153,0.16)',
70
+ font: "'Consolas', 'Courier New', monospace",
71
+ },
72
+ compact: {
73
+ key: 'compact',
74
+ title: 'Compact',
75
+ background: 'linear-gradient(145deg, #1f2937, #111827)',
76
+ card: 'rgba(17,24,39,0.92)',
77
+ border: 'rgba(148,163,184,0.30)',
78
+ text: '#e5e7eb',
79
+ muted: '#94a3b8',
80
+ accent: '#22d3ee',
81
+ pillText: '#22d3ee',
82
+ pillBg: 'rgba(34,211,238,0.15)',
83
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
84
+ },
85
+ card: {
86
+ key: 'card',
87
+ title: 'Card',
88
+ background: 'linear-gradient(145deg, #0f172a, #1e293b)',
89
+ card: 'rgba(15,23,42,0.92)',
90
+ border: 'rgba(148,163,184,0.30)',
91
+ text: '#e2e8f0',
92
+ muted: '#94a3b8',
93
+ accent: '#60a5fa',
94
+ pillText: '#60a5fa',
95
+ pillBg: 'rgba(96,165,250,0.16)',
96
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
97
+ },
98
+ terminal: {
99
+ key: 'terminal',
100
+ title: 'Terminal',
101
+ background: '#0b1020',
102
+ card: '#0b1220',
103
+ border: '#1f2a44',
104
+ text: '#d1fae5',
105
+ muted: '#6ee7b7',
106
+ accent: '#34d399',
107
+ pillText: '#34d399',
108
+ pillBg: 'rgba(52,211,153,0.18)',
109
+ font: "'Consolas', 'Courier New', monospace",
110
+ },
111
+ };
112
+ const THEME_ALIASES = {
113
+ 'gh-light': 'github-light',
114
+ 'gh-dark': 'github-dark',
115
+ };
5
116
  class Notifier extends koishi_1.Service {
6
117
  // @ts-ignore
7
118
  constructor(ctx, config) {
@@ -14,6 +125,18 @@ class Notifier extends koishi_1.Service {
14
125
  this.ctx.logger('githubsth').info('Notifier service initialized');
15
126
  this.registerListeners();
16
127
  }
128
+ listThemes() {
129
+ return Object.keys(THEME_PRESETS);
130
+ }
131
+ normalizeTheme(theme) {
132
+ if (!theme)
133
+ return null;
134
+ const key = theme.trim().toLowerCase();
135
+ const direct = this.listThemes().find((item) => item === key);
136
+ if (direct)
137
+ return direct;
138
+ return THEME_ALIASES[key] || null;
139
+ }
17
140
  setRenderMode(mode) {
18
141
  this.runtimeRenderMode = mode;
19
142
  }
@@ -32,10 +155,13 @@ class Notifier extends koishi_1.Service {
32
155
  hasPuppeteer: Boolean(puppeteer?.render),
33
156
  };
34
157
  }
35
- async renderPreview(event = 'issue_comment') {
158
+ async renderPreview(event = 'issue_comment', theme) {
36
159
  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);
160
+ const text = this.formatByEvent(event, payload)
161
+ || this.formatByEvent('issue_comment', this.getPreviewPayload('issue_comment'))
162
+ || 'Preview unavailable.';
163
+ const preview = await this.prepareOutboundMessage(text, event, payload, theme || this.config.renderTheme);
164
+ return preview?.message || null;
39
165
  }
40
166
  registerListeners() {
41
167
  const bind = (name, event) => {
@@ -51,7 +177,6 @@ class Notifier extends koishi_1.Service {
51
177
  bind('github/fork', 'fork');
52
178
  bind('github/release', 'release');
53
179
  bind('github/discussion', 'discussion');
54
- // legacy aliases
55
180
  bind('github/issues', 'issues');
56
181
  bind('github/pull_request', 'pull_request');
57
182
  bind('github/workflow_run', 'workflow_run');
@@ -63,9 +188,6 @@ class Notifier extends koishi_1.Service {
63
188
  const payload = session.payload || session.extra || session.data;
64
189
  if (!payload)
65
190
  return;
66
- if (this.config.debug) {
67
- this.ctx.logger('githubsth').info('Found payload in session, attempting to handle');
68
- }
69
191
  const realPayload = payload.payload || payload;
70
192
  let eventType = 'unknown';
71
193
  if (realPayload.issue && realPayload.comment)
@@ -90,12 +212,8 @@ class Notifier extends koishi_1.Service {
90
212
  eventType = 'workflow_run';
91
213
  else if (realPayload.repository && (realPayload.action === 'created' || realPayload.action === 'started'))
92
214
  eventType = 'star';
93
- if (eventType !== 'unknown') {
215
+ if (eventType !== 'unknown')
94
216
  void this.handleEvent(eventType, payload);
95
- }
96
- else if (this.config.logUnhandledEvents) {
97
- this.ctx.logger('githubsth').info(`Unhandled payload structure. Keys: ${Object.keys(realPayload).join(', ')}`);
98
- }
99
217
  });
100
218
  }
101
219
  }
@@ -109,46 +227,28 @@ class Notifier extends koishi_1.Service {
109
227
  realPayload.repository = payload.repository;
110
228
  }
111
229
  let repoName = this.extractRepoName(payload, realPayload, event);
112
- if (!realPayload.repository) {
230
+ if (!realPayload.repository)
113
231
  realPayload.repository = { full_name: repoName || 'Unknown/Repo' };
114
- }
115
- else if (!realPayload.repository.full_name) {
232
+ else if (!realPayload.repository.full_name)
116
233
  realPayload.repository.full_name = repoName || 'Unknown/Repo';
117
- }
118
234
  if (!realPayload.sender) {
119
- if (realPayload.issue?.user) {
235
+ if (realPayload.issue?.user)
120
236
  realPayload.sender = realPayload.issue.user;
121
- }
122
- else if (realPayload.pull_request?.user) {
237
+ else if (realPayload.pull_request?.user)
123
238
  realPayload.sender = realPayload.pull_request.user;
124
- }
125
- else if (realPayload.discussion?.user) {
239
+ else if (realPayload.discussion?.user)
126
240
  realPayload.sender = realPayload.discussion.user;
127
- }
128
- else if (realPayload.pusher) {
241
+ else if (realPayload.pusher)
129
242
  realPayload.sender = { login: realPayload.pusher.name || 'Pusher' };
130
- }
131
- else {
243
+ else
132
244
  realPayload.sender = { login: 'GitHub' };
133
- }
134
245
  }
135
- if (!(await this.shouldProcessEvent(event, payload, realPayload, repoName))) {
136
- if (this.config.debug) {
137
- this.ctx.logger('githubsth').info(`Skip duplicated event: ${event} (${repoName || 'unknown'})`);
138
- }
246
+ if (!(await this.shouldProcessEvent(event, payload, realPayload, repoName)))
139
247
  return;
140
- }
141
- try {
142
- this.patchPayloadForEvent(event, realPayload, repoName || 'Unknown/Repo');
143
- repoName = repoName || realPayload.repository?.full_name;
144
- }
145
- catch (error) {
146
- this.ctx.logger('githubsth').warn(`Failed to patch payload for ${event}:`, error);
147
- }
148
- if (!repoName) {
149
- this.ctx.logger('githubsth').warn('Cannot query rules: repoName is missing');
248
+ this.patchPayloadForEvent(event, realPayload, repoName || 'Unknown/Repo');
249
+ repoName = repoName || realPayload.repository?.full_name;
250
+ if (!repoName)
150
251
  return;
151
- }
152
252
  const repoNames = [repoName];
153
253
  if (repoName !== repoName.toLowerCase())
154
254
  repoNames.push(repoName.toLowerCase());
@@ -161,144 +261,235 @@ class Notifier extends koishi_1.Service {
161
261
  const matchedRules = allRules.filter((rule) => rule.events.includes('*') || rule.events.includes(event));
162
262
  if (!matchedRules.length)
163
263
  return;
164
- // De-duplicate same delivery target to avoid double push caused by duplicated rules.
165
264
  const uniqueRules = Array.from(new Map(matchedRules.map((rule) => [`${repoName}|${rule.channelId}`, rule])).values());
166
265
  const textMessage = this.formatByEvent(event, realPayload);
167
266
  if (!textMessage)
168
267
  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
268
  for (const rule of uniqueRules) {
269
+ const theme = this.resolveRuleTheme(rule);
270
+ const outbound = await this.prepareOutboundMessage(textMessage, event, realPayload, theme);
271
+ if (!outbound)
272
+ continue;
176
273
  await this.sendMessage(rule, outbound);
177
274
  }
178
275
  }
276
+ resolveRuleTheme(rule) {
277
+ return this.normalizeTheme(rule.renderTheme) || this.normalizeTheme(this.config.renderTheme) || 'github-dark';
278
+ }
179
279
  formatByEvent(event, payload) {
180
280
  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;
281
+ case 'push': return this.ctx.githubsthFormatter.formatPush(payload);
282
+ case 'issues': return this.ctx.githubsthFormatter.formatIssue(payload);
283
+ case 'pull_request': return this.ctx.githubsthFormatter.formatPullRequest(payload);
284
+ case 'star': return this.ctx.githubsthFormatter.formatStar(payload);
285
+ case 'fork': return this.ctx.githubsthFormatter.formatFork(payload);
286
+ case 'release': return this.ctx.githubsthFormatter.formatRelease(payload);
287
+ case 'discussion': return this.ctx.githubsthFormatter.formatDiscussion(payload);
288
+ case 'workflow_run': return this.ctx.githubsthFormatter.formatWorkflowRun(payload);
289
+ case 'issue_comment': return this.ctx.githubsthFormatter.formatIssueComment(payload);
290
+ case 'pull_request_review': return this.ctx.githubsthFormatter.formatPullRequestReview(payload);
291
+ default: return null;
203
292
  }
204
293
  }
205
- async prepareOutboundMessage(textMessage) {
294
+ async prepareOutboundMessage(textMessage, event, payload, theme) {
206
295
  const mode = this.getRenderMode();
207
296
  if (mode === 'text')
208
- return textMessage;
209
- const image = await this.renderTextAsImage(textMessage);
210
- if (image)
211
- return image;
297
+ return { message: textMessage, text: textMessage, isImage: false };
298
+ const imageMessage = await this.renderTextAsImage(textMessage, event, payload, theme);
299
+ if (imageMessage)
300
+ return { message: imageMessage, text: textMessage, isImage: true };
212
301
  if (mode === 'image' && this.config.renderFallback === 'drop')
213
302
  return null;
214
- return textMessage;
303
+ return { message: textMessage, text: textMessage, isImage: false };
215
304
  }
216
- async renderTextAsImage(textMessage) {
305
+ async renderTextAsImage(textMessage, event, payload, theme) {
217
306
  const puppeteer = this.ctx.puppeteer;
218
307
  if (!puppeteer || typeof puppeteer.render !== 'function')
219
308
  return null;
220
309
  try {
221
- const html = this.buildImageHtml(textMessage);
310
+ const html = this.buildImageHtml(textMessage, event, payload, theme);
222
311
  const task = puppeteer.render(html);
223
312
  const timeout = this.config.renderTimeoutMs || 12000;
224
- const buffer = await Promise.race([
313
+ const rendered = await Promise.race([
225
314
  task,
226
315
  new Promise((resolve) => setTimeout(() => resolve(null), timeout)),
227
316
  ]);
228
- if (!buffer)
317
+ if (!rendered)
229
318
  return null;
230
- return koishi_1.h.image(buffer, 'image/png');
319
+ return this.normalizeRenderedImage(rendered);
231
320
  }
232
321
  catch (error) {
233
322
  this.ctx.logger('githubsth').warn('Image render failed:', error);
234
323
  return null;
235
324
  }
236
325
  }
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";
326
+ normalizeRenderedImage(rendered) {
327
+ if (!rendered)
328
+ return null;
329
+ if (typeof rendered === 'string') {
330
+ const trimmed = rendered.trim();
331
+ if (trimmed.startsWith('<img'))
332
+ return trimmed;
333
+ if (trimmed.startsWith('data:image/'))
334
+ return koishi_1.h.image(trimmed);
335
+ return null;
254
336
  }
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>
337
+ if (node_buffer_1.Buffer.isBuffer(rendered))
338
+ return koishi_1.h.image(rendered, 'image/png');
339
+ if (rendered instanceof Uint8Array)
340
+ return koishi_1.h.image(node_buffer_1.Buffer.from(rendered), 'image/png');
341
+ return null;
342
+ }
343
+ buildImageHtml(textMessage, event, payload, theme) {
344
+ const preset = THEME_PRESETS[theme] || THEME_PRESETS['github-dark'];
345
+ const escapedContent = this.escapeHtml(textMessage).replace(/\n/g, '<br/>');
346
+ const width = this.config.renderWidth || 860;
347
+ const repo = this.escapeHtml(payload?.repository?.full_name || 'unknown/repo');
348
+ const actor = this.escapeHtml(payload?.sender?.login || payload?.pusher?.name || 'github');
349
+ const action = this.escapeHtml(payload?.action || 'updated');
350
+ const title = this.escapeHtml(this.getEventTitle(event, payload));
351
+ const statusPills = this.buildStatusPills(event, payload, preset);
352
+ const commitBlock = this.buildCommitBlock(event, payload, preset);
353
+ return `<!doctype html>
354
+ <html>
355
+ <head>
356
+ <meta charset="utf-8" />
357
+ <style>
358
+ * { box-sizing: border-box; }
359
+ body {
360
+ margin: 0;
361
+ width: ${width}px;
362
+ background: ${preset.background};
363
+ color: ${preset.text};
364
+ font-family: ${preset.font};
365
+ }
366
+ .wrap { padding: 18px; }
367
+ .card {
368
+ border-radius: 14px;
369
+ border: 1px solid ${preset.border};
370
+ background: ${preset.card};
371
+ overflow: hidden;
372
+ box-shadow: 0 10px 30px rgba(0,0,0,0.25);
373
+ }
374
+ .head {
375
+ padding: 12px 16px;
376
+ border-bottom: 1px solid ${preset.border};
377
+ display: flex;
378
+ align-items: center;
379
+ justify-content: space-between;
380
+ gap: 12px;
381
+ }
382
+ .head-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
383
+ .dot { width: 10px; height: 10px; border-radius: 999px; background: ${preset.accent}; box-shadow: 0 0 0 3px ${preset.pillBg}; }
384
+ .repo { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
385
+ .meta { color: ${preset.muted}; font-size: 12px; }
386
+ .pill {
387
+ display: inline-flex;
388
+ align-items: center;
389
+ padding: 2px 9px;
390
+ border-radius: 999px;
391
+ font-size: 12px;
392
+ border: 1px solid ${preset.border};
393
+ color: ${preset.pillText};
394
+ background: ${preset.pillBg};
395
+ margin-left: 6px;
396
+ }
397
+ .body { padding: 14px 16px 16px; }
398
+ .title { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
399
+ .sub { color: ${preset.muted}; margin-bottom: 12px; }
400
+ .content {
401
+ border: 1px dashed ${preset.border};
402
+ border-radius: 10px;
403
+ padding: 10px 12px;
404
+ line-height: 1.6;
405
+ font-size: 14px;
406
+ word-break: break-word;
407
+ margin-top: 8px;
408
+ }
409
+ .commit-list { margin-top: 10px; border-top: 1px solid ${preset.border}; padding-top: 10px; }
410
+ .commit-item { font-size: 13px; margin-bottom: 6px; }
411
+ </style>
412
+ </head>
413
+ <body>
414
+ <div class="wrap">
415
+ <div class="card">
416
+ <div class="head">
417
+ <div class="head-left">
418
+ <span class="dot"></span>
419
+ <span class="repo">${repo}</span>
420
+ </div>
421
+ <div class="meta">${this.escapeHtml(preset.title)}</div>
422
+ </div>
423
+ <div class="body">
424
+ <div class="title">${title}</div>
425
+ <div class="sub">by ${actor}<span class="pill">${action}</span>${statusPills}</div>
426
+ <div class="content">${escapedContent}</div>
427
+ ${commitBlock}
428
+ </div>
429
+ </div>
430
+ </div>
431
+ </body>
300
432
  </html>`;
301
433
  }
434
+ getEventTitle(event, payload) {
435
+ switch (event) {
436
+ case 'push': return '🚀 Push Event';
437
+ case 'issues': return '🐞 Issue Update';
438
+ case 'issue_comment': return payload?.issue?.pull_request ? '💬 Pull Request Comment' : '💬 Issue Comment';
439
+ case 'pull_request': return '🔀 Pull Request Update';
440
+ case 'pull_request_review': return '✅ Pull Request Review';
441
+ case 'release': return '📦 Release Published';
442
+ case 'workflow_run': return '⚙️ Workflow Completed';
443
+ case 'discussion': return '🧵 Discussion Update';
444
+ case 'star': return '⭐ New Star';
445
+ case 'fork': return '🍴 Repository Forked';
446
+ default: return '🔔 GitHub Notification';
447
+ }
448
+ }
449
+ buildStatusPills(event, payload, preset) {
450
+ const pills = [];
451
+ if (event === 'pull_request' && payload?.pull_request?.state) {
452
+ pills.push(payload.pull_request.state);
453
+ }
454
+ if (event === 'workflow_run' && payload?.workflow_run?.conclusion) {
455
+ pills.push(payload.workflow_run.conclusion);
456
+ }
457
+ if (event === 'release' && payload?.release?.tag_name) {
458
+ pills.push(payload.release.tag_name);
459
+ }
460
+ return pills
461
+ .slice(0, 3)
462
+ .map((item) => `<span class="pill" style="margin-left:6px;background:${preset.pillBg};">${this.escapeHtml(String(item))}</span>`)
463
+ .join('');
464
+ }
465
+ buildCommitBlock(event, payload, preset) {
466
+ if (event !== 'push' || !Array.isArray(payload?.commits) || payload.commits.length === 0)
467
+ return '';
468
+ const rows = payload.commits.slice(0, 6).map((commit) => {
469
+ const hash = this.escapeHtml((commit.id || '0000000').slice(0, 7));
470
+ const message = String(commit.message || '').split('\n')[0];
471
+ const icon = this.pickCommitIcon(message);
472
+ const author = this.escapeHtml(commit.author?.name || 'unknown');
473
+ return `<div class="commit-item">${icon} <strong>${hash}</strong> ${this.escapeHtml(message)} <span style="color:${preset.muted}">- ${author}</span></div>`;
474
+ }).join('');
475
+ return `<div class="commit-list">${rows}</div>`;
476
+ }
477
+ pickCommitIcon(message) {
478
+ const lower = (message || '').toLowerCase();
479
+ if (lower.startsWith('feat'))
480
+ return '✨';
481
+ if (lower.startsWith('fix'))
482
+ return '🐛';
483
+ if (lower.startsWith('docs'))
484
+ return '📝';
485
+ if (lower.startsWith('refactor'))
486
+ return '♻️';
487
+ if (lower.startsWith('test'))
488
+ return '✅';
489
+ if (lower.startsWith('chore'))
490
+ return '🧹';
491
+ return '📌';
492
+ }
302
493
  escapeHtml(input) {
303
494
  return String(input)
304
495
  .replace(/&/g, '&amp;')
@@ -314,9 +505,8 @@ class Notifier extends koishi_1.Service {
314
505
  if (parts.length >= 2)
315
506
  repoName = `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
316
507
  }
317
- if (!repoName && realPayload.pull_request?.base?.repo?.full_name) {
508
+ if (!repoName && realPayload.pull_request?.base?.repo?.full_name)
318
509
  repoName = realPayload.pull_request.base.repo.full_name;
319
- }
320
510
  if (!repoName && typeof payload.repoKey === 'string' && payload.repoKey.includes('/'))
321
511
  repoName = payload.repoKey;
322
512
  if (!repoName && typeof payload.owner === 'string' && typeof payload.repo === 'string')
@@ -458,45 +648,41 @@ class Notifier extends koishi_1.Service {
458
648
  createdAt: new Date(),
459
649
  });
460
650
  this.dedupWriteCounter += 1;
461
- if (this.dedupWriteCounter % 200 === 0) {
651
+ if (this.dedupWriteCounter % 200 === 0)
462
652
  void this.cleanupDedupTable();
463
- }
464
653
  }
465
654
  catch (error) {
466
655
  if (error?.code === 'SQLITE_CONSTRAINT')
467
656
  return false;
468
- this.ctx.logger('githubsth').warn('Failed to write dedup record, fallback to in-memory dedup only:', error);
469
657
  }
470
658
  return true;
471
659
  }
472
660
  async cleanupDedupTable() {
473
661
  const cutoff = new Date(Date.now() - this.config.dedupRetentionHours * 60 * 60 * 1000);
474
662
  try {
475
- await this.ctx.database.remove('github_event_dedup', {
476
- createdAt: { $lt: cutoff },
477
- });
478
- }
479
- catch (error) {
480
- if (this.config.debug) {
481
- this.ctx.logger('githubsth').warn('Failed to cleanup dedup table:', error);
482
- }
663
+ await this.ctx.database.remove('github_event_dedup', { createdAt: { $lt: cutoff } });
483
664
  }
665
+ catch { }
484
666
  }
485
- async sendMessage(rule, message) {
667
+ async sendMessage(rule, outbound) {
486
668
  const bots = this.ctx.bots.filter((bot) => !rule.platform || bot.platform === rule.platform);
487
669
  if (!bots.length)
488
670
  return;
489
671
  for (const bot of bots) {
490
672
  try {
491
- await this.sendWithRetry(bot, rule.channelId, message);
492
- if (this.config.debug) {
673
+ await this.sendWithRetry(bot, rule.channelId, outbound.message);
674
+ if (this.config.debug)
493
675
  this.ctx.logger('notifier').info(`Sent message to ${rule.channelId} via ${bot.platform}:${bot.selfId}`);
494
- }
495
676
  return;
496
677
  }
497
678
  catch (error) {
498
- if (this.config.debug) {
499
- this.ctx.logger('notifier').warn(`Bot ${bot.sid} failed to send message with retries: ${error}`);
679
+ if (outbound.isImage && this.config.renderFallback === 'text') {
680
+ try {
681
+ await this.sendWithRetry(bot, rule.channelId, outbound.text);
682
+ this.ctx.logger('notifier').warn(`Image failed on ${bot.platform}:${bot.selfId}, fallback to text succeeded.`);
683
+ return;
684
+ }
685
+ catch { }
500
686
  }
501
687
  }
502
688
  }
@@ -515,8 +701,7 @@ class Notifier extends koishi_1.Service {
515
701
  lastError = error;
516
702
  if (attempt >= retryCount)
517
703
  break;
518
- const delay = baseDelay * Math.pow(2, attempt);
519
- await this.sleep(delay);
704
+ await this.sleep(baseDelay * Math.pow(2, attempt));
520
705
  }
521
706
  }
522
707
  throw lastError;
@@ -525,21 +710,17 @@ class Notifier extends koishi_1.Service {
525
710
  return new Promise((resolve) => setTimeout(resolve, ms));
526
711
  }
527
712
  getPreviewPayload(event) {
528
- const baseRepo = { full_name: 'acmuhan/JackalClientDocs', stargazers_count: 128 };
529
- const baseUser = { login: 'acmuhan' };
530
713
  const payload = {
531
714
  action: 'created',
532
- repository: baseRepo,
533
- sender: baseUser,
715
+ repository: { full_name: 'acmuhan/JackalClientDocs', stargazers_count: 128 },
716
+ sender: { login: 'vercel[bot]' },
534
717
  issue: {
535
718
  number: 29,
536
719
  title: 'Delete demobot',
537
720
  html_url: 'https://github.com/acmuhan/JackalClientDocs/issues/29',
538
721
  pull_request: { html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29' },
539
722
  },
540
- comment: {
541
- body: '[vc]: #gOsUN...=eyJpc01vbm9yZXBvIjp0cnVl...'
542
- },
723
+ comment: { body: '[vc]: #gOsUN...=eyJpc01vbm9yZXBvIjp0cnVl...' },
543
724
  pull_request: {
544
725
  number: 29,
545
726
  title: 'Delete demobot',
@@ -552,9 +733,7 @@ class Notifier extends koishi_1.Service {
552
733
  head_branch: 'main',
553
734
  html_url: 'https://github.com/acmuhan/JackalClientDocs/actions/runs/1',
554
735
  },
555
- commits: [
556
- { id: 'ea5eaddca38f25ce013ee50d70addb49c8d28844', message: 'Delete demobot', author: { name: 'MuHan' } },
557
- ],
736
+ commits: [{ id: 'ea5eaddca38f25ce013ee50d70addb49c8d28844', message: 'feat: delete demobot', author: { name: 'MuHan' } }],
558
737
  ref: 'refs/heads/main',
559
738
  compare: 'https://github.com/acmuhan/JackalClientDocs/compare/old...new',
560
739
  pusher: { name: 'acmuhan' },
@@ -563,8 +742,6 @@ class Notifier extends koishi_1.Service {
563
742
  discussion: { number: 7, title: 'Roadmap', html_url: 'https://github.com/acmuhan/JackalClientDocs/discussions/7' },
564
743
  review: { state: 'approved', html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29#pullrequestreview-1' },
565
744
  };
566
- if (event === 'star')
567
- payload.action = 'created';
568
745
  if (event === 'workflow_run')
569
746
  payload.action = 'completed';
570
747
  if (event === 'pull_request_review')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-githubsth",
3
- "version": "1.0.4",
3
+ "version": "1.0.5-alpha.1",
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",