✨ feat(vscode): 同事改工单所在列、开关工单时,本机看板实时跟着动
此前只有新建工单、改标题、改指派会实时上卡;列变化存在 state JSON 评论里, 而 issue_comment 处理只认带 spx:review 标识的审查评论,别人拖卡这边毫无反应, issues closed/reopened 也没接。抽出 refreshIssueCard 作为统一刷卡入口:重拉 该工单、叠加本机运行态、按 mine 可见性门禁决定上卡还是撤卡,只刷 UI,不写 远端状态也不起会话,观察者机器执行同样安全。state 评论(解析出 column)与 工单开关都接到这个入口上。
This commit is contained in:
1 parent
6603e76fe1
commit
fb122b76ce
3 files changed
+105
-17
No files matched your search
@@ -43,6 +43,15 @@ function parseStateComment(body: string | undefined | null): Record<string, unkn
|
||||
return hasKnownField ? obj : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论是否是一条带 `column` 的 state JSON——即「这张卡换列了」的信号。
|
||||
* webhook 收到这种评论时据此刷新对应卡片。
|
||||
*/
|
||||
export function hasStateColumn(body: string | undefined | null): boolean {
|
||||
const parsed = parseStateComment(body)
|
||||
return parsed !== null && 'column' in parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* 从尾往前找最近一条 state JSON comment,返回解析结果;找不到返回 null。
|
||||
* 跳过中途插入的普通文本/JSON 内容评论,从而不丢失历史状态。
|
||||
|
||||
@@ -22,7 +22,7 @@ import { isMailboxIssue } from '../cc/sessionHandoff'
|
||||
import { detectRepo } from '../git/remote'
|
||||
import { deleteWebhook, getIssue, getPullRequest, listIssueComments } from '../gitea/api'
|
||||
import { loadIssues, loadSingleIssue } from '../gitea/issueLoader'
|
||||
import { mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
|
||||
import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
|
||||
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
||||
import { logger } from '../logging/logger'
|
||||
import { hasLiveIssueSessionTerminal } from '../panel/handlers/terminals'
|
||||
@@ -442,6 +442,9 @@ class WebhookCoordinator {
|
||||
else if (event.action === 'edited') {
|
||||
await this.handleIssueEdited(event)
|
||||
}
|
||||
else if (event.action === 'closed' || event.action === 'reopened') {
|
||||
await this.handleIssueStateChanged(event)
|
||||
}
|
||||
else if (event.action === 'assigned' || event.action === 'unassigned') {
|
||||
// Gitea 的 issue_assign 事件 X-Gitea-Event 也是 'issues',靠
|
||||
// action 区分,不会走到独立的事件分支。
|
||||
@@ -720,10 +723,19 @@ class WebhookCoordinator {
|
||||
}
|
||||
|
||||
/**
|
||||
* issue assigned/unassigned:指派关系变化后,受影响机器的 'mine' 看板
|
||||
* 要实时上卡/撤卡,不能等下次手动刷新。
|
||||
* 把一张工单的最新状态推给看板:重拉这张工单、叠加本机运行态,再按
|
||||
* 'mine' 可见性门禁决定上卡还是撤卡。指派变化、列变化、开关工单都走
|
||||
* 这里。它只刷 UI——不写远端状态、不起会话,所以在"观察者"机器上执行
|
||||
* 同样安全,同事把卡拖到哪一列,这边就跟着动到哪一列。
|
||||
*/
|
||||
private async handleIssueAssigned(event: IssueWebhookEvent): Promise<void> {
|
||||
private async refreshIssueCard(opts: {
|
||||
issueNumber: number
|
||||
/** 原始 webhook payload,'mine' 门禁从里面取 poster / assignees。 */
|
||||
raw: unknown
|
||||
/** 日志里标明这次刷新由什么事件触发,如 'issue assigned'。 */
|
||||
reason: string
|
||||
}): Promise<void> {
|
||||
const { issueNumber, raw, reason } = opts
|
||||
if (!this.ctx)
|
||||
return
|
||||
// 没面板就没什么可更新,下次打开看板的批量加载会兜底。
|
||||
@@ -735,7 +747,7 @@ class WebhookCoordinator {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: 'issue assigned 事件忽略:未打开工作区',
|
||||
message: `${reason} 事件忽略:未打开工作区`,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -744,7 +756,7 @@ class WebhookCoordinator {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: 'issue assigned 事件忽略:工作区未关联 gitea',
|
||||
message: `${reason} 事件忽略:工作区未关联 gitea`,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -753,12 +765,12 @@ class WebhookCoordinator {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: 'issue assigned 事件忽略:未配置 token',
|
||||
message: `${reason} 事件忽略:未配置 token`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 与 handleIssueOpened 同一套 'mine' 可见性门禁:指派给我 → 上卡,
|
||||
// 与 handleIssueOpened 同一套 'mine' 可见性门禁:与我有关 → 上卡,
|
||||
// 与我无关(含被取消指派)→ 撤卡。身份/payload 只在 'mine' 时解析,
|
||||
// 省一次身份请求。
|
||||
const scope = this.ctx.workspaceState.get<KanbanScope>(KANBAN_SCOPE_KEY) ?? DEFAULT_KANBAN_SCOPE
|
||||
@@ -766,20 +778,20 @@ class WebhookCoordinator {
|
||||
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] : []))
|
||||
const payload = raw as { issue?: { user?: { login?: string } | null, assignees?: Array<{ login?: string } | null> | null } | null } | null
|
||||
poster = payload?.issue?.user?.login
|
||||
assignees = (payload?.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 撤卡`,
|
||||
message: `issue #${issueNumber} ${reason} 后与我无关,scope=mine 撤卡`,
|
||||
details: `poster=${poster ?? '<未知>'} assignees=[${assignees.join(',')}] me=${me ?? '<未知>'}`,
|
||||
})
|
||||
// 卡本来不在板上时前端 remove 是无害 no-op。
|
||||
this.activePanel.postMessage({ type: 'issue/remove', issueNumber: event.issueNumber })
|
||||
this.activePanel.postMessage({ type: 'issue/remove', issueNumber })
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -789,11 +801,11 @@ class WebhookCoordinator {
|
||||
repo: remote.repo,
|
||||
token,
|
||||
workspaceRoot: ws,
|
||||
issueNumber: event.issueNumber,
|
||||
issueNumber,
|
||||
})
|
||||
const issue = loaded ? (await annotateReviewSessionFileExists(overlayLocalIssueState(this.ctx, [loaded], ws)))[0] : null
|
||||
// 前端对 issue/append 按 number 去重原位替换:已在板上的卡刷新
|
||||
// assignee,不在板上的新增——指派给我的工单借此实时上卡。
|
||||
// 前端对 issue/append 按 number 去重原位替换:已在板上的卡就地刷新
|
||||
// (含所在列),不在板上的新增——指派给我的工单借此实时上卡。
|
||||
if (issue) {
|
||||
this.activePanel.noteIssueOwnership(issue)
|
||||
this.activePanel.postMessage({ type: 'issue/append', issue })
|
||||
@@ -804,12 +816,36 @@ class WebhookCoordinator {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: 'loadSingleIssue 失败(issue assigned)',
|
||||
message: `loadSingleIssue 失败(${reason})`,
|
||||
details: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* issue assigned/unassigned:指派关系变化后,受影响机器的 'mine' 看板
|
||||
* 要实时上卡/撤卡,不能等下次手动刷新。
|
||||
*/
|
||||
private async handleIssueAssigned(event: IssueWebhookEvent): Promise<void> {
|
||||
await this.refreshIssueCard({
|
||||
issueNumber: event.issueNumber,
|
||||
raw: event.raw,
|
||||
reason: event.action === 'unassigned' ? '取消指派' : '指派',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* issue closed/reopened:工单直接在 Gitea 上开关时,状态评论不一定跟着
|
||||
* 变,但卡片的默认列会变(关闭 → 完成),所以照样重拉一次。
|
||||
*/
|
||||
private async handleIssueStateChanged(event: IssueWebhookEvent): Promise<void> {
|
||||
await this.refreshIssueCard({
|
||||
issueNumber: event.issueNumber,
|
||||
raw: event.raw,
|
||||
reason: event.action === 'closed' ? '工单关闭' : '工单重开',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue `edited` payload: cc 会往 issue body 里追加 `<!-- spx:spec=... -->`
|
||||
* / `<!-- spx:plan=... -->` / `<!-- spx:prototype=... -->` 三种 marker。
|
||||
@@ -1386,6 +1422,23 @@ class WebhookCoordinator {
|
||||
message: `issue_comment body diag #${event.issueNumber} len=${body.length}`,
|
||||
details: body.slice(0, 200),
|
||||
})
|
||||
// 看板的列就存在工单的 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',
|
||||
|
||||
Reference in new issue
Block a user