✨ feat(vscode): 会话 tab 可交接对话给同事

This commit is contained in:
cruldra committed 2026-08-27 15:14:42 +08:00
1 parent 5ee8226167
commit 77037924ef
20 files changed
+938 -26

No files matched your search

+75
View File
@@ -92,6 +92,8 @@ export async function listIssuesByFilter(opts: {
/** 缺省 = 不按人过滤,拉仓库全部工单(团队视图)。 */
filter?: 'assigned_by' | 'created_by'
user?: string
/** 逗号分隔的 label 名。label 必须已存在,否则 Gitea 会丢掉过滤条件。 */
labels?: string
}): Promise<GiteaIssue[]> {
const out: GiteaIssue[] = []
let page = 1
@@ -103,6 +105,8 @@ export async function listIssuesByFilter(opts: {
url.searchParams.set('state', 'all')
if (opts.filter && opts.user)
url.searchParams.set(opts.filter, opts.user)
if (opts.labels)
url.searchParams.set('labels', opts.labels)
url.searchParams.set('limit', String(PAGE_SIZE))
url.searchParams.set('page', String(page))
@@ -119,6 +123,77 @@ export async function listIssuesByFilter(opts: {
return out
}
export interface GiteaLabel {
id: number
name: string
color: string
}
export async function listRepoLabels(opts: {
host: string
token: string
owner: string
repo: string
}): Promise<GiteaLabel[]> {
const out: GiteaLabel[] = []
let page = 1
while (true) {
const url = new URL(`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/labels`)
url.searchParams.set('limit', String(PAGE_SIZE))
url.searchParams.set('page', String(page))
const res = await fetch(url.toString(), { headers: authHeaders(opts.token) })
await ensureOk(res)
const batch = await res.json() as GiteaLabel[]
if (!Array.isArray(batch) || batch.length === 0)
break
out.push(...batch)
if (batch.length < PAGE_SIZE)
break
page += 1
}
return out
}
export async function createRepoLabel(opts: {
host: string
token: string
owner: string
repo: string
name: string
color: string
description?: string
}): Promise<GiteaLabel> {
const res = await fetch(`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/labels`, {
method: 'POST',
headers: { ...authHeaders(opts.token), 'Content-Type': 'application/json' },
body: JSON.stringify({ name: opts.name, color: opts.color, description: opts.description ?? '' }),
})
await ensureOk(res)
return (await res.json()) as GiteaLabel
}
export async function createIssue(opts: {
host: string
token: string
owner: string
repo: string
title: string
body: string
labels?: number[]
}): Promise<GiteaIssue> {
const res = await fetch(`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/issues`, {
method: 'POST',
headers: { ...authHeaders(opts.token), 'Content-Type': 'application/json' },
body: JSON.stringify({
title: opts.title,
body: opts.body,
...(opts.labels && opts.labels.length > 0 ? { labels: opts.labels } : {}),
}),
})
await ensureOk(res)
return (await res.json()) as GiteaIssue
}
/**
* Fetches the firehose `/repos/{owner}/{repo}/issues/comments` endpoint, which
* returns every comment in the repo across all issues. Paginated until a short
+3 -1
View File
@@ -20,6 +20,7 @@
import type { GiteaComment, GiteaIssue } from './api'
import type { Issue, IssueColumn } from './types'
import * as fs from 'node:fs'
import { isMailboxIssue } from '../cc/sessionHandoff'
import { resolveWorktreePath } from '../git/worktree'
import {
getCurrentUser,
@@ -448,6 +449,7 @@ export async function loadIssues(opts: {
merged = mergeIssues(assigned, created)
allComments = comments
}
merged = merged.filter(issue => !isMailboxIssue(issue))
const buckets = groupCommentsByIssue(allComments)
// 先用各工单评论 bucket 算出 column,决定是否需要 per-issue 远程拉取。
@@ -527,7 +529,7 @@ export async function loadSingleIssue(opts: {
const { host, token, owner, repo, workspaceRoot, issueNumber } = opts
const issue = await getIssue({ host, token, owner, repo, index: issueNumber })
if (!issue)
if (!issue || isMailboxIssue(issue))
return null
const [comments, prerequisite] = await Promise.all([
+38
View File
@@ -0,0 +1,38 @@
import type { GiteaLabel } from './api'
import { isMailboxIssue, MAILBOX_BODY, MAILBOX_LABEL, MAILBOX_TITLE } from '../cc/sessionHandoff'
import { createIssue, createRepoLabel, listIssuesByFilter, listRepoLabels } from './api'
export async function findMailboxIssue(opts: {
host: string
token: string
owner: string
repo: string
}): Promise<number | undefined> {
const labels = await listRepoLabels(opts)
if (!labels.some(l => l.name === MAILBOX_LABEL))
return undefined
const issues = await listIssuesByFilter({ ...opts, labels: MAILBOX_LABEL })
return issues.find(i => isMailboxIssue(i))?.number
}
export async function ensureMailboxIssue(opts: {
host: string
token: string
owner: string
repo: string
}): Promise<number> {
const existing = await findMailboxIssue(opts)
if (existing !== undefined)
return existing
const labels = await listRepoLabels(opts)
let label: GiteaLabel | undefined = labels.find(l => l.name === MAILBOX_LABEL)
if (!label)
label = await createRepoLabel({ ...opts, name: MAILBOX_LABEL, color: '586069', description: 'Superwork session mailbox' })
const created = await createIssue({
...opts,
title: MAILBOX_TITLE,
body: MAILBOX_BODY,
labels: [label.id],
})
return created.number
}