@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,310 +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, '&amp;')
70
- .replace(/</g, '&lt;')
71
- .replace(/>/g, '&gt;')
72
- .replace(/"/g, '&quot;');
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
- }
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, '&amp;')
70
+ .replace(/</g, '&lt;')
71
+ .replace(/>/g, '&gt;')
72
+ .replace(/"/g, '&quot;');
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
+ }