koishi-plugin-githubsth 1.0.5-alpha.0 โ†’ 1.0.5-alpha.2

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.
@@ -0,0 +1,216 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <style>
6
+ * { box-sizing: border-box; }
7
+ body {
8
+ margin: 0;
9
+ width: {{width}}px;
10
+ background: {{background}};
11
+ color: {{text}};
12
+ font-family: {{font}};
13
+ }
14
+ .wrap {
15
+ padding: 18px;
16
+ position: relative;
17
+ }
18
+ .wrap::before {
19
+ content: '';
20
+ position: absolute;
21
+ inset: 0;
22
+ pointer-events: none;
23
+ background: {{overlayPattern}};
24
+ opacity: 0.5;
25
+ }
26
+ .card {
27
+ position: relative;
28
+ border-radius: 14px;
29
+ border: 1px solid {{border}};
30
+ background: {{card}};
31
+ overflow: hidden;
32
+ box-shadow: 0 12px 34px rgba(0, 0, 0, 0.28);
33
+ z-index: 1;
34
+ }
35
+ .card::before {
36
+ content: '';
37
+ position: absolute;
38
+ inset: 0;
39
+ pointer-events: none;
40
+ background: {{cardTexture}};
41
+ opacity: 0.28;
42
+ }
43
+ .head {
44
+ position: relative;
45
+ z-index: 1;
46
+ padding: 12px 16px;
47
+ border-bottom: 1px solid {{border}};
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ gap: 12px;
52
+ }
53
+ .head-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
54
+ .dot {
55
+ width: 10px;
56
+ height: 10px;
57
+ border-radius: 999px;
58
+ background: {{accent}};
59
+ box-shadow: 0 0 0 3px {{pillBg}};
60
+ }
61
+ .repo {
62
+ font-weight: 700;
63
+ white-space: nowrap;
64
+ overflow: hidden;
65
+ text-overflow: ellipsis;
66
+ letter-spacing: 0.2px;
67
+ }
68
+ .meta {
69
+ color: {{muted}};
70
+ font-size: 12px;
71
+ letter-spacing: 0.3px;
72
+ }
73
+ .pill {
74
+ display: inline-flex;
75
+ align-items: center;
76
+ padding: 2px 9px;
77
+ border-radius: 999px;
78
+ font-size: 12px;
79
+ border: 1px solid {{border}};
80
+ color: {{pillText}};
81
+ background: {{pillBg}};
82
+ margin-left: 6px;
83
+ }
84
+ .body {
85
+ position: relative;
86
+ z-index: 1;
87
+ padding: 14px 16px 16px;
88
+ }
89
+ .title {
90
+ font-size: 18px;
91
+ font-weight: 700;
92
+ margin-bottom: 6px;
93
+ letter-spacing: 0.2px;
94
+ }
95
+ .sub {
96
+ color: {{muted}};
97
+ margin-bottom: 12px;
98
+ }
99
+ .content {
100
+ border: 1px dashed {{border}};
101
+ border-radius: 10px;
102
+ padding: 10px 12px;
103
+ line-height: 1.62;
104
+ font-size: 14px;
105
+ word-break: break-word;
106
+ margin-top: 8px;
107
+ background: {{contentBg}};
108
+ }
109
+ .commit-list {
110
+ margin-top: 10px;
111
+ border-top: 1px solid {{border}};
112
+ padding-top: 10px;
113
+ }
114
+ .commit-item {
115
+ display: flex;
116
+ align-items: baseline;
117
+ gap: 6px;
118
+ font-size: 13px;
119
+ margin-bottom: 6px;
120
+ }
121
+ .commit-icon { width: 18px; text-align: center; }
122
+ .commit-hash {
123
+ display: inline-block;
124
+ padding: 1px 6px;
125
+ border-radius: 8px;
126
+ border: 1px solid {{border}};
127
+ background: {{pillBg}};
128
+ color: {{pillText}};
129
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
130
+ }
131
+ .commit-author { color: {{muted}}; }
132
+
133
+ body.style-glass .card {
134
+ border-radius: 18px;
135
+ backdrop-filter: blur(6px);
136
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
137
+ }
138
+ body.style-glass .head {
139
+ background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0));
140
+ }
141
+
142
+ body.style-neon .card {
143
+ border-width: 2px;
144
+ box-shadow: 0 0 0 1px {{accent}} inset, 0 0 28px rgba(0,0,0,0.38);
145
+ }
146
+ body.style-neon .dot {
147
+ box-shadow: 0 0 0 4px {{pillBg}}, 0 0 14px {{accent}};
148
+ }
149
+ body.style-neon .title {
150
+ text-transform: uppercase;
151
+ letter-spacing: 0.8px;
152
+ }
153
+
154
+ body.style-compact .wrap { padding: 10px; }
155
+ body.style-compact .head { padding: 10px 12px; }
156
+ body.style-compact .body { padding: 10px 12px 12px; }
157
+ body.style-compact .title { font-size: 16px; }
158
+ body.style-compact .content { font-size: 13px; }
159
+
160
+ body.style-card .card {
161
+ border-radius: 24px;
162
+ border-width: 1px;
163
+ box-shadow: 0 26px 54px rgba(0, 0, 0, 0.35);
164
+ }
165
+ body.style-card .head {
166
+ border-bottom-style: dashed;
167
+ }
168
+ body.style-card .content {
169
+ border-style: solid;
170
+ }
171
+
172
+ body.style-terminal .card {
173
+ border-radius: 0;
174
+ border-width: 2px;
175
+ box-shadow: none;
176
+ }
177
+ body.style-terminal .head,
178
+ body.style-terminal .content,
179
+ body.style-terminal .pill,
180
+ body.style-terminal .commit-hash {
181
+ border-radius: 0;
182
+ }
183
+ body.style-terminal .title {
184
+ font-size: 17px;
185
+ letter-spacing: 0.5px;
186
+ }
187
+
188
+ body.style-github .repo::before {
189
+ content: '#';
190
+ margin-right: 4px;
191
+ color: {{muted}};
192
+ }
193
+
194
+ {{extraCss}}
195
+ </style>
196
+ </head>
197
+ <body class="theme-{{themeKey}} style-{{styleVariant}}">
198
+ <div class="wrap">
199
+ <div class="card">
200
+ <div class="head">
201
+ <div class="head-left">
202
+ <span class="dot"></span>
203
+ <span class="repo">{{repo}}</span>
204
+ </div>
205
+ <div class="meta">{{themeTitle}}</div>
206
+ </div>
207
+ <div class="body">
208
+ <div class="title">{{eventTitle}}</div>
209
+ <div class="sub">by {{actor}}<span class="pill">{{action}}</span>{{statusPills}}</div>
210
+ <div class="content">{{content}}</div>
211
+ {{commitBlock}}
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </body>
216
+ </html>
@@ -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,26 +13,27 @@ 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;
33
36
  private normalizeRenderedImage;
