🐛 fix(vscode): 移除侵入式弹窗并过滤 PR 评论,仅对工单讨论发送简洁系统通知
This commit is contained in:
1 parent
8ea5361f3d
commit
6a2ab7eaf4
5 files changed
+172
-24
No files matched your search
@@ -53,3 +53,72 @@ export function systemCommentText(body: string): string | undefined {
|
||||
return `更新了工单状态,放到${column}`
|
||||
return '更新了工单状态'
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗 Markdown 评论内容并截断为单行纯文本摘要:
|
||||
* 1. 移除 HTML 注释与代码块
|
||||
* 2. 移除引用行(> 开头)、表格分割线行、水平分割线
|
||||
* 3. 移除/转换行内 Markdown(链接、图片、行内代码、粗斜体、删除线、表格符号、标题与列表标记等)
|
||||
* 4. 坍缩为空格单行,最多保留 maxLength 个字符,超出追加 '...'
|
||||
*/
|
||||
export function cleanCommentSummary(body: string, maxLength = 60): string {
|
||||
if (!body)
|
||||
return ''
|
||||
|
||||
// 移除 HTML 注释和代码块
|
||||
let text = body
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
|
||||
const rawLines = text.split(/\r?\n/)
|
||||
// 逐行剔除引用行、表格分割线、水平分割线
|
||||
let lines = rawLines.filter((line) => {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed)
|
||||
return false
|
||||
if (trimmed.startsWith('>'))
|
||||
return false
|
||||
if (/^[-: |]+$/.test(trimmed) && trimmed.includes('-'))
|
||||
return false
|
||||
if (/^[-*_]{3,}$/.test(trimmed))
|
||||
return false
|
||||
return true
|
||||
})
|
||||
|
||||
// 若全是引用行,回退保留去掉 > 后的行
|
||||
if (lines.length === 0) {
|
||||
lines = rawLines
|
||||
.map(line => line.trim().replace(/^>+\s*/, ''))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
text = lines.join(' ')
|
||||
// 移除图片 
|
||||
.replace(/!\[.*?\]\(.*?\)/g, '')
|
||||
// 链接 [text](url) -> text
|
||||
.replace(/\[(.*?)\]\(.*?\)/g, '$1')
|
||||
// 自动链接 <http...> 与纯标签
|
||||
.replace(/<https?:\/\/[^>]+>/g, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
// 行内代码 `code` -> code
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
// 粗体、斜体、删除线
|
||||
.replace(/(\*\*|__)(.*?)\1/g, '$2')
|
||||
.replace(/(\*|_)(.*?)\1/g, '$2')
|
||||
.replace(/~~(.*?)~~/g, '$1')
|
||||
// 任务列表 [- [x] / - [ ]]
|
||||
.replace(/\[[ x]\]\s+/gi, '')
|
||||
// 标题、无序列表、有序列表前缀
|
||||
.replace(/(^|\s)(?:#{1,6}|[*+-])\s+/g, '$1')
|
||||
.replace(/(^|\s)\d+\.\s+/g, '$1')
|
||||
// 表格竖线符号
|
||||
.replace(/\|/g, ' ')
|
||||
|
||||
// 坍缩连续空白字符为单空格
|
||||
const singleLine = text.replace(/\s+/g, ' ').trim()
|
||||
|
||||
if (singleLine.length <= maxLength)
|
||||
return singleLine
|
||||
|
||||
return `${singleLine.slice(0, maxLength)}...`
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import { buildSessionSubmitPayload } from './sessionSubmit'
|
||||
describe('buildSessionSubmitPayload', () => {
|
||||
it('换行留在粘贴括号里,发送用的回车在括号外面', () => {
|
||||
const payload = buildSessionSubmitPayload('第一行\n第二行')
|
||||
expect(payload.startsWith('\u001b[200~')).toBe(true)
|
||||
expect(payload.endsWith('\u001b[201~\r')).toBe(true)
|
||||
expect(payload.startsWith('\u001B[200~')).toBe(true)
|
||||
expect(payload.endsWith('\u001B[201~\r')).toBe(true)
|
||||
expect(payload).toContain('第一行\n第二行')
|
||||
})
|
||||
|
||||
it('正文里的转义字符会被去掉,避免提前结束粘贴', () => {
|
||||
expect(buildSessionSubmitPayload('看到\u001b[201~就截断')).toBe(
|
||||
'\u001b[200~看到[201~就截断\u001b[201~\r',
|
||||
expect(buildSessionSubmitPayload('看到\u001B[201~就截断')).toBe(
|
||||
'\u001B[200~看到[201~就截断\u001B[201~\r',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -9,10 +9,10 @@
|
||||
* 而且必须和正文在同一次写入里:分开写的话,长正文还没写完,回车可能插进正文中间。
|
||||
* 正文里的转义字符要去掉,否则会把粘贴提前截断。
|
||||
*/
|
||||
const PASTE_START = '\u001b[200~'
|
||||
const PASTE_END = '\u001b[201~'
|
||||
const PASTE_START = '\u001B[200~'
|
||||
const PASTE_END = '\u001B[201~'
|
||||
|
||||
export function buildSessionSubmitPayload(text: string): string {
|
||||
const body = text.replaceAll('\u001b', '')
|
||||
const body = text.replaceAll('\u001B', '')
|
||||
return `${PASTE_START}${body}${PASTE_END}\r`
|
||||
}
|
||||
@@ -10,18 +10,19 @@
|
||||
*/
|
||||
|
||||
import type { ExtensionContext } from 'vscode'
|
||||
import type { KanbanWebviewPanel } from '../panel/KanbanPanel'
|
||||
import type { KanbanScope } from '../panel/kanbanScope'
|
||||
import type { IssueCommentWebhookEvent, IssueWebhookEvent, PrWebhookEvent, WebhookEvent } from './server'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import { env, Uri, ViewColumn, window, workspace } from 'vscode'
|
||||
import { env, Uri, window, workspace } from 'vscode'
|
||||
import { loginForToken } from '../auth/identity'
|
||||
import { getToken } from '../auth/secrets'
|
||||
import { renderReviewPrompt } from '../cc/prompts'
|
||||
import { isMailboxIssue } from '../cc/sessionHandoff'
|
||||
import { detectRepo } from '../git/remote'
|
||||
import { deleteWebhook, getIssue, getPullRequest, listIssueComments } from '../gitea/api'
|
||||
import { systemCommentText } from '../gitea/issueComment'
|
||||
import { cleanCommentSummary, 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'
|
||||
@@ -29,7 +30,6 @@ import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from
|
||||
import { logger } from '../logging/logger'
|
||||
import * as issueChat from '../panel/handlers/issueChat'
|
||||
import { hasLiveIssueSessionTerminal } from '../panel/handlers/terminals'
|
||||
import { KanbanWebviewPanel } from '../panel/KanbanPanel'
|
||||
import { DEFAULT_KANBAN_SCOPE, KANBAN_SCOPE_KEY } from '../panel/kanbanScope'
|
||||
import { annotateMissingSessionIds } from '../sessions/sessionFiles'
|
||||
import { getSettings } from '../settings/store'
|
||||
@@ -1498,17 +1498,20 @@ class WebhookCoordinator {
|
||||
return
|
||||
}
|
||||
|
||||
// 判定发言人:如果发言人不是当前用户(不是 me),且不是插件状态机自动评论(systemCommentText(body) 为空,即真人评论):
|
||||
// 判定发言人:只有纯工单(event.prNumber === undefined)上的真人评论才提醒;
|
||||
// PR 评论(代码审查、PR 讨论、Review 机器人等)绝对不发通知。
|
||||
const isPureIssueComment = event.prNumber === undefined
|
||||
const author = event.commentAuthor
|
||||
const me = await loginForToken(ctx.host, ctx.token)
|
||||
const isSystemComment = !!systemCommentText(body)
|
||||
const isMine = !!(me && author === me)
|
||||
|
||||
if (author && !isMine && !isSystemComment) {
|
||||
if (isPureIssueComment && author && !isMine && !isSystemComment) {
|
||||
const summary = cleanCommentSummary(body)
|
||||
// 触发 macOS 系统原生右上角横幅通知
|
||||
execFile('osascript', [
|
||||
'-e',
|
||||
`display notification ${JSON.stringify(body)} with title ${JSON.stringify(`工单 #${realIssueNumber} 新讨论`)} subtitle ${JSON.stringify(author)}`,
|
||||
`display notification ${JSON.stringify(summary)} with title ${JSON.stringify(`工单 #${realIssueNumber} 新讨论`)} subtitle ${JSON.stringify(author)}`,
|
||||
], (err) => {
|
||||
if (err) {
|
||||
logger.add({
|
||||
@@ -1519,17 +1522,6 @@ class WebhookCoordinator {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 同时在 VS Code 内部弹出信息框
|
||||
void window.showInformationMessage(`[#${realIssueNumber}] ${author}: ${body}`, '查看').then((action) => {
|
||||
if (action === '查看') {
|
||||
const panel = this.activePanel ?? (this.ctx ? KanbanWebviewPanel.createOrShow(this.ctx) : undefined)
|
||||
if (panel) {
|
||||
panel.reveal(ViewColumn.Active)
|
||||
panel.selectIssueAndTab(realIssueNumber, 'chat')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!hasReviewMarker(body)) {
|
||||
|
||||
Reference in new issue
Block a user