diff --git a/vscode/src/webhook/coordinator.ts b/vscode/src/webhook/coordinator.ts index 7a0a335..e4aee34 100644 --- a/vscode/src/webhook/coordinator.ts +++ b/vscode/src/webhook/coordinator.ts @@ -409,6 +409,11 @@ class WebhookCoordinator { else if (event.action === 'edited') { await this.handleIssueEdited(event) } + else if (event.action === 'assigned' || event.action === 'unassigned') { + // Gitea 的 issue_assign 事件 X-Gitea-Event 也是 'issues',靠 + // action 区分,不会走到独立的事件分支。 + await this.handleIssueAssigned(event) + } else { logger.add({ level: 'info', @@ -671,6 +676,95 @@ class WebhookCoordinator { } } + /** + * issue assigned/unassigned:指派关系变化后,受影响机器的 'mine' 看板 + * 要实时上卡/撤卡,不能等下次手动刷新。 + */ + private async handleIssueAssigned(event: IssueWebhookEvent): Promise { + if (!this.ctx) + return + // 没面板就没什么可更新,下次打开看板的批量加载会兜底。 + if (!this.activePanel) + return + + const ws = workspace.workspaceFolders?.[0]?.uri.fsPath + if (!ws) { + logger.add({ + level: 'warn', + source: 'webhook', + message: 'issue assigned 事件忽略:未打开工作区', + }) + return + } + const remote = await detectRepo(ws) + if (!remote) { + logger.add({ + level: 'warn', + source: 'webhook', + message: 'issue assigned 事件忽略:工作区未关联 gitea', + }) + return + } + const token = await getToken(this.ctx, remote.host) + if (!token) { + logger.add({ + level: 'warn', + source: 'webhook', + message: 'issue assigned 事件忽略:未配置 token', + }) + return + } + + // 与 handleIssueOpened 同一套 'mine' 可见性门禁:指派给我 → 上卡, + // 与我无关(含被取消指派)→ 撤卡。身份/payload 只在 'mine' 时解析, + // 省一次身份请求。 + const scope = this.ctx.workspaceState.get(KANBAN_SCOPE_KEY) ?? 'mine' + let poster: string | undefined + let assignees: string[] = [] + let me: string | undefined + if (scope === 'mine') { + const raw = event.raw as { issue?: { user?: { login?: string } | null, assignees?: Array<{ login?: string } | null> | null } | null } | null + poster = raw?.issue?.user?.login + assignees = (raw?.issue?.assignees ?? []).flatMap(a => (a?.login ? [a.login] : [])) + me = await loginForToken(remote.host, token) + } + if (!decideIssueAppend({ scope, pendingCreatedLocally: false, poster, assignees, me })) { + logger.add({ + level: 'info', + source: 'webhook', + message: `issue #${event.issueNumber} ${event.action === 'unassigned' ? '取消指派' : '指派'}后与我无关,scope=mine 撤卡`, + details: `poster=${poster ?? '<未知>'} assignees=[${assignees.join(',')}] me=${me ?? '<未知>'}`, + }) + // 卡本来不在板上时前端 remove 是无害 no-op。 + this.activePanel.postMessage({ type: 'issue/remove', issueNumber: event.issueNumber }) + return + } + try { + const loaded = await loadSingleIssue({ + host: remote.host, + owner: remote.owner, + repo: remote.repo, + token, + workspaceRoot: ws, + issueNumber: event.issueNumber, + }) + const issue = loaded ? (await annotateReviewSessionFileExists(overlayLocalIssueState(this.ctx, [loaded], ws)))[0] : null + // 前端对 issue/append 按 number 去重原位替换:已在板上的卡刷新 + // assignee,不在板上的新增——指派给我的工单借此实时上卡。 + if (issue) + this.activePanel.postMessage({ type: 'issue/append', issue }) + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err) + logger.add({ + level: 'warn', + source: 'webhook', + message: 'loadSingleIssue 失败(issue assigned)', + details: msg, + }) + } + } + /** * Issue `edited` payload: cc updates the issue body with `` * / `` annotation lines as it discovers spec/plan files.