34
- private buildImageHtml;
35
- private escapeHtml;
36
37
  private extractRepoName;
37
38
  private patchPayloadForEvent;
38
39
  private buildEventDedupKey;
@@ -1,7 +1,9 @@
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 render_card_1 = require("./render-card");
5
7
  class Notifier extends koishi_1.Service {
6
8
  // @ts-ignore
7
9
  constructor(ctx, config) {
@@ -14,6 +16,12 @@ class Notifier extends koishi_1.Service {
14
16
  this.ctx.logger('githubsth').info('Notifier service initialized');
15
17
  this.registerListeners();
16
18
  }
19
+ listThemes() {
20
+ return (0, render_card_1.listRenderThemes)();
21
+ }
22
+ normalizeTheme(theme) {
23
+ return (0, render_card_1.normalizeRenderTheme)(theme);
24
+ }
17
25
  setRenderMode(mode) {
18
26
  this.runtimeRenderMode = mode;
19
27
  }
@@ -32,10 +40,12 @@ class Notifier extends koishi_1.Service {
32
40
  hasPuppeteer: Boolean(puppeteer?.render),
33
41
  };
34
42
  }
35
- async renderPreview(event = 'issue_comment') {
43
+ async renderPreview(event = 'issue_comment', theme) {
36
44
  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);
45
+ const text = this.formatByEvent(event, payload)
46
+ || this.formatByEvent('issue_comment', this.getPreviewPayload('issue_comment'))
47
+ || 'Preview unavailable.';
48
+ const preview = await this.prepareOutboundMessage(text, event, payload, theme || this.config.renderTheme);
39
49
  return preview?.message || null;
40
50
  }
41
51
  registerListeners() {
@@ -52,7 +62,6 @@ class Notifier extends koishi_1.Service {
52
62
  bind('github/fork', 'fork');
53
63
  bind('github/release', 'release');
54
64
  bind('github/discussion', 'discussion');
55
- // legacy aliases
56
65
  bind('github/issues', 'issues');
57
66
  bind('github/pull_request', 'pull_request');
58
67
  bind('github/workflow_run', 'workflow_run');
@@ -64,9 +73,6 @@ class Notifier extends koishi_1.Service {
64
73
  const payload = session.payload || session.extra || session.data;
65
74
  if (!payload)
66
75
  return;
67
- if (this.config.debug) {
68
- this.ctx.logger('githubsth').info('Found payload in session, attempting to handle');
69
- }
70
76
  const realPayload = payload.payload || payload;
71
77
  let eventType = 'unknown';
72
78
  if (realPayload.issue && realPayload.comment)
@@ -91,12 +97,8 @@ class Notifier extends koishi_1.Service {
91
97
  eventType = 'workflow_run';
92
98
  else if (realPayload.repository && (realPayload.action === 'created' || realPayload.action === 'started'))
93
99
  eventType = 'star';
94
- if (eventType !== 'unknown') {
100
+ if (eventType !== 'unknown')
95
101
  void this.handleEvent(eventType, payload);
96
- }
97
- else if (this.config.logUnhandledEvents) {
98
- this.ctx.logger('githubsth').info(`Unhandled payload structure. Keys: ${Object.keys(realPayload).join(', ')}`);
99
- }
100
102
  });
101
103
  }
102
104
  }
@@ -110,46 +112,28 @@ class Notifier extends koishi_1.Service {
110
112
  realPayload.repository = payload.repository;
111
113
  }
112
114
  let repoName = this.extractRepoName(payload, realPayload, event);
113
- if (!realPayload.repository) {
115
+ if (!realPayload.repository)
114
116
  realPayload.repository = { full_name: repoName || 'Unknown/Repo' };
115
- }
116
- else if (!realPayload.repository.full_name) {
117
+ else if (!realPayload.repository.full_name)
117
118
  realPayload.repository.full_name = repoName || 'Unknown/Repo';
118
- }
119
119
  if (!realPayload.sender) {
120
- if (realPayload.issue?.user) {
120
+ if (realPayload.issue?.user)
121
121
  realPayload.sender = realPayload.issue.user;
122
- }
123
- else if (realPayload.pull_request?.user) {
122
+ else if (realPayload.pull_request?.user)
124
123
  realPayload.sender = realPayload.pull_request.user;
125
- }
126
- else if (realPayload.discussion?.user) {
124
+ else if (realPayload.discussion?.user)
127
125
  realPayload.sender = realPayload.discussion.user;
128
- }
129
- else if (realPayload.pusher) {
126
+ else if (realPayload.pusher)
130
127
  realPayload.sender = { login: realPayload.pusher.name || 'Pusher' };
131
- }
132
- else {
128
+ else
133
129
  realPayload.sender = { login: 'GitHub' };
134
- }
135
130
  }
136
- if (!(await this.shouldProcessEvent(event, payload, realPayload, repoName))) {
137
- if (this.config.debug) {
138
- this.ctx.logger('githubsth').info(`Skip duplicated event: ${event} (${repoName || 'unknown'})`);
139
- }
131
+ if (!(await this.shouldProcessEvent(event, payload, realPayload, repoName)))
140
132
  return;
141
- }
142
- try {
143
- this.patchPayloadForEvent(event, realPayload, repoName || 'Unknown/Repo');
144
- repoName = repoName || realPayload.repository?.full_name;
145
- }
146
- catch (error) {
147
- this.ctx.logger('githubsth').warn(`Failed to patch payload for ${event}:`, error);
148
- }
149
- if (!repoName) {
150
- this.ctx.logger('githubsth').warn('Cannot query rules: repoName is missing');
133
+ this.patchPayloadForEvent(event, realPayload, repoName || 'Unknown/Repo');
134
+ repoName = repoName || realPayload.repository?.full_name;
135
+ if (!repoName)
151
136
  return;
152
- }
153
137
  const repoNames = [repoName];
154
138
  if (repoName !== repoName.toLowerCase())
155
139
  repoNames.push(repoName.toLowerCase());
@@ -162,66 +146,53 @@ class Notifier extends koishi_1.Service {
162
146
  const matchedRules = allRules.filter((rule) => rule.events.includes('*') || rule.events.includes(event));
163
147
  if (!matchedRules.length)
164
148
  return;
165
- // De-duplicate same delivery target to avoid double push caused by duplicated rules.
166
149
  const uniqueRules = Array.from(new Map(matchedRules.map((rule) => [`${repoName}|${rule.channelId}`, rule])).values());
167
150
  const textMessage = this.formatByEvent(event, realPayload);
168
151
  if (!textMessage)
169
152
  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
153
  for (const rule of uniqueRules) {
154
+ const theme = this.resolveRuleTheme(rule);
155
+ const outbound = await this.prepareOutboundMessage(textMessage, event, realPayload, theme);
156
+ if (!outbound)
157
+ continue;
177
158
  await this.sendMessage(rule, outbound);
178
159
  }
179
160
  }
161
+ resolveRuleTheme(rule) {
162
+ return this.normalizeTheme(rule.renderTheme) || this.normalizeTheme(this.config.renderTheme) || 'github-dark';
163
+ }
180
164
  formatByEvent(event, payload) {
181
165
  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;
166
+ case 'push': return this.ctx.githubsthFormatter.formatPush(payload);
167
+ case 'issues': return this.ctx.githubsthFormatter.formatIssue(payload);
168
+ case 'pull_request': return this.ctx.githubsthFormatter.formatPullRequest(payload);
169
+ case 'star': return this.ctx.githubsthFormatter.formatStar(payload);
170
+ case 'fork': return this.ctx.githubsthFormatter.formatFork(payload);
171
+ case 'release': return this.ctx.githubsthFormatter.formatRelease(payload);
172
+ case 'discussion': return this.ctx.githubsthFormatter.formatDiscussion(payload);
173
+ case 'workflow_run': return this.ctx.githubsthFormatter.formatWorkflowRun(payload);
174
+ case 'issue_comment': return this.ctx.githubsthFormatter.formatIssueComment(payload);
175
+ case 'pull_request_review': return this.ctx.githubsthFormatter.formatPullRequestReview(payload);
176
+ default: return null;
204
177
  }
205
178
  }
