✨ feat(vscode): 申请移交时给负责人发系统通知
This commit is contained in:
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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user