✨ feat(vscode): 申请移交时给负责人发系统通知

This commit is contained in:
cruldra committed 2026-10-04 10:01:41 +08:00
1 parent 5bcbb87152
commit a1fac829f8
3 files changed
+108 -1

No files matched your search

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest, handoffRequestNotices } from './handoffRequest'
import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest, handoffRequestNotices, handoffRequestSystemNotice } from './handoffRequest'
const review = {
source: 'gitea' as const,
@@ -37,6 +37,34 @@ describe('移交申请', () => {
.toEqual(['#435 没交出来:会话太大'])
})
it('系统通知只打给负责人,同一条申请不重复', () => {
expect(handoffRequestSystemNotice({
issueNumber: 435,
me: 'chw',
assignees: ['chw'],
from: 'cruldra',
})).toBe('cruldra 想要 #435')
expect(handoffRequestSystemNotice({
issueNumber: 435,
me: 'chw',
assignees: ['chw'],
from: 'cruldra',
seenFrom: 'cruldra',
})).toBeUndefined()
expect(handoffRequestSystemNotice({
issueNumber: 435,
me: 'cruldra',
assignees: ['chw'],
from: 'cruldra',
})).toBeUndefined()
expect(handoffRequestSystemNotice({
issueNumber: 435,
me: 'other',
assignees: ['chw'],
from: 'cruldra',
})).toBeUndefined()
})
it('只有负责人能同意或拒绝', () => {
const pending = { ...review, handoffRequestFrom: 'cruldra' }
expect(canAnswerHandoffRequest(pending, 'chw')).toBe(true)
@@ -50,6 +50,22 @@ export function handoffRequestNotices(before: HandoffRequestFacts & { number: nu
return notices
}
/** 负责人没开着看板也能看到的那一句。同一申请人不重复。 */
export function handoffRequestSystemNotice(opts: {
issueNumber: number
me?: string
assignees: string[]
from?: string
seenFrom?: string
}): string | undefined {
const from = opts.from?.trim()
if (!from || !opts.me || from === opts.me || from === opts.seenFrom)
return
if (!opts.assignees.includes(opts.me))
return
return `${from} 想要 #${opts.issueNumber}`
}
export function canAnswerHandoffRequest(issue: HandoffRequestFacts, me: string | undefined): boolean {
return !!me
&& openReview(issue)
+63
View File
@@ -28,6 +28,7 @@ import { hasReviewMarker, parseClosingIssueNumber, stripReviewMarker } from '../
import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
import { logger } from '../logging/logger'
import { handoffRequestSystemNotice } from '../panel/handlers/handoffRequest'
import * as issueChat from '../panel/handlers/issueChat'
import { hasLiveIssueSessionTerminal } from '../panel/handlers/terminals'
import { DEFAULT_KANBAN_SCOPE, KANBAN_SCOPE_KEY } from '../panel/kanbanScope'
@@ -68,6 +69,16 @@ const BODY_MARKERS = [
/** 本次从 body 里扫出来的 marker 值,按 state JSON 字段名索引。 */
type MarkerPatch = { [K in typeof BODY_MARKERS[number]['field']]?: string }
function handoffRequestFromOf(body: string): string {
try {
const obj = JSON.parse(body) as { handoffRequestFrom?: unknown }
return typeof obj.handoffRequestFrom === 'string' ? obj.handoffRequestFrom.trim() : ''
}
catch {
return ''
}
}
function parseBodyMarkers(body: string): MarkerPatch {
const out: MarkerPatch = {}
for (const { type, field, ext } of BODY_MARKERS) {
@@ -100,6 +111,8 @@ class WebhookCoordinator {
private eventSubscription?: { dispose: () => void }
/** 同 issue 自动审查启动中不重入(opened 与 synchronize 常连发)。 */
private reviewInFlight = new Set<number>()
/** 已经为这条移交申请通知过负责人。申请取消后清掉。 */
private handoffRequestSeen = new Map<number, string>()
/** Called once from extension.ts activate(). Idempotent. */
init(ctx: ExtensionContext): void {
@@ -1436,6 +1449,55 @@ class WebhookCoordinator {
* first review and `injectIntoImplTerminal` appends the merge-to-main
* suffix.
*/
/** 申请移交时给负责人发系统通知。看板关着也发。 */
private async notifyHandoffRequest(event: IssueCommentWebhookEvent): Promise<void> {
const from = handoffRequestFromOf(event.commentBody)
if (!from) {
this.handoffRequestSeen.delete(event.issueNumber)
return
}
const seen = this.handoffRequestSeen.get(event.issueNumber) ?? ''
if (from === seen || !this.ctx)
return
const ws = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!ws)
return
const remote = await detectRepo(ws)
if (!remote)
return
const token = await getToken(this.ctx, remote.host)
if (!token)
return
const me = await loginForToken(remote.host, token)
const payload = event.raw as { issue?: { assignees?: Array<{ login?: string } | null> | null } | null } | null
const assignees = (payload?.issue?.assignees ?? []).flatMap(a => (a?.login ? [a.login] : []))
if (!me || assignees.length === 0)
return
const notice = handoffRequestSystemNotice({
issueNumber: event.issueNumber,
me,
assignees,
from,
seenFrom: seen,
})
this.handoffRequestSeen.set(event.issueNumber, from)
if (!notice)
return
execFile('osascript', [
'-e',
`display notification ${JSON.stringify('打开看板同意或拒绝')} with title ${JSON.stringify(notice)}`,
], (err) => {
if (err) {
logger.add({
level: 'warn',
source: 'webhook',
message: `移交申请通知发送失败 #${event.issueNumber}`,
details: err.message,
})
}
})
}
private async handleIssueCommentCreated(event: IssueCommentWebhookEvent): Promise<void> {
if (event.action !== 'created') {
logger.add({
@@ -1519,6 +1581,7 @@ class WebhookCoordinator {
source: 'webhook',
message: `issue_comment 是 state 评论,刷新 #${event.issueNumber} 的卡片`,
})
await this.notifyHandoffRequest(event)
await this.refreshIssueCard({
issueNumber: event.issueNumber,
raw: event.raw,