✨ feat(vscode): 多人协作二期:状态分层/写校验/签名/团队视图
- 本机状态(会话id/worktree/profile/prDiffFile)迁 workspaceState, 共享 state JSON 只留团队字段;读侧本地记录叠加,旧数据兜底 - mergeStateJsonComment 写后读回校验,并发覆盖时在最新状态上重放一次 - webhook 支持 Gitea secret,校验 X-Gitea-Signature(HMAC-SHA256), 不匹配 401;设置面板新增 Webhook Secret 字段 - 看板新增 我的/全部 范围切换(团队视图),按工作区持久化
This commit is contained in:
@@ -21,6 +21,7 @@ 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 { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
||||
import { logger } from '../logging/logger'
|
||||
import { hasLiveIssueSessionTerminal } from '../panel/handlers/terminals'
|
||||
import { getSettings } from '../settings/store'
|
||||
@@ -86,6 +87,7 @@ class WebhookCoordinator {
|
||||
}
|
||||
|
||||
this.server = new WebhookServer()
|
||||
this.server.setSecret(getSettings(ctx).webhookSecret)
|
||||
this.eventSubscription = this.server.onEvent((event: WebhookEvent) => {
|
||||
void this.handleEvent(event)
|
||||
})
|
||||
@@ -160,6 +162,11 @@ class WebhookCoordinator {
|
||||
await srv.start(port)
|
||||
}
|
||||
|
||||
/** 设置面板保存后热更新签名 secret,无需重启监听。 */
|
||||
ensureSecret(secret: string): void {
|
||||
this.server?.setSecret(secret)
|
||||
}
|
||||
|
||||
/** Register a pending hook after createWebhook on gitea succeeded. */
|
||||
async addPending(issueNumber: number, info: PendingHook): Promise<void> {
|
||||
this.pendingHooks.set(issueNumber, info)
|
||||
@@ -529,14 +536,15 @@ class WebhookCoordinator {
|
||||
message: `匹配到 pending 创建 nonce=${nonce} → 写入 state JSON`,
|
||||
details: `issue=#${event.issueNumber} sessionId=${pending.sessionId ?? '<待定>'}`,
|
||||
})
|
||||
const extra: Record<string, unknown> = {
|
||||
column: 'todo',
|
||||
color: pending.color,
|
||||
}
|
||||
// 会话 id / profile 路径是本机状态,落 workspaceState;共享评论只写
|
||||
// column / color 这类团队可见字段。
|
||||
const localPatch: Record<string, unknown> = {}
|
||||
if (typeof pending.sessionId === 'string' && pending.sessionId.length > 0)
|
||||
extra.sessionId = pending.sessionId
|
||||
localPatch.sessionId = pending.sessionId
|
||||
if (typeof pending.brainstormProfilePath === 'string' && pending.brainstormProfilePath.length > 0)
|
||||
extra.brainstormProfilePath = pending.brainstormProfilePath
|
||||
localPatch.brainstormProfilePath = pending.brainstormProfilePath
|
||||
if (Object.keys(localPatch).length > 0)
|
||||
await mergeLocalIssueState(this.ctx, 'gitea', event.issueNumber, localPatch)
|
||||
|
||||
try {
|
||||
await mergeStateJsonComment({
|
||||
@@ -545,7 +553,10 @@ class WebhookCoordinator {
|
||||
repo: remote.repo,
|
||||
token,
|
||||
issueNumber: event.issueNumber,
|
||||
extra,
|
||||
extra: {
|
||||
column: 'todo',
|
||||
color: pending.color,
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
@@ -589,7 +600,7 @@ class WebhookCoordinator {
|
||||
|
||||
if (this.activePanel) {
|
||||
try {
|
||||
const issue = await loadSingleIssue({
|
||||
const loaded = await loadSingleIssue({
|
||||
host: remote.host,
|
||||
owner: remote.owner,
|
||||
repo: remote.repo,
|
||||
@@ -597,6 +608,7 @@ class WebhookCoordinator {
|
||||
workspaceRoot: ws,
|
||||
issueNumber: event.issueNumber,
|
||||
})
|
||||
const issue = loaded ? overlayLocalIssueState(this.ctx, [loaded], ws)[0] : null
|
||||
if (issue) {
|
||||
this.activePanel.postMessage({ type: 'issue/append', issue, select: pending ? true : undefined })
|
||||
if (pending) {
|
||||
@@ -1473,39 +1485,35 @@ class WebhookCoordinator {
|
||||
return
|
||||
}
|
||||
|
||||
// Re-fetch state JSON to pick up the issue's worktreePath. We don't
|
||||
// hard-fail if it's missing — triggerAutoReviewTab will fall back to
|
||||
// workspaceRoot and toast the user.
|
||||
// worktree 是本机状态:优先 workspaceState 本地记录('' 墓碑 = 本机已删,
|
||||
// 不回落);本地无记录时读共享评论兜底(存量数据)。缺失不硬失败——
|
||||
// triggerAutoReviewTab 会回退 workspaceRoot 并 toast。
|
||||
let worktreePath = ''
|
||||
try {
|
||||
const comments = await listIssueComments({
|
||||
host: ctx.host,
|
||||
token: ctx.token,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
index: issueNumber,
|
||||
})
|
||||
const last = comments[comments.length - 1]
|
||||
const body = (last?.body ?? '').trim()
|
||||
if (body) {
|
||||
try {
|
||||
const parsed = JSON.parse(body) as { worktreePath?: unknown }
|
||||
if (typeof parsed?.worktreePath === 'string' && parsed.worktreePath.length > 0)
|
||||
worktreePath = parsed.worktreePath
|
||||
}
|
||||
catch {
|
||||
// last comment isn't JSON — proceed without a worktree.
|
||||
}
|
||||
}
|
||||
const local = getLocalIssueState(this.ctx, 'gitea', issueNumber)
|
||||
if (local.worktreePath !== undefined) {
|
||||
worktreePath = local.worktreePath
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: `读取 state JSON 失败(triggerReview)#${issueNumber}`,
|
||||
details: message,
|
||||
})
|
||||
else {
|
||||
try {
|
||||
const state = await readStateJsonComment({
|
||||
host: ctx.host,
|
||||
token: ctx.token,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
issueNumber,
|
||||
})
|
||||
if (typeof state.worktreePath === 'string')
|
||||
worktreePath = state.worktreePath
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: `读取 state JSON 失败(triggerReview)#${issueNumber}`,
|
||||
details: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logger.add({
|
||||
|
||||
Reference in New Issue
Block a user