@dsh-plugin/dsh-synapse 0.0.0-stage → 0.1.0-dev.41044228975
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 -0
- package/NOTICE.md +18 -0
- package/README.md +136 -2
- package/cordis.patch.yml +3 -0
- package/lib/bot.cjs +8314 -0
- package/lib/bot.cjs.map +7 -0
- package/lib/build.json +1 -0
- package/lib/client.js +46 -0
- package/lib/host/agent.d.ts +21 -0
- package/lib/host/agent.js +151 -0
- package/lib/host/agent.js.map +1 -0
- package/lib/host/bot.d.ts +17 -0
- package/lib/host/bot.js +158 -0
- package/lib/host/bot.js.map +1 -0
- package/lib/host/client.d.ts +2 -0
- package/lib/host/client.js +19 -0
- package/lib/host/client.js.map +1 -0
- package/lib/host/config.d.ts +24 -0
- package/lib/host/config.js +192 -0
- package/lib/host/config.js.map +1 -0
- package/lib/host/files.d.ts +29 -0
- package/lib/host/files.js +188 -0
- package/lib/host/files.js.map +1 -0
- package/lib/host/gateway.d.ts +33 -0
- package/lib/host/gateway.js +237 -0
- package/lib/host/gateway.js.map +1 -0
- package/lib/host/github.d.ts +47 -0
- package/lib/host/github.js +219 -0
- package/lib/host/github.js.map +1 -0
- package/lib/host/http.d.ts +4 -0
- package/lib/host/http.js +41 -0
- package/lib/host/http.js.map +1 -0
- package/lib/host/index.d.ts +32 -0
- package/lib/host/index.js +192 -0
- package/lib/host/index.js.map +1 -0
- package/lib/host/inference-worker.d.ts +1 -0
- package/lib/host/inference-worker.js +72 -0
- package/lib/host/inference-worker.js.map +1 -0
- package/lib/host/mcp.d.ts +138 -0
- package/lib/host/mcp.js +104 -0
- package/lib/host/mcp.js.map +1 -0
- package/lib/host/memory.d.ts +26 -0
- package/lib/host/memory.js +155 -0
- package/lib/host/memory.js.map +1 -0
- package/lib/host/panel.d.ts +1 -0
- package/lib/host/panel.html +174 -0
- package/lib/host/panel.js +388 -0
- package/lib/host/panel.js.map +1 -0
- package/lib/host/runtime.d.ts +21 -0
- package/lib/host/runtime.js +145 -0
- package/lib/host/runtime.js.map +1 -0
- package/lib/host/types.d.ts +112 -0
- package/lib/host/types.js +2 -0
- package/lib/host/types.js.map +1 -0
- package/package.json +94 -4
- package/scripts/build.mjs +90 -0
- package/scripts/check.mjs +13 -0
- package/scripts/dump-npm-latest.mjs +30 -0
- package/scripts/model-smoke.mjs +25 -0
- package/src/agent.ts +203 -0
- package/src/bot.ts +173 -0
- package/src/client.ts +20 -0
- package/src/config.ts +217 -0
- package/src/files.ts +206 -0
- package/src/gateway.ts +276 -0
- package/src/github.ts +263 -0
- package/src/http.ts +41 -0
- package/src/index.ts +227 -0
- package/src/inference-worker.ts +89 -0
- package/src/mcp.ts +112 -0
- package/src/memory.ts +178 -0
- package/src/panel.html +174 -0
- package/src/panel.ts +440 -0
- package/src/runtime.ts +164 -0
- package/src/types.ts +106 -0
- package/test/core.test.mjs +279 -0
- package/test/fixtures/mcp-server.mjs +14 -0
- package/test/integration.test.mjs +253 -0
- package/test/panel.test.mjs +73 -0
- package/tsconfig.json +18 -0
- package/vendor/gugle-chat-bot/LICENSE +661 -0
- package/vendor/gugle-chat-bot/package.json +57 -0
- package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -0
- package/vendor/gugle-chat-bot/src/api.json +4285 -0
- package/vendor/gugle-chat-bot/src/command/index.ts +141 -0
- package/vendor/gugle-chat-bot/src/config/features.ts +548 -0
- package/vendor/gugle-chat-bot/src/config/index.ts +184 -0
- package/vendor/gugle-chat-bot/src/config/manager.ts +357 -0
- package/vendor/gugle-chat-bot/src/constants/index.ts +13 -0
- package/vendor/gugle-chat-bot/src/custom.ts +548 -0
- package/vendor/gugle-chat-bot/src/event/index.ts +118 -0
- package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -0
- package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -0
- package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -0
- package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -0
- package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -0
- package/vendor/gugle-chat-bot/src/features/bili/template/video.html +57 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -0
- package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -0
- package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -0
- package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -0
- package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -0
- package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -0
- package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -0
- package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +469 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -0
- package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -0
- package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -0
- package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -0
- package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -0
- package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -0
- package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -0
- package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -0
- package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -0
- package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -0
- package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -0
- package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -0
- package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -0
- package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -0
- package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -0
- package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -0
- package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -0
- package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +452 -0
- package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -0
- package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +78 -0
- package/vendor/gugle-chat-bot/src/features/welcome/index.ts +58 -0
- package/vendor/gugle-chat-bot/src/github-ip.json +5814 -0
- package/vendor/gugle-chat-bot/src/image/index.ts +278 -0
- package/vendor/gugle-chat-bot/src/index.ts +695 -0
- package/vendor/gugle-chat-bot/src/logger/index.ts +54 -0
- package/vendor/gugle-chat-bot/src/result.json +359 -0
- package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -0
- package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -0
- package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -0
- package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -0
- package/vendor/gugle-chat-bot/tsconfig.json +46 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AllReleaseEvent,
|
|
3
|
+
ClosedIssueEvent,
|
|
4
|
+
ClosedPullRequestEvent,
|
|
5
|
+
Issue,
|
|
6
|
+
OpenedIssueEvent,
|
|
7
|
+
OpenedPullRequestEvent,
|
|
8
|
+
PullRequest,
|
|
9
|
+
Release,
|
|
10
|
+
ReopenedIssueEvent,
|
|
11
|
+
ReopenedPullRequestEvent,
|
|
12
|
+
User
|
|
13
|
+
} from '@/type/github';
|
|
14
|
+
import { Logger } from 'winston';
|
|
15
|
+
import markdownit from 'markdown-it';
|
|
16
|
+
import hljs from 'highlight.js';
|
|
17
|
+
import { full as emoji } from 'markdown-it-emoji';
|
|
18
|
+
import { issuesClosed, issuesHandler, issuesOpened } from '@/features/github/image/impl/issue';
|
|
19
|
+
import { prClosed, prHandler, prOpened } from '@/features/github/image/impl/pr';
|
|
20
|
+
import { releaseHandler, releasePublished } from '@/features/github/image/impl/release';
|
|
21
|
+
import { fetchIssueDetail, LinkedIssueDetail, LinkedIssueStatus } from '@/features/github/api';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 解析 PR/Issue 正文列表行中的 issue/PR 引用(如 "- resolved #4851"、
|
|
25
|
+
* "- fixed owner/repo#4858"、自定义文字 "- 解决了 #4851"),
|
|
26
|
+
* 只替换其中的 "#编号" 部分为 GitHub 风格的行内引用:
|
|
27
|
+
* 状态图标 + 标题 + 链接色编号;行内其它文字原样保留。
|
|
28
|
+
*
|
|
29
|
+
* 与 GitHub 官方 markdown 渲染行为一致(列表保留、文字保留、#编号变链接),
|
|
30
|
+
* 只是额外把标题与状态图标一并渲染出来。非列表行中的 #编号 不处理。
|
|
31
|
+
*/
|
|
32
|
+
/** 匹配列表行(无序标记 -、*、+ 或有序标记 1. / 1) ) */
|
|
33
|
+
const LIST_LINE = /^\s*(?:[-*+]|\d+[.)])\s+.*$/gm;
|
|
34
|
+
/** 匹配行内的引用:#1234 或 owner/repo#1234(不匹配单词/路径中间) */
|
|
35
|
+
const REF_IN_LINE = /(?<![\w/])((?:[\w.-]+\/[\w.-]+)?#\d+)(?!\w)/g;
|
|
36
|
+
/** 匹配 GitHub issue/PR 完整地址:https://github.com/owner/repo/(pull|issues)/1234 */
|
|
37
|
+
const ISSUE_URL = /(?:https?:\/\/)?(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/(issues|pull)\/(\d+)/g;
|
|
38
|
+
/** 代码段(围栏代码块或行内代码):缩短 URL 时跳过,避免破坏代码内容 */
|
|
39
|
+
const CODE_SEGMENT = /(```[\s\S]*?```|`[^`\n]*`)/g;
|
|
40
|
+
|
|
41
|
+
/** 占位符前缀/后缀:避开 markdown 特殊字符,渲染后再换回引用 HTML */
|
|
42
|
+
const PLACEHOLDER_PREFIX = 'GHILINKISSUEPILLZ';
|
|
43
|
+
const PLACEHOLDER_SUFFIX = 'ZLLIPISSUEKNILHG';
|
|
44
|
+
/** 纯链接(URL 缩短而来)的占位符,与引用分开编号避免冲突 */
|
|
45
|
+
const LINK_PLACEHOLDER_PREFIX = 'GHILINKSHORTURLZ';
|
|
46
|
+
const LINK_PLACEHOLDER_SUFFIX = 'ZLRUHTROHSKNILIHG';
|
|
47
|
+
|
|
48
|
+
interface LinkedIssueReference {
|
|
49
|
+
/** 完整仓库(owner/repo),短引用用当前仓库展开 */
|
|
50
|
+
repository: string;
|
|
51
|
+
/** issue 编号 */
|
|
52
|
+
number: number;
|
|
53
|
+
/** 查询失败时回退显示的文本(owner/repo#编号) */
|
|
54
|
+
label: string;
|
|
55
|
+
/** 是否跨仓库引用(显式写了 owner/repo#编号) */
|
|
56
|
+
crossRepo: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 由完整 GitHub 地址缩短而来的引用(只渲染为蓝色 #编号,不查询 API) */
|
|
60
|
+
interface ShortUrl {
|
|
61
|
+
url: string;
|
|
62
|
+
number: number;
|
|
63
|
+
label: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 转义标题中的 HTML 特殊字符,避免注入/破坏模板 */
|
|
67
|
+
function escapeHtml(text: string): string {
|
|
68
|
+
return text
|
|
69
|
+
.replace(/&/g, '&')
|
|
70
|
+
.replace(/</g, '<')
|
|
71
|
+
.replace(/>/g, '>')
|
|
72
|
+
.replace(/"/g, '"');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 按状态返回对应的 SVG 图标(GitHub octicon 原版:彩色圆环+同色图标,颜色与卡片状态标签一致) */
|
|
76
|
+
function iconForStatus(status: LinkedIssueStatus): string {
|
|
77
|
+
switch (status) {
|
|
78
|
+
case 'open':
|
|
79
|
+
// issue 进行中:绿色圆环+绿点(issue-opened)
|
|
80
|
+
return `<svg class="issue-ref-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="#347d39"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/><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 0Z"/></svg>`;
|
|
81
|
+
case 'completed':
|
|
82
|
+
// issue 已完成关闭:紫色圆环+紫色对勾(issue-closed)
|
|
83
|
+
return `<svg class="issue-ref-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="#8256d0"><path d="M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z"/><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z"/></svg>`;
|
|
84
|
+
case 'not_planned':
|
|
85
|
+
// issue 未计划等关闭:灰色圆环+灰色斜杠(skip)
|
|
86
|
+
return `<svg class="issue-ref-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="#656c76"><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 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/></svg>`;
|
|
87
|
+
case 'pr_open':
|
|
88
|
+
// PR 进行中:绿色 PR 图标(git-pull-request,原生无圆环)
|
|
89
|
+
return `<svg class="issue-ref-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="#347d39"><path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Z"/></svg>`;
|
|
90
|
+
case 'pr_merged':
|
|
91
|
+
// PR 已合并:紫色合并图标(git-merge,原生无圆环)
|
|
92
|
+
return `<svg class="issue-ref-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="#8256d0"><path d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z"/></svg>`;
|
|
93
|
+
case 'pr_closed':
|
|
94
|
+
// PR 关闭未合并:红色 PR 关闭图标(git-pull-request-closed,原生无圆环)
|
|
95
|
+
return `<svg class="issue-ref-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="#cf222e"><path d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.75.75 0 0 1-1.06 1.061l-.97-.97-.97.97a.75.75 0 0 1-1.06-1.06l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm0 9.5a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm9.5 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"/></svg>`;
|
|
96
|
+
default:
|
|
97
|
+
// 状态未知(查询失败回退,通常不显示):灰色圆环+问号
|
|
98
|
+
return `<svg class="issue-ref-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="#8b949e"><circle cx="8" cy="8" r="6.5" fill="none" stroke="#8b949e" stroke-width="1.5"/><text x="8" y="11" text-anchor="middle" font-size="9" font-family="sans-serif">?</text></svg>`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 把单个引用渲染为 GitHub 风格的行内引用:
|
|
104
|
+
* 查到标题时显示「状态图标 + 标题 + 链接色编号」;查询失败时回退为纯链接色编号。
|
|
105
|
+
*/
|
|
106
|
+
function renderInlineRef(ref: LinkedIssueReference, detail: LinkedIssueDetail): string {
|
|
107
|
+
const numberLabel = ref.crossRepo ? `${ref.repository}#${ref.number}` : `#${ref.number}`;
|
|
108
|
+
const url = ref.repository ? `https://github.com/${ref.repository}/issues/${ref.number}` : '';
|
|
109
|
+
if (!detail.title) {
|
|
110
|
+
return `<a class="issue-ref-number" href="${url}">${numberLabel}</a>`;
|
|
111
|
+
}
|
|
112
|
+
return (
|
|
113
|
+
`<span class="issue-ref issue-ref-${detail.status}">${iconForStatus(detail.status)}` +
|
|
114
|
+
`<a class="issue-ref-title" href="${url}">${escapeHtml(detail.title)}</a>` +
|
|
115
|
+
`<a class="issue-ref-number" href="${url}">${numberLabel}</a></span>`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 占位符 -> 引用索引 */
|
|
120
|
+
function placeholder(index: number): string {
|
|
121
|
+
return `${PLACEHOLDER_PREFIX}${index}${PLACEHOLDER_SUFFIX}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 占位符 -> 缩短链接索引 */
|
|
125
|
+
function linkPlaceholder(index: number): string {
|
|
126
|
+
return `${LINK_PLACEHOLDER_PREFIX}${index}${LINK_PLACEHOLDER_SUFFIX}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 把正文中的 issue/PR 完整地址缩短为 "#编号"(渲染后为蓝色链接)。
|
|
131
|
+
* 例如 https://github.com/owner/repo/pull/1234 -> #1234,
|
|
132
|
+
* 不处理 compare/commit 等其他 GitHub 地址,代码段内的 URL 也跳过。
|
|
133
|
+
* @param body 原始 markdown
|
|
134
|
+
* @param repository 当前仓库(owner/repo),同仓库时只显示 #编号
|
|
135
|
+
* @param links 收集缩短后的链接信息
|
|
136
|
+
*/
|
|
137
|
+
function shortenIssueUrls(body: string, repository: string, links: ShortUrl[]): string {
|
|
138
|
+
// 先按代码段切分,只处理非代码段,避免破坏代码块中的 URL
|
|
139
|
+
return body
|
|
140
|
+
.split(CODE_SEGMENT)
|
|
141
|
+
.map((segment, index) => {
|
|
142
|
+
// 奇数索引是捕获到的代码段,原样保留
|
|
143
|
+
if (index % 2 === 1) return segment;
|
|
144
|
+
return segment.replace(ISSUE_URL, (match, owner: string, repo: string, kind: string, number: string) => {
|
|
145
|
+
const fullName = `${owner}/${repo}`;
|
|
146
|
+
const url = `https://github.com/${fullName}/${kind}/${number}`;
|
|
147
|
+
const sameRepo = fullName.toLowerCase() === repository.toLowerCase();
|
|
148
|
+
const index = links.length;
|
|
149
|
+
links.push({ url, number: Number.parseInt(number, 10), label: sameRepo ? `#${number}` : `${fullName}#${number}` });
|
|
150
|
+
return linkPlaceholder(index);
|
|
151
|
+
});
|
|
152
|
+
})
|
|
153
|
+
.join('');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 从正文的列表行中提取 issue/PR 引用,替换为占位符(行内替换,保留其它文字)。
|
|
158
|
+
*/
|
|
159
|
+
function extractReferences(
|
|
160
|
+
body: string,
|
|
161
|
+
repository: string,
|
|
162
|
+
references: LinkedIssueReference[]
|
|
163
|
+
): string {
|
|
164
|
+
const toRef = (reference: string): string => {
|
|
165
|
+
// reference 形如 "#4851" 或 "owner/repo#4851"
|
|
166
|
+
const hasRepo = reference.includes('/');
|
|
167
|
+
const repo = (hasRepo ? reference.split('#')[0] : repository) ?? '';
|
|
168
|
+
const numberText = reference.slice(reference.indexOf('#') + 1);
|
|
169
|
+
const number = Number.parseInt(numberText, 10);
|
|
170
|
+
if (!Number.isFinite(number)) return reference;
|
|
171
|
+
const label = repo ? `${repo}#${number}` : `#${number}`;
|
|
172
|
+
const index = references.length;
|
|
173
|
+
references.push({ repository: repo, number, label, crossRepo: hasRepo });
|
|
174
|
+
return placeholder(index);
|
|
175
|
+
};
|
|
176
|
+
// 只处理列表行;行内每个 #编号 独立替换为占位符,其余文字不动
|
|
177
|
+
return body.replace(LIST_LINE, line => line.replace(REF_IN_LINE, (m: string) => toRef(m)));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 渲染 PR/Issue 正文 markdown。列表行中的 "#编号" 引用替换为
|
|
182
|
+
* 随 issue/PR 状态变化的行内引用(图标 + 标题 + 链接色编号),
|
|
183
|
+
* 行内其它文字(resolved / fixed / 自定义文字)原样保留;
|
|
184
|
+
* 异步查询状态与标题,失败回退为纯链接色编号。
|
|
185
|
+
* @param body 原始 markdown 正文
|
|
186
|
+
* @param repository 当前仓库(owner/repo),用于展开 "#1234" 形式的短引用
|
|
187
|
+
* @param logger 可选日志器
|
|
188
|
+
*/
|
|
189
|
+
export async function renderIssueBody(body?: string, repository: string = '', logger?: Logger): Promise<any> {
|
|
190
|
+
const references: LinkedIssueReference[] = [];
|
|
191
|
+
const links: ShortUrl[] = [];
|
|
192
|
+
// 先把完整 issue/PR 地址缩短为占位符(不查 API),再处理 #编号 引用
|
|
193
|
+
const shortened = shortenIssueUrls(body || '', repository, links);
|
|
194
|
+
const withPlaceholders = extractReferences(shortened, repository, references);
|
|
195
|
+
let html = renderMarkdown(withPlaceholders);
|
|
196
|
+
|
|
197
|
+
// 缩短链接 -> 蓝色 #编号(纯链接,不查询状态与标题)
|
|
198
|
+
links.forEach((link, i) => {
|
|
199
|
+
const shortHtml = `<a class="issue-ref-number" href="${link.url}">${link.label}</a>`;
|
|
200
|
+
html = html.split(linkPlaceholder(i)).join(shortHtml);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (references.length === 0) {
|
|
204
|
+
return html;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 并发查询所有引用的 issue 状态与标题;单个失败回退为 unknown(无标题),不阻塞其他
|
|
208
|
+
const details = await Promise.all(
|
|
209
|
+
references.map(ref => fetchIssueDetail(ref.repository, ref.number, logger))
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// 占位符 -> 行内引用 HTML(列表结构与其余文字已由 markdown 渲染保留)
|
|
213
|
+
let replaced = html;
|
|
214
|
+
references.forEach((ref, i) => {
|
|
215
|
+
const refHtml = renderInlineRef(ref, details[i] ?? { status: 'unknown' });
|
|
216
|
+
replaced = replaced.split(placeholder(i)).join(refHtml);
|
|
217
|
+
});
|
|
218
|
+
return replaced;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function renderMarkdown(body?: string): any {
|
|
222
|
+
const taskLists = require('markdown-it-task-lists');
|
|
223
|
+
const abbr = require('markdown-it-abbr');
|
|
224
|
+
const container = require('markdown-it-container');
|
|
225
|
+
const footnote = require('markdown-it-footnote');
|
|
226
|
+
const ins = require('markdown-it-ins');
|
|
227
|
+
const mark = require('markdown-it-mark');
|
|
228
|
+
const sup = require('markdown-it-sup');
|
|
229
|
+
const sub = require('markdown-it-sub');
|
|
230
|
+
const renderer = markdownit({
|
|
231
|
+
html: true,
|
|
232
|
+
linkify: true,
|
|
233
|
+
breaks: true,
|
|
234
|
+
xhtmlOut: true,
|
|
235
|
+
highlight: function (str, lang): any {
|
|
236
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
237
|
+
try {
|
|
238
|
+
return (
|
|
239
|
+
'<pre><code class="hljs">' +
|
|
240
|
+
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
|
|
241
|
+
'</code></pre>'
|
|
242
|
+
);
|
|
243
|
+
} catch (__) {}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return '<pre><code class="hljs">' + renderer.utils.escapeHtml(str) + '</code></pre>';
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
.use(taskLists)
|
|
250
|
+
.use(abbr)
|
|
251
|
+
.use(container)
|
|
252
|
+
.use(emoji)
|
|
253
|
+
.use(footnote)
|
|
254
|
+
.use(ins)
|
|
255
|
+
.use(mark)
|
|
256
|
+
.use(sup)
|
|
257
|
+
.use(sub);
|
|
258
|
+
return renderer.render(body || '');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export class GitHubImage {
|
|
262
|
+
public static issuesHandler(
|
|
263
|
+
issue: Issue,
|
|
264
|
+
logger?: Logger,
|
|
265
|
+
operation?: string,
|
|
266
|
+
sender?: User,
|
|
267
|
+
extra?: string
|
|
268
|
+
): Promise<string> {
|
|
269
|
+
return issuesHandler(issue, logger, operation, sender, extra);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public static prHandler(
|
|
273
|
+
pr: PullRequest,
|
|
274
|
+
logger?: Logger,
|
|
275
|
+
operation?: string,
|
|
276
|
+
sender?: User,
|
|
277
|
+
extra?: string
|
|
278
|
+
): Promise<string> {
|
|
279
|
+
return prHandler(pr, logger, operation, sender, extra);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
public static issuesOpened(issue: OpenedIssueEvent | ReopenedIssueEvent, logger?: Logger): Promise<string> {
|
|
283
|
+
return issuesOpened(issue, logger);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public static issuesClosed(issue: ClosedIssueEvent, logger?: Logger): Promise<string> {
|
|
287
|
+
return issuesClosed(issue, logger);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public static prOpened(pr: OpenedPullRequestEvent | ReopenedPullRequestEvent, logger?: Logger): Promise<string> {
|
|
291
|
+
return prOpened(pr, logger);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public static prClosed(pr: ClosedPullRequestEvent, logger?: Logger): Promise<string> {
|
|
295
|
+
return prClosed(pr, logger);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public static releaseHandler(
|
|
299
|
+
release: Release,
|
|
300
|
+
repository?: string,
|
|
301
|
+
logger?: Logger,
|
|
302
|
+
sender?: User
|
|
303
|
+
): Promise<string> {
|
|
304
|
+
return releaseHandler(release, repository, logger, sender);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
public static releasePublished(event: AllReleaseEvent, logger?: Logger): Promise<string> {
|
|
308
|
+
return releasePublished(event, logger);
|
|
309
|
+
}
|
|
310
|
+
}
|