@dsh-plugin/dsh-synapse 0.1.0-dev.41044228975 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -661
- package/README.md +0 -14
- package/package.json +1 -1
- package/vendor/gugle-chat-bot/LICENSE +661 -661
- package/vendor/gugle-chat-bot/package.json +57 -57
- package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -9345
- package/vendor/gugle-chat-bot/src/api.json +4284 -4284
- package/vendor/gugle-chat-bot/src/command/index.ts +141 -141
- package/vendor/gugle-chat-bot/src/config/features.ts +547 -547
- package/vendor/gugle-chat-bot/src/config/index.ts +184 -184
- package/vendor/gugle-chat-bot/src/config/manager.ts +356 -356
- package/vendor/gugle-chat-bot/src/constants/index.ts +13 -13
- package/vendor/gugle-chat-bot/src/custom.ts +548 -548
- package/vendor/gugle-chat-bot/src/event/index.ts +118 -118
- package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -414
- package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -39
- package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -37
- package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -65
- package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -97
- package/vendor/gugle-chat-bot/src/features/bili/template/video.html +56 -56
- package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -1128
- package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -105
- package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -355
- package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -96
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -14
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -144
- package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -93
- package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -161
- package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -132
- package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -144
- package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -160
- package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -310
- package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -302
- package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -118
- package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -125
- package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -53
- package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -468
- package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +468 -468
- package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -412
- package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -184
- package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -210
- package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -67
- package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -86
- package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -1533
- package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -319
- package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -181
- package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -118
- package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -331
- package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -406
- package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -30
- package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -21
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -81
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -120
- package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -86
- package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -327
- package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -156
- package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -127
- package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -71
- package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -117
- package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +451 -451
- package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -24
- package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +77 -77
- package/vendor/gugle-chat-bot/src/features/welcome/index.ts +57 -57
- package/vendor/gugle-chat-bot/src/github-ip.json +5814 -5814
- package/vendor/gugle-chat-bot/src/image/index.ts +278 -278
- package/vendor/gugle-chat-bot/src/index.ts +695 -695
- package/vendor/gugle-chat-bot/src/logger/index.ts +54 -54
- package/vendor/gugle-chat-bot/src/result.json +359 -359
- package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -825
- package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -146
- package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -446
- package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -254
- package/vendor/gugle-chat-bot/tsconfig.json +46 -46
- package/scripts/dump-npm-latest.mjs +0 -30
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
import { Release, ReleaseAsset, ReleaseEvent, User } from '@/type/github';
|
|
2
|
-
import { Logger } from 'winston';
|
|
3
|
-
import { Template, tryGenerateImage } from '@/image';
|
|
4
|
-
import dayjs from 'dayjs';
|
|
5
|
-
import { renderIssueBody } from '@/features/github/image';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 解析仓库 owner/name。
|
|
9
|
-
* 优先使用 webhook 载荷中的 repository.full_name(最可靠),
|
|
10
|
-
* 其次从 html_url(https://github.com/owner/repo/releases/tag/x)解析。
|
|
11
|
-
* 注意:不能用 release.url——它是 API 地址
|
|
12
|
-
* (https://api.github.com/repos/owner/repo/releases/123),末两段是 releases/123。
|
|
13
|
-
*/
|
|
14
|
-
function getRepositoryParts(repositoryFullName?: string, htmlUrl?: string): { owner: string; name: string } {
|
|
15
|
-
if (repositoryFullName) {
|
|
16
|
-
const parts = repositoryFullName.split('/').filter(Boolean);
|
|
17
|
-
if (parts.length === 2) return { owner: parts[0]!, name: parts[1]! };
|
|
18
|
-
}
|
|
19
|
-
if (htmlUrl) {
|
|
20
|
-
const parts = htmlUrl.split('/').filter(Boolean);
|
|
21
|
-
const githubIndex = parts.indexOf('github.com');
|
|
22
|
-
const repositoryParts = githubIndex >= 0 ? parts.slice(githubIndex + 1, githubIndex + 3) : [];
|
|
23
|
-
if (repositoryParts.length === 2) return { owner: repositoryParts[0]!, name: repositoryParts[1]! };
|
|
24
|
-
}
|
|
25
|
-
return { owner: 'GitHub', name: '' };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 格式化为 GitHub 风格的体积文本(如 23.8 MB) */
|
|
29
|
-
export function formatBytes(bytes: number): string {
|
|
30
|
-
if (!Number.isFinite(bytes) || bytes < 0) return '0 Bytes';
|
|
31
|
-
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
32
|
-
if (bytes === 0) return '0 Bytes';
|
|
33
|
-
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
34
|
-
const value = bytes / Math.pow(1024, exponent);
|
|
35
|
-
// GitHub 展示保留 1 位小数(整数体积不显示小数)
|
|
36
|
-
const text = exponent === 0 || Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
37
|
-
return `${text} ${units[exponent]}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 转义 HTML 特殊字符,避免标题/文件名破坏模板 */
|
|
41
|
-
function escapeHtml(text: string): string {
|
|
42
|
-
return text
|
|
43
|
-
.replace(/&/g, '&')
|
|
44
|
-
.replace(/</g, '<')
|
|
45
|
-
.replace(/>/g, '>')
|
|
46
|
-
.replace(/"/g, '"');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** 「Latest」/「Pre-release」徽章 */
|
|
50
|
-
function getReleaseBadge(release: Release): string {
|
|
51
|
-
if (release.prerelease) {
|
|
52
|
-
return `<span class="release_badge release_badge_prerelease"><svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.25-3.25v6.5a.75.75 0 0 1-1.5 0v-6.5a.75.75 0 0 1 1.5 0Zm3.5 0v6.5a.75.75 0 0 1-1.5 0v-6.5a.75.75 0 0 1 1.5 0Z"/></svg>Pre-release</span>`;
|
|
53
|
-
}
|
|
54
|
-
if (release.draft) {
|
|
55
|
-
return `<span class="release_badge release_badge_prerelease">Draft</span>`;
|
|
56
|
-
}
|
|
57
|
-
return `<span class="release_badge"><svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm3.78-9.72a.751.751 0 0 0-.018-1.042.751.751 0 0 0-1.042-.018L6.75 9.19 5.28 7.72a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042l2 2a.75.75 0 0 0 1.06 0Z"/></svg>Latest</span>`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 渲染 Assets 列表(GitHub release 页面的附件行) */
|
|
61
|
-
function renderAssets(assets: ReleaseAsset[]): string {
|
|
62
|
-
if (!assets || assets.length === 0) {
|
|
63
|
-
return '<div class="empty_assets">No assets uploaded.</div>';
|
|
64
|
-
}
|
|
65
|
-
return assets
|
|
66
|
-
.map(asset => {
|
|
67
|
-
const downloadCount = asset.download_count > 0 ? `${asset.download_count} downloads` : '0 downloads';
|
|
68
|
-
return (
|
|
69
|
-
`<div class="asset_item">` +
|
|
70
|
-
`<svg class="asset_icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.75 1A1.75 1.75 0 0 0 1 2.75v10.5c0 .966.784 1.75 1.75 1.75h10.5c.966 0 1.75-.784 1.75-1.75V2.75A1.75 1.75 0 0 0 13.25 1Zm-.25 1.75a.25.25 0 0 1 .25-.25h10.5a.25.25 0 0 1 .25.25v10.5a.25.25 0 0 1-.25.25H2.75a.25.25 0 0 1-.25-.25Z"/><path d="M9.5 4.5v3.293l1.146-1.147a.75.75 0 1 1 1.061 1.061l-2.5 2.5a.75.75 0 0 1-1.06 0l-2.5-2.5a.75.75 0 0 1 1.06-1.06L8 7.792V4.5a.75.75 0 0 1 1.5 0Z"/></svg>` +
|
|
71
|
-
`<a class="asset_name" href="${asset.browser_download_url}">${escapeHtml(asset.name)}</a>` +
|
|
72
|
-
`<span class="asset_spacer"></span>` +
|
|
73
|
-
`<span class="asset_size">${formatBytes(asset.size)}</span>` +
|
|
74
|
-
`<span class="asset_downloads">${downloadCount}</span>` +
|
|
75
|
-
`</div>`
|
|
76
|
-
);
|
|
77
|
-
})
|
|
78
|
-
.join('\n');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function releaseHandler(release: Release, repositoryFullName?: string, logger?: Logger, sender?: User) {
|
|
82
|
-
let headerExtra: string | undefined = undefined;
|
|
83
|
-
if (sender) {
|
|
84
|
-
headerExtra = `<div class="message">用户<div class="user">${sender.login}</div>发布了 </div>`;
|
|
85
|
-
}
|
|
86
|
-
const repository = getRepositoryParts(repositoryFullName, release.html_url);
|
|
87
|
-
const repoFullName = `${repository.owner}/${repository.name}`;
|
|
88
|
-
// release 标题为空时回退为 tag 名
|
|
89
|
-
const title = release.name || release.tag_name;
|
|
90
|
-
return new Promise<string>((resolve, reject) => {
|
|
91
|
-
renderIssueBody(release.body || '', repoFullName, logger)
|
|
92
|
-
.then(bodyHtml => {
|
|
93
|
-
const publishedAt = release.published_at || release.created_at;
|
|
94
|
-
const template = Template.load('release', 'src/features/github/template')
|
|
95
|
-
.arg('header extra', headerExtra || '')
|
|
96
|
-
.arg('repository owner', repository.owner)
|
|
97
|
-
.arg('repository name', repository.name)
|
|
98
|
-
.arg('release badge', getReleaseBadge(release))
|
|
99
|
-
.arg('release title', title)
|
|
100
|
-
.arg('release tag', release.tag_name)
|
|
101
|
-
.arg('release commitish', release.target_commitish)
|
|
102
|
-
.arg('release author', release.author?.login || 'unknown')
|
|
103
|
-
.arg('release published at', dayjs(publishedAt).format('YYYY-MM-DD HH:mm:ss'))
|
|
104
|
-
.arg('release body', bodyHtml || '<p><em>No description provided.</em></p>')
|
|
105
|
-
.arg('release asset count', release.assets?.length || 0)
|
|
106
|
-
.arg('release assets', renderAssets(release.assets || []));
|
|
107
|
-
const templateFile = template.file();
|
|
108
|
-
template
|
|
109
|
-
.handler()
|
|
110
|
-
.then(html => {
|
|
111
|
-
logger?.debug(`Start process release message...`);
|
|
112
|
-
tryGenerateImage(resolve, reject, html, 820, templateFile);
|
|
113
|
-
})
|
|
114
|
-
.catch(reject);
|
|
115
|
-
})
|
|
116
|
-
.catch(reject);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function releasePublished(event: ReleaseEvent, logger?: Logger): Promise<string> {
|
|
121
|
-
return releaseHandler(event.release, event.repository?.full_name, logger, event.sender);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** 视为「发布」的 release action(其余如 edited / deleted / unpublished 不推送) */
|
|
125
|
-
const RELEASE_PUBLISH_ACTIONS = new Set(['created', 'published', 'released', 'prereleased']);
|
|
126
|
-
/** 同一次发布的多次投递会集中在数秒内到达,用该窗口折叠 */
|
|
127
|
-
export const RELEASE_DEDUPE_WINDOW_MS = 60 * 1000;
|
|
128
|
-
/** release id -> 最近一次推送时间 */
|
|
129
|
-
const handledReleases = new Map<number, number>();
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* 判断一个 release webhook 事件是否应当推送。
|
|
133
|
-
*
|
|
134
|
-
* GitHub 对同一次发布会投递多个 action(实测同一 release id 会在数秒内先后收到
|
|
135
|
-
* created + released、published + created + prereleased 等组合,且组合不固定),
|
|
136
|
-
* 因此不能只按 action 过滤:这里以 release id 在时间窗口内去重,保证一次发布只推一张。
|
|
137
|
-
* 同时跳过仍是草稿的 release(保存草稿同样会触发 created)。
|
|
138
|
-
*
|
|
139
|
-
* @param event release webhook 事件
|
|
140
|
-
* @param now 当前时间戳(便于验证)
|
|
141
|
-
* @returns 是否推送,以及跳过原因
|
|
142
|
-
*/
|
|
143
|
-
export function shouldPushRelease(event: ReleaseEvent, now: number = Date.now()): { push: boolean; reason: string } {
|
|
144
|
-
if (!RELEASE_PUBLISH_ACTIONS.has(event.action)) {
|
|
145
|
-
return { push: false, reason: `action ${event.action} 不属于发布` };
|
|
146
|
-
}
|
|
147
|
-
if (event.release.draft) {
|
|
148
|
-
return { push: false, reason: 'release 仍为草稿' };
|
|
149
|
-
}
|
|
150
|
-
const last = handledReleases.get(event.release.id);
|
|
151
|
-
if (last !== undefined && now - last < RELEASE_DEDUPE_WINDOW_MS) {
|
|
152
|
-
return { push: false, reason: `同一 release 已于 ${now - last}ms 前推送` };
|
|
153
|
-
}
|
|
154
|
-
// 清理窗口外的记录,避免长期运行后 Map 持续增长
|
|
155
|
-
for (const [id, timestamp] of handledReleases) {
|
|
156
|
-
if (now - timestamp >= RELEASE_DEDUPE_WINDOW_MS) handledReleases.delete(id);
|
|
157
|
-
}
|
|
158
|
-
handledReleases.set(event.release.id, now);
|
|
159
|
-
return { push: true, reason: '首次收到该 release 的发布事件' };
|
|
160
|
-
}
|
|
1
|
+
import { Release, ReleaseAsset, ReleaseEvent, User } from '@/type/github';
|
|
2
|
+
import { Logger } from 'winston';
|
|
3
|
+
import { Template, tryGenerateImage } from '@/image';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { renderIssueBody } from '@/features/github/image';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 解析仓库 owner/name。
|
|
9
|
+
* 优先使用 webhook 载荷中的 repository.full_name(最可靠),
|
|
10
|
+
* 其次从 html_url(https://github.com/owner/repo/releases/tag/x)解析。
|
|
11
|
+
* 注意:不能用 release.url——它是 API 地址
|
|
12
|
+
* (https://api.github.com/repos/owner/repo/releases/123),末两段是 releases/123。
|
|
13
|
+
*/
|
|
14
|
+
function getRepositoryParts(repositoryFullName?: string, htmlUrl?: string): { owner: string; name: string } {
|
|
15
|
+
if (repositoryFullName) {
|
|
16
|
+
const parts = repositoryFullName.split('/').filter(Boolean);
|
|
17
|
+
if (parts.length === 2) return { owner: parts[0]!, name: parts[1]! };
|
|
18
|
+
}
|
|
19
|
+
if (htmlUrl) {
|
|
20
|
+
const parts = htmlUrl.split('/').filter(Boolean);
|
|
21
|
+
const githubIndex = parts.indexOf('github.com');
|
|
22
|
+
const repositoryParts = githubIndex >= 0 ? parts.slice(githubIndex + 1, githubIndex + 3) : [];
|
|
23
|
+
if (repositoryParts.length === 2) return { owner: repositoryParts[0]!, name: repositoryParts[1]! };
|
|
24
|
+
}
|
|
25
|
+
return { owner: 'GitHub', name: '' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 格式化为 GitHub 风格的体积文本(如 23.8 MB) */
|
|
29
|
+
export function formatBytes(bytes: number): string {
|
|
30
|
+
if (!Number.isFinite(bytes) || bytes < 0) return '0 Bytes';
|
|
31
|
+
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
32
|
+
if (bytes === 0) return '0 Bytes';
|
|
33
|
+
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
34
|
+
const value = bytes / Math.pow(1024, exponent);
|
|
35
|
+
// GitHub 展示保留 1 位小数(整数体积不显示小数)
|
|
36
|
+
const text = exponent === 0 || Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
37
|
+
return `${text} ${units[exponent]}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 转义 HTML 特殊字符,避免标题/文件名破坏模板 */
|
|
41
|
+
function escapeHtml(text: string): string {
|
|
42
|
+
return text
|
|
43
|
+
.replace(/&/g, '&')
|
|
44
|
+
.replace(/</g, '<')
|
|
45
|
+
.replace(/>/g, '>')
|
|
46
|
+
.replace(/"/g, '"');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 「Latest」/「Pre-release」徽章 */
|
|
50
|
+
function getReleaseBadge(release: Release): string {
|
|
51
|
+
if (release.prerelease) {
|
|
52
|
+
return `<span class="release_badge release_badge_prerelease"><svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.25-3.25v6.5a.75.75 0 0 1-1.5 0v-6.5a.75.75 0 0 1 1.5 0Zm3.5 0v6.5a.75.75 0 0 1-1.5 0v-6.5a.75.75 0 0 1 1.5 0Z"/></svg>Pre-release</span>`;
|
|
53
|
+
}
|
|
54
|
+
if (release.draft) {
|
|
55
|
+
return `<span class="release_badge release_badge_prerelease">Draft</span>`;
|
|
56
|
+
}
|
|
57
|
+
return `<span class="release_badge"><svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm3.78-9.72a.751.751 0 0 0-.018-1.042.751.751 0 0 0-1.042-.018L6.75 9.19 5.28 7.72a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042l2 2a.75.75 0 0 0 1.06 0Z"/></svg>Latest</span>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 渲染 Assets 列表(GitHub release 页面的附件行) */
|
|
61
|
+
function renderAssets(assets: ReleaseAsset[]): string {
|
|
62
|
+
if (!assets || assets.length === 0) {
|
|
63
|
+
return '<div class="empty_assets">No assets uploaded.</div>';
|
|
64
|
+
}
|
|
65
|
+
return assets
|
|
66
|
+
.map(asset => {
|
|
67
|
+
const downloadCount = asset.download_count > 0 ? `${asset.download_count} downloads` : '0 downloads';
|
|
68
|
+
return (
|
|
69
|
+
`<div class="asset_item">` +
|
|
70
|
+
`<svg class="asset_icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.75 1A1.75 1.75 0 0 0 1 2.75v10.5c0 .966.784 1.75 1.75 1.75h10.5c.966 0 1.75-.784 1.75-1.75V2.75A1.75 1.75 0 0 0 13.25 1Zm-.25 1.75a.25.25 0 0 1 .25-.25h10.5a.25.25 0 0 1 .25.25v10.5a.25.25 0 0 1-.25.25H2.75a.25.25 0 0 1-.25-.25Z"/><path d="M9.5 4.5v3.293l1.146-1.147a.75.75 0 1 1 1.061 1.061l-2.5 2.5a.75.75 0 0 1-1.06 0l-2.5-2.5a.75.75 0 0 1 1.06-1.06L8 7.792V4.5a.75.75 0 0 1 1.5 0Z"/></svg>` +
|
|
71
|
+
`<a class="asset_name" href="${asset.browser_download_url}">${escapeHtml(asset.name)}</a>` +
|
|
72
|
+
`<span class="asset_spacer"></span>` +
|
|
73
|
+
`<span class="asset_size">${formatBytes(asset.size)}</span>` +
|
|
74
|
+
`<span class="asset_downloads">${downloadCount}</span>` +
|
|
75
|
+
`</div>`
|
|
76
|
+
);
|
|
77
|
+
})
|
|
78
|
+
.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function releaseHandler(release: Release, repositoryFullName?: string, logger?: Logger, sender?: User) {
|
|
82
|
+
let headerExtra: string | undefined = undefined;
|
|
83
|
+
if (sender) {
|
|
84
|
+
headerExtra = `<div class="message">用户<div class="user">${sender.login}</div>发布了 </div>`;
|
|
85
|
+
}
|
|
86
|
+
const repository = getRepositoryParts(repositoryFullName, release.html_url);
|
|
87
|
+
const repoFullName = `${repository.owner}/${repository.name}`;
|
|
88
|
+
// release 标题为空时回退为 tag 名
|
|
89
|
+
const title = release.name || release.tag_name;
|
|
90
|
+
return new Promise<string>((resolve, reject) => {
|
|
91
|
+
renderIssueBody(release.body || '', repoFullName, logger)
|
|
92
|
+
.then(bodyHtml => {
|
|
93
|
+
const publishedAt = release.published_at || release.created_at;
|
|
94
|
+
const template = Template.load('release', 'src/features/github/template')
|
|
95
|
+
.arg('header extra', headerExtra || '')
|
|
96
|
+
.arg('repository owner', repository.owner)
|
|
97
|
+
.arg('repository name', repository.name)
|
|
98
|
+
.arg('release badge', getReleaseBadge(release))
|
|
99
|
+
.arg('release title', title)
|
|
100
|
+
.arg('release tag', release.tag_name)
|
|
101
|
+
.arg('release commitish', release.target_commitish)
|
|
102
|
+
.arg('release author', release.author?.login || 'unknown')
|
|
103
|
+
.arg('release published at', dayjs(publishedAt).format('YYYY-MM-DD HH:mm:ss'))
|
|
104
|
+
.arg('release body', bodyHtml || '<p><em>No description provided.</em></p>')
|
|
105
|
+
.arg('release asset count', release.assets?.length || 0)
|
|
106
|
+
.arg('release assets', renderAssets(release.assets || []));
|
|
107
|
+
const templateFile = template.file();
|
|
108
|
+
template
|
|
109
|
+
.handler()
|
|
110
|
+
.then(html => {
|
|
111
|
+
logger?.debug(`Start process release message...`);
|
|
112
|
+
tryGenerateImage(resolve, reject, html, 820, templateFile);
|
|
113
|
+
})
|
|
114
|
+
.catch(reject);
|
|
115
|
+
})
|
|
116
|
+
.catch(reject);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function releasePublished(event: ReleaseEvent, logger?: Logger): Promise<string> {
|
|
121
|
+
return releaseHandler(event.release, event.repository?.full_name, logger, event.sender);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 视为「发布」的 release action(其余如 edited / deleted / unpublished 不推送) */
|
|
125
|
+
const RELEASE_PUBLISH_ACTIONS = new Set(['created', 'published', 'released', 'prereleased']);
|
|
126
|
+
/** 同一次发布的多次投递会集中在数秒内到达,用该窗口折叠 */
|
|
127
|
+
export const RELEASE_DEDUPE_WINDOW_MS = 60 * 1000;
|
|
128
|
+
/** release id -> 最近一次推送时间 */
|
|
129
|
+
const handledReleases = new Map<number, number>();
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 判断一个 release webhook 事件是否应当推送。
|
|
133
|
+
*
|
|
134
|
+
* GitHub 对同一次发布会投递多个 action(实测同一 release id 会在数秒内先后收到
|
|
135
|
+
* created + released、published + created + prereleased 等组合,且组合不固定),
|
|
136
|
+
* 因此不能只按 action 过滤:这里以 release id 在时间窗口内去重,保证一次发布只推一张。
|
|
137
|
+
* 同时跳过仍是草稿的 release(保存草稿同样会触发 created)。
|
|
138
|
+
*
|
|
139
|
+
* @param event release webhook 事件
|
|
140
|
+
* @param now 当前时间戳(便于验证)
|
|
141
|
+
* @returns 是否推送,以及跳过原因
|
|
142
|
+
*/
|
|
143
|
+
export function shouldPushRelease(event: ReleaseEvent, now: number = Date.now()): { push: boolean; reason: string } {
|
|
144
|
+
if (!RELEASE_PUBLISH_ACTIONS.has(event.action)) {
|
|
145
|
+
return { push: false, reason: `action ${event.action} 不属于发布` };
|
|
146
|
+
}
|
|
147
|
+
if (event.release.draft) {
|
|
148
|
+
return { push: false, reason: 'release 仍为草稿' };
|
|
149
|
+
}
|
|
150
|
+
const last = handledReleases.get(event.release.id);
|
|
151
|
+
if (last !== undefined && now - last < RELEASE_DEDUPE_WINDOW_MS) {
|
|
152
|
+
return { push: false, reason: `同一 release 已于 ${now - last}ms 前推送` };
|
|
153
|
+
}
|
|
154
|
+
// 清理窗口外的记录,避免长期运行后 Map 持续增长
|
|
155
|
+
for (const [id, timestamp] of handledReleases) {
|
|
156
|
+
if (now - timestamp >= RELEASE_DEDUPE_WINDOW_MS) handledReleases.delete(id);
|
|
157
|
+
}
|
|
158
|
+
handledReleases.set(event.release.id, now);
|
|
159
|
+
return { push: true, reason: '首次收到该 release 的发布事件' };
|
|
160
|
+
}
|