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:
2026-08-19 00:32:06 +08:00
parent c370214d12
commit faf227412e
18 changed files with 665 additions and 202 deletions
+29 -11
View File
@@ -4,7 +4,9 @@ import type {
TerminalEditorLocationOptions,
WebviewPanel,
} from 'vscode'
import type { HookContext } from '../git/worktreeHooks'
import type { Issue } from '../gitea/types'
import type { IssueRef } from '../issues/stateRouter'
import type { ExtensionToWebview, WebviewToExtension } from './messages'
import { randomBytes } from 'node:crypto'
import * as fs from 'node:fs'
@@ -12,13 +14,11 @@ import * as path from 'node:path'
import { env, TabInputTerminal, ThemeColor, Uri, ViewColumn, window, workspace } from 'vscode'
import { deleteToken, getToken } from '../auth/secrets'
import { detectRepo } from '../git/remote'
import type { HookContext } from '../git/worktreeHooks'
import {
GiteaApiError,
postIssueComment,
} from '../gitea/api'
import { loadIssues } from '../gitea/issueLoader'
import type { IssueRef } from '../issues/stateRouter'
import { overlayLocalIssueState } from '../issues/localState'
import { closeIssueByRef, mergeIssueState, readIssueState } from '../issues/stateRouter'
import { logger } from '../logging/logger'
import { getEffectiveCommitProfilePath, getSettings } from '../settings/store'
@@ -39,6 +39,9 @@ import { PALETTE, resolveIssueColor, themeColorIdToIconUri } from './issueColor'
/** Resolved at runtime via getProfilesDir() — do not hardcode user paths. */
export { getDefaultProfilePath, getPrDiffSummaryProfilePath } from '../cc/profiles'
/** workspaceState key:看板范围('mine' 只看我的 / 'all' 团队全部)。 */
const SCOPE_KEY = 'superpowers.kanbanScope'
export class KanbanWebviewPanel {
static readonly viewType = 'superpowers.kanbanPanel'
@@ -108,9 +111,11 @@ export class KanbanWebviewPanel {
workspaceRoot: string
inboxDir: string
terminalName: string
/** The brainstorm terminal created synchronously in `handleIssueCreate`.
/**
* The brainstorm terminal created synchronously in `handleIssueCreate`.
* Stored here so `linkPendingTerminalToIssue` can promote it into
* `newIssueTerminals` once the webhook tells us the issueNumber. */
* `newIssueTerminals` once the webhook tells us the issueNumber.
*/
terminal: Terminal
createdAt: number
}>()
@@ -360,6 +365,12 @@ export class KanbanWebviewPanel {
void this.loadAndPush()
return
}
if (msg.type === 'issues/set-scope') {
// 视图偏好按工作区持久化;切换即重拉。
void this.context.workspaceState.update(SCOPE_KEY, msg.scope)
void this.loadAndPush()
return
}
if (msg.type === 'settings/save') {
void settings.handleSettingsSave(this, msg)
return
@@ -594,7 +605,6 @@ export class KanbanWebviewPanel {
}
if (msg.type === 'pr-diff-mode/set') {
void prFiles.handleSetPrDiffMode(this, msg.mode)
return
}
}
@@ -777,7 +787,6 @@ export class KanbanWebviewPanel {
return worktree.dispatchImplTabPostCloseAsync(this, issueNumber)
}
/**
* Server-side lock check for prerequisite gating. Fetches a fresh issues
* snapshot (we don't trust webview state) and reports whether
@@ -846,6 +855,7 @@ export class KanbanWebviewPanel {
host,
tokenSaved: false,
webhookPort: s.webhookPort,
webhookSecret: s.webhookSecret,
brainstormPrompt: s.brainstormPrompt,
brainstormContinuePrompt: s.brainstormContinuePrompt,
implementPlanPrompt: s.implementPlanPrompt,
@@ -869,8 +879,9 @@ export class KanbanWebviewPanel {
return
}
const scope = this.context.workspaceState.get<'mine' | 'all'>(SCOPE_KEY) ?? 'mine'
try {
const giteaIssues = await loadIssues({ host, token, owner, repo, workspaceRoot })
const giteaIssues = await loadIssues({ host, token, owner, repo, workspaceRoot, scope })
// YouTrack is a best-effort second source: a failure here must never
// break the gitea board, so swallow it into a toast + log.
let youtrackList: Issue[] = []
@@ -888,7 +899,10 @@ export class KanbanWebviewPanel {
dismissOnTimer: 6000,
})
}
const issues = this.withLiveTerminalTabState([...giteaIssues, ...youtrackList])
// 本机记录(会话 id / worktree 等)叠加在共享状态之上,再补 live terminal 态。
const issues = this.withLiveTerminalTabState(
overlayLocalIssueState(this.context, [...giteaIssues, ...youtrackList], workspaceRoot),
)
this.issueRefs = new Map(
issues.map(i => [i.number, { source: i.source ?? 'gitea', externalId: i.externalId }] as const),
)
@@ -896,6 +910,7 @@ export class KanbanWebviewPanel {
this.postMessage({
type: 'issues/update',
issues,
scope,
globalAutoReview: ytSettings.autoReview,
youtrackConfigured: ytSettings.youtrackBaseUrl.trim() !== '' && ytSettings.youtrackProjectShortName.trim() !== '',
})
@@ -914,6 +929,7 @@ export class KanbanWebviewPanel {
errorMessage: 'Token 无效或已过期,请重新填写',
tokenSaved: false,
webhookPort: s.webhookPort,
webhookSecret: s.webhookSecret,
brainstormPrompt: s.brainstormPrompt,
brainstormContinuePrompt: s.brainstormContinuePrompt,
implementPlanPrompt: s.implementPlanPrompt,
@@ -970,8 +986,10 @@ export class KanbanWebviewPanel {
return mergeIssueState(this.context, this.refFor(issueNumber), extra)
}
/** Resolve/close the issue in its tracker. Returns false when a youtrack
* close command can't be determined. */
/**
* Resolve/close the issue in its tracker. Returns false when a youtrack
* close command can't be determined.
*/
// internal: handler 模块访问
closeIssueByNumber(issueNumber: number): Promise<boolean> {
return closeIssueByRef(this.context, this.refFor(issueNumber))