From 858bd1e34b91e4053f902bedbb4cf37b0699bf48 Mon Sep 17 00:00:00 2001 From: cheho Date: Tue, 29 Sep 2026 04:43:55 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(vscode):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20PR=20=E4=B8=8A=E7=9A=84=E5=AE=A1=E6=9F=A5=E6=84=8F?= =?UTF-8?q?=E8=A7=81=E6=B2=A1=E8=87=AA=E5=8A=A8=E8=BD=AC=E7=BB=99=E5=AE=9E?= =?UTF-8?q?=E6=96=BD=E4=BC=9A=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 识别审查评论标记和 PR 里 Closes #N 的规则被写坏(多了反斜杠、标记名少了个 s), 审查评论全被当成普通评论丢掉。现在这两条规则只在 gitea/markers.ts 里定义一份, webhook 和评论气泡都调用它,并补上单测,以后写坏会直接测出来。 --- vscode/src/gitea/issueComment.ts | 6 ++- vscode/src/gitea/markers.test.ts | 65 +++++++++++++++++++++++++++++++ vscode/src/gitea/markers.ts | 27 +++++++++++++ vscode/src/webhook/coordinator.ts | 18 ++++----- 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 vscode/src/gitea/markers.test.ts create mode 100644 vscode/src/gitea/markers.ts diff --git a/vscode/src/gitea/issueComment.ts b/vscode/src/gitea/issueComment.ts index 9504bb3..ab84833 100644 --- a/vscode/src/gitea/issueComment.ts +++ b/vscode/src/gitea/issueComment.ts @@ -1,3 +1,5 @@ +import { hasReviewMarker, stripReviewMarker } from './markers' + const COLUMN_LABEL: Record = { 'todo': '待办', 'in-progress': '进行中', @@ -26,8 +28,8 @@ const STATE_KEYS = new Set([ /** 插件自己写的评论。返回系统气泡文案;普通讨论返回 undefined。 */ export function systemCommentText(body: string): string | undefined { const trimmed = body.trim() - if (//i.test(trimmed)) { - const text = trimmed.replace(//gi, '').trim() + if (hasReviewMarker(trimmed)) { + const text = stripReviewMarker(trimmed) return text.length > 0 ? text : '提交了审查' } if (!trimmed.startsWith('{')) diff --git a/vscode/src/gitea/markers.test.ts b/vscode/src/gitea/markers.test.ts new file mode 100644 index 0000000..d4ab6ca --- /dev/null +++ b/vscode/src/gitea/markers.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest' +import { hasReviewMarker, parseClosingIssueNumber, stripReviewMarker } from './markers' + +const REAL_REVIEW = ` +## 审查意见 + +- **coordinator.ts:1452** 正则多写了反斜杠 +- 建议补单测 + +\`\`\`ts +const x = 1 +\`\`\`` + +describe('审查标记', () => { + it('真实审查评论:识别为审查,去标记后正文完整', () => { + expect(hasReviewMarker(REAL_REVIEW)).toBe(true) + expect(stripReviewMarker(REAL_REVIEW)).toBe(REAL_REVIEW.slice('\n'.length)) + }) + + it('普通评论不是审查', () => { + expect(hasReviewMarker('这个方案可以')).toBe(false) + expect(hasReviewMarker('提到了 spx:review=1 但不是注释')).toBe(false) + }) + + it('大小写和注释内空白都放宽', () => { + expect(hasReviewMarker('\n正文')).toBe(true) + expect(hasReviewMarker('')).toBe(true) + }) + + it('反复调用结果稳定', () => { + for (let i = 0; i < 3; i++) { + expect(hasReviewMarker(REAL_REVIEW)).toBe(true) + expect(stripReviewMarker(REAL_REVIEW)).toMatch(/^## 审查意见/) + } + }) + + it('多个标记全部去掉,只剩标记时为空串', () => { + expect(stripReviewMarker('\n a b')).toBe('a b') + expect(stripReviewMarker('\n')).toBe('') + }) +}) + +describe('parseClosingIssueNumber', () => { + it('多行 PR body 最后一行 Closes #462', () => { + const body = '## 改动\n\n- 修了正则\n- 加了测试\n\nCloses #462' + expect(parseClosingIssueNumber(body)).toBe(462) + }) + + it('其它关键字与大小写', () => { + expect(parseClosingIssueNumber('fixes #12')).toBe(12) + expect(parseClosingIssueNumber('FiXeS #7')).toBe(7) + expect(parseClosingIssueNumber('RESOLVE #3')).toBe(3) + expect(parseClosingIssueNumber('close\t#5')).toBe(5) + }) + + it('没有关键字返回 undefined', () => { + expect(parseClosingIssueNumber('关联 #462')).toBeUndefined() + expect(parseClosingIssueNumber('')).toBeUndefined() + }) + + it('不误匹配:# 前无空白、关键字粘在别的词后面', () => { + expect(parseClosingIssueNumber('closes#1')).toBeUndefined() + expect(parseClosingIssueNumber('prefixcloses #1')).toBeUndefined() + }) +}) diff --git a/vscode/src/gitea/markers.ts b/vscode/src/gitea/markers.ts new file mode 100644 index 0000000..0d379ac --- /dev/null +++ b/vscode/src/gitea/markers.ts @@ -0,0 +1,27 @@ +/** + * 评论 / PR 正文里约定标记的唯一解析入口。 + * + * - 审查标记:审查会话发到 PR 上的评论第一行写 ``, + * webhook 靠它识别"这是审查意见,要转给实施会话"。 + * - 关闭关键字:PR body 里的 `Closes #N` 等,用来从 PR 反查工单号。 + */ + +const REVIEW_MARKER = //i +// 带 g 的正则只给 replace 用:replace 每次从头扫,不受 lastIndex 残留影响。 +const REVIEW_MARKER_WITH_TRAILING_SPACE = new RegExp(`${REVIEW_MARKER.source}\\s*`, 'gi') +const CLOSING_KEYWORD = /\b(?:closes|fixes|resolves|close|fix|resolve)\s+#(\d+)/i + +export function hasReviewMarker(body: string): boolean { + return REVIEW_MARKER.test(body) +} + +/** 去掉所有审查标记及其后空白,返回 trim 后的正文。 */ +export function stripReviewMarker(body: string): string { + return body.replace(REVIEW_MARKER_WITH_TRAILING_SPACE, '').trim() +} + +/** 从 PR body 取第一个关闭关键字指向的工单号。 */ +export function parseClosingIssueNumber(body: string): number | undefined { + const m = body.match(CLOSING_KEYWORD) + return m ? Number.parseInt(m[1], 10) : undefined +} diff --git a/vscode/src/webhook/coordinator.ts b/vscode/src/webhook/coordinator.ts index 1237074..6b92165 100644 --- a/vscode/src/webhook/coordinator.ts +++ b/vscode/src/webhook/coordinator.ts @@ -23,6 +23,7 @@ import { detectRepo } from '../git/remote' import { deleteWebhook, getIssue, getPullRequest, listIssueComments } from '../gitea/api' import { systemCommentText } from '../gitea/issueComment' import { loadIssues, loadSingleIssue } from '../gitea/issueLoader' +import { hasReviewMarker, parseClosingIssueNumber, stripReviewMarker } from '../gitea/markers' import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson' import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState' import { logger } from '../logging/logger' @@ -357,9 +358,8 @@ class WebhookCoordinator { return event.issueNumber const body = event.body || '' - const m = body.match(/\b(?:closes|fixes|resolves|close|fix|resolve)\s+#(\d+)/i) - if (m) { - const n = Number.parseInt(m[1], 10) + const n = parseClosingIssueNumber(body) + if (n !== undefined) { logger.add({ level: 'info', source: 'webhook', @@ -1449,9 +1449,9 @@ class WebhookCoordinator { repo: ctx.repo, index: event.prNumber, }) - const m = (pr.body ?? '').match(/\\b(?:closes|fixes|resolves|close|fix|resolve)\\s+#(\\d+)/i) - if (m) { - realIssueNumber = Number.parseInt(m[1], 10) + const closing = parseClosingIssueNumber(pr.body ?? '') + if (closing !== undefined) { + realIssueNumber = closing logger.add({ level: 'info', source: 'webhook', @@ -1532,7 +1532,7 @@ class WebhookCoordinator { }) } - if (!//i.test(body)) { + if (!hasReviewMarker(body)) { logger.add({ level: 'info', source: 'webhook', @@ -1540,7 +1540,7 @@ class WebhookCoordinator { }) return } - const text = body.replace(/\\s*/i, '').trim() + const text = stripReviewMarker(body) if (!text) { logger.add({ level: 'warn', @@ -1570,7 +1570,7 @@ class WebhookCoordinator { repo: ctx.repo, index: commentsIndex, }) - reviewCount = comments.filter(c => //i.test(c.body ?? '')).length + reviewCount = comments.filter(c => hasReviewMarker(c.body ?? '')).length } catch (err) { const message = err instanceof Error ? err.message : String(err)