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.
- package/lib/commands/render.js +70 -11
- package/lib/commands/subscribe.js +34 -62
- package/lib/config.d.ts +4 -3
- package/lib/config.js +20 -5
- package/lib/database.d.ts +2 -0
- package/lib/database.js +1 -0
- package/lib/services/notifier.d.ts +12 -4
- package/lib/services/notifier.js +353 -176
- package/package.json +1 -1
package/lib/commands/render.js
CHANGED
|
@@ -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>', '
|
|
29
|
+
ctx.command('githubsth.render.theme <theme:string>', '设置全局默认主题', { authority: 3 })
|
|
31
30
|
.action(async (_, theme) => {
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const normalized = ctx.githubsthNotifier.normalizeTheme(theme);
|
|
32
|
+
if (!normalized) {
|
|
33
|
+
return `无效主题。请使用 githubsth.render.themes 查看可用主题。`;
|
|
34
34
|
}
|
|
35
|
-
config.renderTheme =
|
|
36
|
-
return
|
|
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.
|
|
47
|
-
.action(async (
|
|
48
|
-
const
|
|
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
|
|
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
|
-
-
|
|
21
|
-
-
|
|
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 '
|
|
24
|
+
return '请指定仓库(owner/repo)。';
|
|
38
25
|
if (!repoRegex.test(repo))
|
|
39
|
-
return '
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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 (
|
|
90
|
-
logger.warn(
|
|
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 '
|
|
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(
|
|
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('
|
|
29
|
-
koishi_1.Schema.const('
|
|
30
|
-
koishi_1.Schema.const('
|
|
31
|
-
|
|
32
|
-
|
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Context, Service
|
|
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:
|
|
24
|
+
theme: RenderTheme;
|
|
23
25
|
width: number;
|
|
24
26
|
timeoutMs: number;
|
|
25
27
|
hasPuppeteer: boolean;
|
|
26
28
|
};
|
|
27
|
-
renderPreview(event?: string
|
|
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;
|
package/lib/services/notifier.js
CHANGED
|
@@ -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)
|
|
38
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
183
|
-
case '
|
|
184
|
-
|
|
185
|
-
case '
|
|
186
|
-
|
|
187
|
-
case '
|
|
188
|
-
|
|
189
|
-
case '
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
210
|
-
if (
|
|
211
|
-
return
|
|
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
|
|
313
|
+
const rendered = await Promise.race([
|
|
225
314
|
task,
|
|
226
315
|
new Promise((resolve) => setTimeout(() => resolve(null), timeout)),
|
|
227
316
|
]);
|
|
228
|
-
if (!
|
|
317
|
+
if (!rendered)
|
|
229
318
|
return null;
|
|
230
|
-
return
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
color: ${
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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, '&')
|
|
@@ -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,
|
|
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.
|
|
499
|
-
|
|
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
|
-
|
|
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:
|
|
533
|
-
sender:
|
|
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