✨ feat(vscode): 支持讨论评论引用与 macOS 系统级新评论桌面提醒
This commit is contained in:
1 parent
540c212bad
commit
a2c3c1f919
13 files changed
+638
-302
No files matched your search
@@ -10,23 +10,25 @@
|
||||
*/
|
||||
|
||||
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, window, workspace } from 'vscode'
|
||||
import { env, Uri, ViewColumn, 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 { loadIssues, loadSingleIssue } from '../gitea/issueLoader'
|
||||
import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
|
||||
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
||||
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'
|
||||
@@ -1423,49 +1425,7 @@ class WebhookCoordinator {
|
||||
message: `issue_comment body diag #${event.issueNumber} len=${body.length}`,
|
||||
details: body.slice(0, 200),
|
||||
})
|
||||
if (this.activePanel && typeof event.issueNumber === 'number') {
|
||||
void issueChat.handleIssueComments(this.activePanel, event.issueNumber)
|
||||
}
|
||||
|
||||
// 看板的列就存在工单的 state JSON 评论里,每次改列都新发一条。谁把卡
|
||||
// 拖到哪一列,其他人的面板靠这条评论跟着动——解析出 column 就重拉这张
|
||||
// 卡,不限于「完成」。PR 上的评论不可能是 state 评论,直接排除。
|
||||
if (event.prNumber === undefined && hasStateColumn(body)) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
message: `issue_comment 是 state 评论,刷新 #${event.issueNumber} 的卡片`,
|
||||
})
|
||||
await this.refreshIssueCard({
|
||||
issueNumber: event.issueNumber,
|
||||
raw: event.raw,
|
||||
reason: '列变化',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!/<!--\s*spx:review=1\s*-->/i.test(body)) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
message: `issue_comment 无 spx:review 标识,忽略 #${event.issueNumber}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const text = body.replace(/<!--\s*spx:review=1\s*-->\s*/i, '').trim()
|
||||
if (!text) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: `审查评论正文为空 #${event.issueNumber}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// resolveRepoContext is keyed by pendingHooks/workspace, not by issue
|
||||
// number — it just needs *some* number to log against. We pass the
|
||||
// gitea-reported number here (which is the PR number when this comment
|
||||
// is on a PR) and reverse-resolve to the real issue number below.
|
||||
const ctx = await this.resolveRepoContext(event.issueNumber)
|
||||
if (!ctx) {
|
||||
logger.add({
|
||||
@@ -1489,7 +1449,7 @@ class WebhookCoordinator {
|
||||
repo: ctx.repo,
|
||||
index: event.prNumber,
|
||||
})
|
||||
const m = (pr.body ?? '').match(/\b(?:closes|fixes|resolves|close|fix|resolve)\s+#(\d+)/i)
|
||||
const m = (pr.body ?? '').match(/\\b(?:closes|fixes|resolves|close|fix|resolve)\\s+#(\\d+)/i)
|
||||
if (m) {
|
||||
realIssueNumber = Number.parseInt(m[1], 10)
|
||||
logger.add({
|
||||
@@ -1497,9 +1457,6 @@ class WebhookCoordinator {
|
||||
source: 'webhook',
|
||||
message: `issue_comment 评论在 PR #${event.prNumber},反查到工单 #${realIssueNumber}`,
|
||||
})
|
||||
if (this.activePanel) {
|
||||
void issueChat.handleIssueComments(this.activePanel, realIssueNumber)
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.add({
|
||||
@@ -1519,6 +1476,80 @@ class WebhookCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
// 若看板面板正打开且当前选中的正是该工单,自动刷新讨论列表
|
||||
if (this.activePanel) {
|
||||
void issueChat.handleIssueComments(this.activePanel, realIssueNumber)
|
||||
}
|
||||
|
||||
// 看板的列就存在工单的 state JSON 评论里,每次改列都新发一条。谁把卡
|
||||
// 拖到哪一列,其他人的面板靠这条评论跟着动——解析出 column 就重拉这张
|
||||
// 卡,不限于「完成」。PR 上的评论不可能是 state 评论,直接排除。
|
||||
if (event.prNumber === undefined && hasStateColumn(body)) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
message: `issue_comment 是 state 评论,刷新 #${event.issueNumber} 的卡片`,
|
||||
})
|
||||
await this.refreshIssueCard({
|
||||
issueNumber: event.issueNumber,
|
||||
raw: event.raw,
|
||||
reason: '列变化',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 判定发言人:如果发言人不是当前用户(不是 me),且不是插件状态机自动评论(systemCommentText(body) 为空,即真人评论):
|
||||
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) {
|
||||
// 触发 macOS 系统原生右上角横幅通知
|
||||
execFile('osascript', [
|
||||
'-e',
|
||||
`display notification ${JSON.stringify(body)} with title ${JSON.stringify(`工单 #${realIssueNumber} 新讨论`)} subtitle ${JSON.stringify(author)}`,
|
||||
], (err) => {
|
||||
if (err) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: `macOS 原生通知发送失败 #${realIssueNumber}`,
|
||||
details: err.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 同时在 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 (!/<!--\\s+px:review=1\\s*-->/i.test(body)) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
message: `issue_comment 无 spx:review 标识,忽略 #${event.issueNumber}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const text = body.replace(/<!--\\s+px:review=1\\s*-->\\s*/i, '').trim()
|
||||
if (!text) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: `审查评论正文为空 #${event.issueNumber}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (await this.resolveOwnership(realIssueNumber, ctx) === 'observe') {
|
||||
// 审查反馈只注入责任人机器的实施终端;其他机器上本来也找不到
|
||||
// 该终端,提前拦截省掉后面的评论计数 API 往返。
|
||||
@@ -1539,7 +1570,7 @@ class WebhookCoordinator {
|
||||
repo: ctx.repo,
|
||||
index: commentsIndex,
|
||||
})
|
||||
reviewCount = comments.filter(c => /<!--\s*spx:review=1\s*-->/i.test(c.body ?? '')).length
|
||||
reviewCount = comments.filter(c => /<!--\\s+px:review=1\\s*-->/i.test(c.body ?? '')).length
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
|
||||
Reference in new issue
Block a user