206
- async prepareOutboundMessage(textMessage) {
179
+ async prepareOutboundMessage(textMessage, event, payload, theme) {
207
180
  const mode = this.getRenderMode();
208
- if (mode === 'text') {
181
+ if (mode === 'text')
209
182
  return { message: textMessage, text: textMessage, isImage: false };
210
- }
211
- const imageMessage = await this.renderTextAsImage(textMessage);
212
- if (imageMessage) {
183
+ const imageMessage = await this.renderTextAsImage(textMessage, event, payload, theme);
184
+ if (imageMessage)
213
185
  return { message: imageMessage, text: textMessage, isImage: true };
214
- }
215
186
  if (mode === 'image' && this.config.renderFallback === 'drop')
216
187
  return null;
217
188
  return { message: textMessage, text: textMessage, isImage: false };
218
189
  }
219
- async renderTextAsImage(textMessage) {
190
+ async renderTextAsImage(textMessage, event, payload, theme) {
220
191
  const puppeteer = this.ctx.puppeteer;
221
192
  if (!puppeteer || typeof puppeteer.render !== 'function')
222
193
  return null;
223
194
  try {
224
- const html = this.buildImageHtml(textMessage);
195
+ const html = (0, render_card_1.buildRenderHtml)(textMessage, event, payload, theme, this.config.renderWidth || 860);
225
196
  const task = puppeteer.render(html);
226
197
  const timeout = this.config.renderTimeoutMs || 12000;
227
198
  const rendered = await Promise.race([
@@ -248,85 +219,12 @@ class Notifier extends koishi_1.Service {
248
219
  return koishi_1.h.image(trimmed);
249
220
  return null;
250
221
  }
251
- if (Buffer.isBuffer(rendered))
222
+ if (node_buffer_1.Buffer.isBuffer(rendered))
252
223
  return koishi_1.h.image(rendered, 'image/png');
253
224
  if (rendered instanceof Uint8Array)
254
- return koishi_1.h.image(Buffer.from(rendered), 'image/png');
225
+ return koishi_1.h.image(node_buffer_1.Buffer.from(rendered), 'image/png');
255
226
  return null;
256
227
  }
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;');
329
- }
330
228
  extractRepoName(payload, realPayload, event) {
331
229
  let repoName = realPayload.repository?.full_name;
332
230
  if (!repoName && realPayload.issue?.repository_url) {
@@ -334,9 +232,8 @@ class Notifier extends koishi_1.Service {
334
232
  if (parts.length >= 2)
335
233
  repoName = `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
336
234
  }
337
- if (!repoName && realPayload.pull_request?.base?.repo?.full_name) {
235
+ if (!repoName && realPayload.pull_request?.base?.repo?.full_name)
338
236
  repoName = realPayload.pull_request.base.repo.full_name;
339
- }
340
237
  if (!repoName && typeof payload.repoKey === 'string' && payload.repoKey.includes('/'))
341
238
  repoName = payload.repoKey;
342
239
  if (!repoName && typeof payload.owner === 'string' && typeof payload.repo === 'string')
@@ -478,29 +375,21 @@ class Notifier extends koishi_1.Service {
478
375
  createdAt: new Date(),
479
376
  });
480
377
  this.dedupWriteCounter += 1;
481
- if (this.dedupWriteCounter % 200 === 0) {
378
+ if (this.dedupWriteCounter % 200 === 0)
482
379
  void this.cleanupDedupTable();
483
- }
484
380
  }
485
381
  catch (error) {
486
382
  if (error?.code === 'SQLITE_CONSTRAINT')
487
383
  return false;
488
- this.ctx.logger('githubsth').warn('Failed to write dedup record, fallback to in-memory dedup only:', error);
489
384
  }
490
385
  return true;
491
386
  }
492
387
  async cleanupDedupTable() {
493
388
  const cutoff = new Date(Date.now() - this.config.dedupRetentionHours * 60 * 60 * 1000);
494
389
  try {
495
- await this.ctx.database.remove('github_event_dedup', {
496
- createdAt: { $lt: cutoff },
497
- });
498
- }
499
- catch (error) {
500
- if (this.config.debug) {
501
- this.ctx.logger('githubsth').warn('Failed to cleanup dedup table:', error);
502
- }
390
+ await this.ctx.database.remove('github_event_dedup', { createdAt: { $lt: cutoff } });
503
391
  }
392
+ catch { }
504
393
  }
505
394
  async sendMessage(rule, outbound) {
506
395
  const bots = this.ctx.bots.filter((bot) => !rule.platform || bot.platform === rule.platform);
@@ -520,13 +409,8 @@ class Notifier extends koishi_1.Service {
520
409
  this.ctx.logger('notifier').warn(`Image failed on ${bot.platform}:${bot.selfId}, fallback to text succeeded.`);
521
410
  return;
522
411
  }
523
- catch (fallbackError) {
524
- if (this.config.debug)
525
- this.ctx.logger('notifier').warn(`Fallback text send failed: ${fallbackError}`);
526
- }
412
+ catch { }
527
413
  }
528
- if (this.config.debug)
529
- this.ctx.logger('notifier').warn(`Bot ${bot.sid} failed to send message with retries: ${error}`);
530
414
  }
531
415
  }
532
416
  this.ctx.logger('notifier').warn(`Failed to send message to ${rule.channelId}`);
@@ -544,8 +428,7 @@ class Notifier extends koishi_1.Service {
544
428
  lastError = error;
545
429
  if (attempt >= retryCount)
546
430
  break;
547
- const delay = baseDelay * Math.pow(2, attempt);
548
- await this.sleep(delay);
431
+ await this.sleep(baseDelay * Math.pow(2, attempt));
549
432
  }
550
433
  }
551
434
  throw lastError;
@@ -554,21 +437,17 @@ class Notifier extends koishi_1.Service {
554
437
  return new Promise((resolve) => setTimeout(resolve, ms));
555
438
  }
556
439
  getPreviewPayload(event) {
557
- const baseRepo = { full_name: 'acmuhan/JackalClientDocs', stargazers_count: 128 };
558
- const baseUser = { login: 'acmuhan' };
559
440
  const payload = {
560
441
  action: 'created',
561
- repository: baseRepo,
562
- sender: baseUser,
442
+ repository: { full_name: 'acmuhan/JackalClientDocs', stargazers_count: 128 },
443
+ sender: { login: 'vercel[bot]' },
563
444
  issue: {
564
445
  number: 29,
565
446
  title: 'Delete demobot',
566
447
  html_url: 'https://github.com/acmuhan/JackalClientDocs/issues/29',
567
448
  pull_request: { html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29' },
568
449
  },
569
- comment: {
570
- body: '[vc]: #gOsUN...=eyJpc01vbm9yZXBvIjp0cnVl...'
571
- },
450
+ comment: { body: '[vc]: #gOsUN...=eyJpc01vbm9yZXBvIjp0cnVl...' },
572
451
  pull_request: {
573
452
  number: 29,
574
453
  title: 'Delete demobot',
@@ -581,9 +460,7 @@ class Notifier extends koishi_1.Service {
581
460
  head_branch: 'main',
582
461
  html_url: 'https://github.com/acmuhan/JackalClientDocs/actions/runs/1',
583
462
  },
584
- commits: [
585
- { id: 'ea5eaddca38f25ce013ee50d70addb49c8d28844', message: 'Delete demobot', author: { name: 'MuHan' } },
586
- ],
463
+ commits: [{ id: 'ea5eaddca38f25ce013ee50d70addb49c8d28844', message: 'feat: delete demobot', author: { name: 'MuHan' } }],
587
464
  ref: 'refs/heads/main',
588
465
  compare: 'https://github.com/acmuhan/JackalClientDocs/compare/old...new',
589
466
  pusher: { name: 'acmuhan' },
@@ -592,8 +469,6 @@ class Notifier extends koishi_1.Service {
592
469
  discussion: { number: 7, title: 'Roadmap', html_url: 'https://github.com/acmuhan/JackalClientDocs/discussions/7' },
593
470
  review: { state: 'approved', html_url: 'https://github.com/acmuhan/JackalClientDocs/pull/29#pullrequestreview-1' },
594
471
  };
595
- if (event === 'star')
596
- payload.action = 'created';
597
472
  if (event === 'workflow_run')
598
473
  payload.action = 'completed';
599
474
  if (event === 'pull_request_review')
@@ -0,0 +1,24 @@
1
+ import type { RenderTheme } from '../config';
2
+ type StyleVariant = 'github' | 'glass' | 'neon' | 'compact' | 'card' | 'terminal';
3
+ export type ThemePreset = {
4
+ key: string;
5
+ title: string;
6
+ style: StyleVariant;
7
+ background: string;
8
+ card: string;
9
+ border: string;
10
+ text: string;
11
+ muted: string;
12
+ accent: string;
13
+ pillText: string;
14
+ pillBg: string;
15
+ contentBg: string;
16
+ overlayPattern: string;
17
+ cardTexture: string;
18
+ extraCss: string;
19
+ font: string;
20
+ };
21
+ export declare function listRenderThemes(): RenderTheme[];
22
+ export declare function normalizeRenderTheme(theme?: string | null): RenderTheme | null;
23
+ export declare function buildRenderHtml(textMessage: string, event: string, payload: any, theme: RenderTheme, width: number): string;
24
+ export {};
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listRenderThemes = listRenderThemes;
4
+ exports.normalizeRenderTheme = normalizeRenderTheme;
5
+ exports.buildRenderHtml = buildRenderHtml;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ const NO_PATTERN = 'none';
9
+ const NO_TEXTURE = 'none';
10
+ const THEME_PRESETS = {
11
+ 'github-light': {
12
+ key: 'github-light', title: 'GitHub Light', style: 'github',
13
+ background: 'linear-gradient(145deg, #f6f8fa, #eef2f7)', card: '#ffffff', border: '#d0d7de',
14
+ text: '#24292f', muted: '#57606a', accent: '#0969da', pillText: '#0969da', pillBg: '#ddf4ff',
15
+ contentBg: 'rgba(246,248,250,0.72)',
16
+ overlayPattern: NO_PATTERN,
17
+ cardTexture: NO_TEXTURE,
18
+ extraCss: '.theme-github-light .head { background: linear-gradient(180deg, #ffffff, #f9fbfd); }',
19
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
20
+ },
21
+ 'github-dark': {
22
+ key: 'github-dark', title: 'GitHub Dark', style: 'github',
23
+ background: 'linear-gradient(145deg, #0d1117, #161b22)', card: '#161b22', border: '#30363d',
24
+ text: '#c9d1d9', muted: '#8b949e', accent: '#58a6ff', pillText: '#58a6ff', pillBg: 'rgba(56,139,253,0.15)',
25
+ contentBg: 'rgba(13,17,23,0.45)',
26
+ overlayPattern: NO_PATTERN,
27
+ cardTexture: NO_TEXTURE,
28
+ extraCss: '.theme-github-dark .head { background: linear-gradient(180deg, rgba(88,166,255,0.07), rgba(22,27,34,0)); }',
29
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
30
+ },
31
+ aurora: {
32
+ key: 'aurora', title: 'Aurora', style: 'glass',
33
+ background: 'linear-gradient(155deg, #07203f, #2b5876)', card: 'rgba(12,24,42,0.86)', border: 'rgba(143,211,244,0.35)',
34
+ text: '#e8f5ff', muted: '#b7d7ef', accent: '#7dd3fc', pillText: '#7dd3fc', pillBg: 'rgba(125,211,252,0.16)',
35
+ contentBg: 'rgba(10,21,37,0.5)',
36
+ overlayPattern: 'radial-gradient(circle at 18% 15%, rgba(125,211,252,0.24), transparent 30%), radial-gradient(circle at 82% 80%, rgba(56,189,248,0.24), transparent 26%)',
37
+ cardTexture: 'linear-gradient(135deg, rgba(255,255,255,0.14) 0%, transparent 32%, rgba(255,255,255,0.06) 70%, transparent 100%)',
38
+ extraCss: '.theme-aurora .title { text-shadow: 0 2px 16px rgba(125,211,252,0.25); }',
39
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
40
+ },
41
+ sunset: {
42
+ key: 'sunset', title: 'Sunset', style: 'card',
43
+ background: 'linear-gradient(155deg, #3f2b96, #a83279)', card: 'rgba(46,16,57,0.9)', border: 'rgba(255,183,197,0.34)',
44
+ text: '#fff1f6', muted: '#ffd2e4', accent: '#fda4af', pillText: '#fecdd3', pillBg: 'rgba(253,164,175,0.18)',
45
+ contentBg: 'rgba(86,28,84,0.32)',
46
+ overlayPattern: 'radial-gradient(circle at 85% 16%, rgba(255,219,234,0.28), transparent 28%), radial-gradient(circle at 10% 78%, rgba(249,115,22,0.2), transparent 34%)',
47
+ cardTexture: 'linear-gradient(160deg, rgba(255,255,255,0.12), transparent 46%)',
48
+ extraCss: '.theme-sunset .head { background: linear-gradient(120deg, rgba(253,164,175,0.2), rgba(168,85,247,0.12)); }',
49
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
50
+ },
51
+ matrix: {
52
+ key: 'matrix', title: 'Matrix', style: 'neon',
53
+ background: '#07140d', card: '#0a1e13', border: '#1f5136',
54
+ text: '#b8f7cc', muted: '#7fd79f', accent: '#34d399', pillText: '#34d399', pillBg: 'rgba(52,211,153,0.16)',
55
+ contentBg: 'rgba(9,30,18,0.62)',
56
+ overlayPattern: 'repeating-linear-gradient(180deg, rgba(52,211,153,0.06), rgba(52,211,153,0.06) 1px, transparent 1px, transparent 6px)',
57
+ cardTexture: 'linear-gradient(0deg, rgba(52,211,153,0.07), transparent 40%)',
58
+ extraCss: '.theme-matrix .commit-item { letter-spacing: 0.2px; } .theme-matrix .title { color: #8af5b7; }',
59
+ font: "'Consolas', 'Courier New', monospace",
60
+ },
61
+ compact: {
62
+ key: 'compact', title: 'Compact', style: 'compact',
63
+ background: 'linear-gradient(145deg, #1f2937, #111827)', card: 'rgba(17,24,39,0.92)', border: 'rgba(148,163,184,0.30)',
64
+ text: '#e5e7eb', muted: '#94a3b8', accent: '#22d3ee', pillText: '#22d3ee', pillBg: 'rgba(34,211,238,0.15)',
65
+ contentBg: 'rgba(15,23,42,0.5)',
66
+ overlayPattern: NO_PATTERN,
67
+ cardTexture: NO_TEXTURE,
68
+ extraCss: '.theme-compact .meta { display: none; } .theme-compact .commit-item { margin-bottom: 4px; }',
69
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
70
+ },
71
+ card: {
72
+ key: 'card', title: 'Card', style: 'card',
73
+ background: 'linear-gradient(145deg, #0f172a, #1e293b)', card: 'rgba(15,23,42,0.92)', border: 'rgba(148,163,184,0.30)',
74
+ text: '#e2e8f0', muted: '#94a3b8', accent: '#60a5fa', pillText: '#60a5fa', pillBg: 'rgba(96,165,250,0.16)',
75
+ contentBg: 'rgba(30,41,59,0.42)',
76
+ overlayPattern: 'radial-gradient(circle at 80% 20%, rgba(96,165,250,0.2), transparent 35%)',
77
+ cardTexture: 'linear-gradient(180deg, rgba(255,255,255,0.07), transparent 18%)',
78
+ extraCss: '.theme-card .title { font-size: 20px; } .theme-card .pill { border-width: 1px; }',
79
+ font: "'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
80
+ },
81
+ terminal: {
82
+ key: 'terminal', title: 'Terminal', style: 'terminal',
83
+ background: '#0b1020', card: '#0b1220', border: '#1f2a44',
84
+ text: '#d1fae5', muted: '#6ee7b7', accent: '#34d399', pillText: '#34d399', pillBg: 'rgba(52,211,153,0.18)',
85
+ contentBg: 'rgba(8,15,28,0.62)',
86
+ overlayPattern: 'repeating-linear-gradient(0deg, transparent, transparent 23px, rgba(52,211,153,0.08) 24px)',
87
+ cardTexture: NO_TEXTURE,
88
+ extraCss: '.theme-terminal .repo, .theme-terminal .title { text-transform: uppercase; }',
89
+ font: "'Consolas', 'Courier New', monospace",
90
+ },
91
+ };
92
+ const THEME_ALIASES = {
93
+ 'gh-light': 'github-light',
94
+ 'gh-dark': 'github-dark',
95
+ };
96
+ let templateCache = '';
97
+ function getTemplate() {
98
+ if (!templateCache) {
99
+ const templatePath = (0, node_path_1.join)(__dirname, '..', 'assets', 'render-card.html');
100
+ templateCache = (0, node_fs_1.readFileSync)(templatePath, 'utf8');
101
+ }
102
+ return templateCache;
103
+ }
104
+ function escapeHtml(input) {
105
+ return String(input)
106
+ .replace(/&/g, '&amp;')
107
+ .replace(/</g, '&lt;')
108
+ .replace(/>/g, '&gt;')
109
+ .replace(/"/g, '&quot;')
110
+ .replace(/'/g, '&#39;');
111
+ }
112
+ function pickCommitIcon(message) {
113
+ const lower = (message || '').toLowerCase();
114
+ if (lower.startsWith('feat'))
115
+ return 'โœจ';
116
+ if (lower.startsWith('fix'))
117
+ return '๐Ÿ›';
118
+ if (lower.startsWith('docs'))
119
+ return '๐Ÿ“';
120
+ if (lower.startsWith('refactor'))
121
+ return 'โ™ป๏ธ';
122
+ if (lower.startsWith('test'))
123
+ return 'โœ…';
124
+ if (lower.startsWith('chore'))
125
+ return '๐Ÿงน';
126
+ return '๐Ÿ“Œ';
127
+ }
128
+ function getEventTitle(event, payload) {
129
+ switch (event) {
130
+ case 'push': return '๐Ÿš€ Push Event';
131
+ case 'issues': return '๐Ÿž Issue Update';
132
+ case 'issue_comment': return payload?.issue?.pull_request ? '๐Ÿ’ฌ Pull Request Comment' : '๐Ÿ’ฌ Issue Comment';
133
+ case 'pull_request': return '๐Ÿ”€ Pull Request Update';
134
+ case 'pull_request_review': return 'โœ… Pull Request Review';
135
+ case 'release': return '๐Ÿ“ฆ Release Published';
136
+ case 'workflow_run': return 'โš™๏ธ Workflow Completed';
137
+ case 'discussion': return '๐Ÿงต Discussion Update';
138
+ case 'star': return 'โญ New Star';
139
+ case 'fork': return '๐Ÿด Repository Forked';
140
+ default: return '๐Ÿ”” GitHub Notification';
141
+ }
142
+ }
143
+ function buildStatusPills(event, payload) {
144
+ const pills = [];
145
+ if (event === 'pull_request' && payload?.pull_request?.state)
146
+ pills.push(payload.pull_request.state);
147
+ if (event === 'workflow_run' && payload?.workflow_run?.conclusion)
148
+ pills.push(payload.workflow_run.conclusion);
149
+ if (event === 'release' && payload?.release?.tag_name)
150
+ pills.push(payload.release.tag_name);
151
+ return pills
152
+ .slice(0, 3)
153
+ .map((item) => `<span class="pill">${escapeHtml(String(item))}</span>`)
154
+ .join('');
155
+ }
156
+ function buildCommitBlock(event, payload) {
157
+ if (event !== 'push' || !Array.isArray(payload?.commits) || payload.commits.length === 0)
158
+ return '';
159
+ const rows = payload.commits.slice(0, 6).map((commit) => {
160
+ const hash = escapeHtml((commit.id || '0000000').slice(0, 7));
161
+ const message = String(commit.message || '').split('\n')[0];
162
+ const icon = pickCommitIcon(message);
163
+ const author = escapeHtml(commit.author?.name || 'unknown');
164
+ return `<div class="commit-item"><span class="commit-icon">${icon}</span><span class="commit-hash">${hash}</span><span class="commit-msg">${escapeHtml(message)}</span><span class="commit-author">- ${author}</span></div>`;
165
+ }).join('');
166
+ return `<div class="commit-list">${rows}</div>`;
167
+ }
168
+ function listRenderThemes() {
169
+ return Object.keys(THEME_PRESETS);
170
+ }
171
+ function normalizeRenderTheme(theme) {
172
+ if (!theme)
173
+ return null;
174
+ const key = theme.trim().toLowerCase();
175
+ const direct = listRenderThemes().find((item) => item === key);
176
+ if (direct)
177
+ return direct;
178
+ return THEME_ALIASES[key] || null;
179
+ }
180
+ function buildRenderHtml(textMessage, event, payload, theme, width) {
181
+ const preset = THEME_PRESETS[theme] || THEME_PRESETS['github-dark'];
182
+ const content = escapeHtml(textMessage).replace(/\n/g, '<br/>');
183
+ const repo = escapeHtml(payload?.repository?.full_name || 'unknown/repo');
184
+ const actor = escapeHtml(payload?.sender?.login || payload?.pusher?.name || 'github');
185
+ const action = escapeHtml(payload?.action || 'updated');
186
+ const eventTitle = escapeHtml(getEventTitle(event, payload));
187
+ const statusPills = buildStatusPills(event, payload);
188
+ const commitBlock = buildCommitBlock(event, payload);
189
+ const variables = {
190
+ width: String(width || 860),
191
+ themeKey: preset.key,
192
+ styleVariant: preset.style,
193
+ background: preset.background,
194
+ card: preset.card,
195
+ border: preset.border,
196
+ text: preset.text,
197
+ muted: preset.muted,
198
+ accent: preset.accent,
199
+ pillText: preset.pillText,
200
+ pillBg: preset.pillBg,
201
+ contentBg: preset.contentBg,
202
+ overlayPattern: preset.overlayPattern,
203
+ cardTexture: preset.cardTexture,
204
+ extraCss: preset.extraCss,
205
+ font: preset.font,
206
+ repo,
207
+ themeTitle: escapeHtml(preset.title),
208
+ eventTitle,
209
+ actor,
210
+ action,
211
+ statusPills,
212
+ content,
213
+ commitBlock,
214
+ };
215
+ let html = getTemplate();
216
+ for (const [key, value] of Object.entries(variables)) {
217
+ html = html.replace(new RegExp(`{{${key}}}`, 'g'), value);
218
+ }
219
+ return html;
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-githubsth",
3
- "version": "1.0.5-alpha.0",
3
+ "version": "1.0.5-alpha.2",
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",
@@ -10,7 +10,7 @@
10
10
  ],
11
11
  "license": "MIT",
12
12
  "scripts": {
13
- "build": "tsc",
13
+ "build": "tsc && node scripts/copy-assets.js",
14
14
  "dev": "tsc -w",
15
15
  "test": "jest"
16
16
  },
@@ -35,7 +35,7 @@
35
35
  "koishi": {
36
36
  "description": {
37
37
  "en": "Github Subscriptions Notifications, push notifications for GitHub subscriptions",
38
- "zh": "GitHub่ฎข้˜…ๆŽจ้€้€š็Ÿฅ,ไพ่ต–ไบŽGithub้€‚้…ๅ™จ"
38
+ "zh": "GitHub ่ฎข้˜…ๆŽจ้€้€š็Ÿฅ๏ผŒไพ่ต– GitHub ้€‚้…ๅ™จ"
39
39
  },
40
40
  "service": {
41
41
  "required": [
@@ -47,3 +47,5 @@
47
47
  }
48
48
  }
49
49
  }
50
+
51
+