feat(vscode): webhook 副作用按 assignee 门禁,适配多人协作

This commit is contained in:
2026-08-18 23:57:21 +08:00
parent c20bd86370
commit c370214d12
11 changed files with 450 additions and 22 deletions
+131
View File
@@ -0,0 +1,131 @@
/**
* 会话身份:当前 Gitea token 对应的用户,按 host 缓存。
*
* 多人协作的三个消费方:
* - webhook 副作用门禁(coordinator)用 login 判断"这是不是我的工单"
* - 实施认领(handleImplement)把 assignee 设为当前用户
* - 状态栏常驻展示身份,token 失效/用错同事 token 时第一眼就能看出来
*/
import type { ExtensionContext, StatusBarItem } from 'vscode'
import type { GiteaUser } from '../gitea/api'
import { spawnSync } from 'node:child_process'
import { StatusBarAlignment, ThemeColor, window, workspace } from 'vscode'
import { detectRepo } from '../git/remote'
import { getCurrentUser } from '../gitea/api'
import { logger } from '../logging/logger'
import { getToken } from './secrets'
const userByHost = new Map<string, GiteaUser>()
let statusBar: StatusBarItem | undefined
/**
* 解析并缓存 token 对应的 login。失败(网络/token 失效)返回 undefined,
* 由调用方决定降级行为——门禁侧的语义是"身份未知 → observe"。
*/
export async function loginForToken(host: string, token: string): Promise<string | undefined> {
const hit = userByHost.get(host)
if (hit)
return hit.login
try {
const user = await getCurrentUser({ host, token })
userByHost.set(host, user)
return user.login
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.add({
level: 'error',
source: 'identity',
message: `解析 Gitea 身份失败 host=${host}`,
details: message,
})
return undefined
}
}
/** token 变更后必须调用,否则状态栏和门禁还认旧身份。 */
export function clearIdentityCache(host?: string): void {
if (host)
userByHost.delete(host)
else
userByHost.clear()
}
/**
* 激活时(以及 token 保存后)调用:解析当前工作区的 Gitea 身份,
* 展示到状态栏,并与 git 提交邮箱比对——token 被同步/复制到同事机器
* 是已发生过的真实事故(chw 机器),这里是最后一道人眼防线。
*/
export async function initIdentity(ctx: ExtensionContext): Promise<void> {
if (!statusBar) {
statusBar = window.createStatusBarItem(StatusBarAlignment.Left, 0)
ctx.subscriptions.push(statusBar)
}
const ws = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!ws) {
statusBar.hide()
return
}
const remote = await detectRepo(ws)
if (!remote) {
statusBar.hide()
return
}
const token = await getToken(ctx, remote.host)
if (!token) {
statusBar.text = '$(account) spx: 未配置 token'
statusBar.tooltip = `尚未为 ${remote.host} 配置 Gitea token,点击配置`
statusBar.command = 'superpowers.setGiteaToken'
statusBar.backgroundColor = new ThemeColor('statusBarItem.warningBackground')
statusBar.show()
return
}
const login = await loginForToken(remote.host, token)
if (!login) {
statusBar.text = '$(account) spx: token 失效'
statusBar.tooltip = `${remote.host} 的 token 无法解析身份,点击重新配置`
statusBar.command = 'superpowers.setGiteaToken'
statusBar.backgroundColor = new ThemeColor('statusBarItem.errorBackground')
statusBar.show()
return
}
statusBar.text = `$(account) spx: ${login}`
statusBar.tooltip = `Gitea 身份 ${login} @ ${remote.host}`
statusBar.command = 'superpowers.setGiteaToken'
statusBar.backgroundColor = undefined
statusBar.show()
logger.add({
level: 'info',
source: 'identity',
message: `当前 Gitea 身份 ${login} @ ${remote.host}`,
})
checkGitIdentityMatch(ws, remote.host, login)
}
/**
* Gitea 账号邮箱 vs `git config user.email`:不一致基本等于"用了别人的
* token"或"git 身份没配"。只警告不拦截——同名多邮箱的合法场景存在。
*/
function checkGitIdentityMatch(workspaceRoot: string, host: string, login: string): void {
const giteaEmail = userByHost.get(host)?.email?.trim().toLowerCase()
if (!giteaEmail)
return
const res = spawnSync('git', ['-C', workspaceRoot, 'config', 'user.email'], { encoding: 'utf-8' })
const gitEmail = (res.stdout ?? '').trim().toLowerCase()
if (!gitEmail || gitEmail === giteaEmail)
return
logger.add({
level: 'warn',
source: 'identity',
message: `Gitea 身份与 git 提交邮箱不一致: ${login}(${giteaEmail}) vs git(${gitEmail})`,
})
void window.showWarningMessage(
`Gitea token 身份是 ${login}${giteaEmail}),但 git 提交邮箱是 ${gitEmail}。确认没有用错同事的 token。`,
)
}