@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.
Files changed (74) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +0 -14
  3. package/package.json +1 -1
  4. package/vendor/gugle-chat-bot/LICENSE +661 -661
  5. package/vendor/gugle-chat-bot/package.json +57 -57
  6. package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -9345
  7. package/vendor/gugle-chat-bot/src/api.json +4284 -4284
  8. package/vendor/gugle-chat-bot/src/command/index.ts +141 -141
  9. package/vendor/gugle-chat-bot/src/config/features.ts +547 -547
  10. package/vendor/gugle-chat-bot/src/config/index.ts +184 -184
  11. package/vendor/gugle-chat-bot/src/config/manager.ts +356 -356
  12. package/vendor/gugle-chat-bot/src/constants/index.ts +13 -13
  13. package/vendor/gugle-chat-bot/src/custom.ts +548 -548
  14. package/vendor/gugle-chat-bot/src/event/index.ts +118 -118
  15. package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -414
  16. package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -39
  17. package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -37
  18. package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -65
  19. package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -97
  20. package/vendor/gugle-chat-bot/src/features/bili/template/video.html +56 -56
  21. package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -1128
  22. package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -105
  23. package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -355
  24. package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -96
  25. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -14
  26. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -144
  27. package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -93
  28. package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -161
  29. package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -132
  30. package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -144
  31. package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -160
  32. package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -310
  33. package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -302
  34. package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -118
  35. package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -125
  36. package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -53
  37. package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -468
  38. package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +468 -468
  39. package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -412
  40. package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -184
  41. package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -210
  42. package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -67
  43. package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -86
  44. package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -1533
  45. package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -319
  46. package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -181
  47. package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -118
  48. package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -331
  49. package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -406
  50. package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -30
  51. package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -21
  52. package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -81
  53. package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -120
  54. package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -86
  55. package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -327
  56. package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -156
  57. package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -127
  58. package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -71
  59. package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -117
  60. package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +451 -451
  61. package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -24
  62. package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +77 -77
  63. package/vendor/gugle-chat-bot/src/features/welcome/index.ts +57 -57
  64. package/vendor/gugle-chat-bot/src/github-ip.json +5814 -5814
  65. package/vendor/gugle-chat-bot/src/image/index.ts +278 -278
  66. package/vendor/gugle-chat-bot/src/index.ts +695 -695
  67. package/vendor/gugle-chat-bot/src/logger/index.ts +54 -54
  68. package/vendor/gugle-chat-bot/src/result.json +359 -359
  69. package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -825
  70. package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -146
  71. package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -446
  72. package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -254
  73. package/vendor/gugle-chat-bot/tsconfig.json +46 -46
  74. 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, '&amp;')
44
- .replace(/</g, '&lt;')
45
- .replace(/>/g, '&gt;')
46
- .replace(/"/g, '&quot;');
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, '&amp;')
44
+ .replace(/</g, '&lt;')
45
+ .replace(/>/g, '&gt;')
46
+ .replace(/"/g, '&quot;');
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
+ }