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
+26 -18
View File
@@ -17,11 +17,10 @@
* still display the issue in the computed column.
*/
import * as fs from 'node:fs'
import * as path from 'node:path'
import { resolveWorktreePath } from '../git/worktree'
import type { GiteaComment, GiteaIssue } from './api'
import type { Issue, IssueColumn } from './types'
import * as fs from 'node:fs'
import { resolveWorktreePath } from '../git/worktree'
import {
getCurrentUser,
getDependencies,
@@ -52,7 +51,7 @@ export function isValidSpxFilePath(v: unknown): v is string {
function isValidPrDiffFilePath(v: unknown): v is string {
return typeof v === 'string'
&& /^docs\/pr-diff\/[^\s]+\.md$/.test(v)
&& /^docs\/pr-diff\/\S+\.md$/.test(v)
}
function isIssueColumn(value: unknown): value is IssueColumn {
@@ -351,7 +350,6 @@ async function buildIssue(opts: {
})
}
catch (postErr) {
// eslint-disable-next-line no-console
console.warn(`[superpowers] failed to seed state comment on ${id}:`, postErr)
}
}
@@ -405,24 +403,34 @@ export async function loadIssues(opts: {
repo: string
/** Absolute workspace root used to resolve `worktreeExists` against disk. */
workspaceRoot?: string
/** 'mine'(默认) = assigned+created 给我的;'all' = 仓库全部工单(团队视图)。 */
scope?: 'mine' | 'all'
}): Promise<Issue[]> {
const { host, token, owner, repo, workspaceRoot } = opts
// `/user` validates the token and gives us the login. The repo-wide
// comments firehose doesn't need the login, so we kick it off in parallel.
// The two issue-filter calls need `user.login` and run together after.
const userPromise = getCurrentUser({ host, token })
// The repo-wide comments firehose doesn't depend on scope; kick it off first.
const commentsPromise = listAllRepoComments({ host, token, owner, repo })
const user = await userPromise
const [assigned, created, allComments] = await Promise.all([
listIssuesByFilter({ host, token, owner, repo, filter: 'assigned_by', user: user.login }),
listIssuesByFilter({ host, token, owner, repo, filter: 'created_by', user: user.login }),
commentsPromise,
])
const merged = mergeIssues(assigned, created)
let merged: Awaited<ReturnType<typeof listIssuesByFilter>>
let allComments: Awaited<typeof commentsPromise>
if (opts.scope === 'all') {
;[merged, allComments] = await Promise.all([
listIssuesByFilter({ host, token, owner, repo }),
commentsPromise,
])
}
else {
// `/user` validates the token and gives us the login. The two
// issue-filter calls need `user.login` and run together after.
const user = await getCurrentUser({ host, token })
const [assigned, created, comments] = await Promise.all([
listIssuesByFilter({ host, token, owner, repo, filter: 'assigned_by', user: user.login }),
listIssuesByFilter({ host, token, owner, repo, filter: 'created_by', user: user.login }),
commentsPromise,
])
merged = mergeIssues(assigned, created)
allComments = comments
}
const buckets = groupCommentsByIssue(allComments)
// 先用各工单评论 bucket 算出 column,决定是否需要 per-issue 远程拉取。