✨ feat(vscode): 会话交接改走阿里云 OSS 收件箱、换 xz 压缩,另加从 env 文件配置移交存储的 URI
- 会话包传到 OSS 上收件人的收件箱 spx-handoff/<owner>/<repo>/session/<收件人>/<sid>!<发送人>!<会话名>.tar.xz,对象私有;owner/repo/收件人转小写 - 待接收列表就是列自己的收件箱;同一会话再交给同一人先删旧包;接收后删包,包已被别处收走时提示并刷新 - 打包换成 xz 1 级,和工单移交共用 packHandoff / unpackHandoff,删掉 gzip 版 - 删掉邮箱工单整套(mailboxIssue、MAILBOX_*、isMailboxIssue 及看板 / webhook 里的过滤、附件名编解码)和不再用的 Gitea 附件、标签、建工单接口 - session-handoff/accept 与待接收项改用 OSS 对象 key 标识 - 没配移交存储时发送、接收、刷新都提示去配,刷新回空列表 - 进度提示照工单移交:同一条提示依次显示打包、上传、下载多少 MB、解包、装会话 - 新增 URI configure-handoff-store?envFile=<绝对路径>:读 OSS_ENDPOINT / OSS_BUCKET_NAME / OSS_ACCESS_KEY / OSS_SECRET_KEY 保存,和命令共用 saveHandoffStore;URI 入口按 path 统一分发,未知 path 报错
This commit is contained in:
1 parent
81fe14f6dc
commit
fcee4143d4
26 files changed
+542
-515
No files matched your search
@@ -9,8 +9,6 @@
|
||||
* distinguish 401 (token invalid) from other failure modes.
|
||||
*/
|
||||
|
||||
import { Buffer } from 'node:buffer'
|
||||
|
||||
import { logger } from '../logging/logger'
|
||||
|
||||
export const PAGE_SIZE = 50
|
||||
@@ -100,8 +98,6 @@ 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
|
||||
@@ -113,8 +109,6 @@ 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))
|
||||
|
||||
@@ -131,77 +125,6 @@ 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
|
||||
@@ -713,15 +636,6 @@ export async function getRawFile(opts: {
|
||||
return res.text()
|
||||
}
|
||||
|
||||
export interface GiteaAttachment {
|
||||
id: number
|
||||
name: string
|
||||
size: number
|
||||
uuid: string
|
||||
browser_download_url: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** 可被指派的用户(仓库协作者 + owner),移交选人用。 */
|
||||
export async function listRepoAssignees(opts: {
|
||||
host: string
|
||||
@@ -736,72 +650,3 @@ export async function listRepoAssignees(opts: {
|
||||
await ensureOk(res)
|
||||
return (await res.json()) as GiteaUser[]
|
||||
}
|
||||
|
||||
export async function listIssueAttachments(opts: {
|
||||
host: string
|
||||
token: string
|
||||
owner: string
|
||||
repo: string
|
||||
index: number
|
||||
}): Promise<GiteaAttachment[]> {
|
||||
const res = await fetch(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/issues/${opts.index}/assets`,
|
||||
{ headers: authHeaders(opts.token) },
|
||||
)
|
||||
await ensureOk(res)
|
||||
return (await res.json()) as GiteaAttachment[]
|
||||
}
|
||||
|
||||
export async function uploadIssueAttachment(opts: {
|
||||
host: string
|
||||
token: string
|
||||
owner: string
|
||||
repo: string
|
||||
index: number
|
||||
name: string
|
||||
data: Buffer
|
||||
}): Promise<GiteaAttachment> {
|
||||
const url = new URL(`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/issues/${opts.index}/assets`)
|
||||
url.searchParams.set('name', opts.name)
|
||||
const form = new FormData()
|
||||
// Buffer 的底层 ArrayBufferLike 包含 SharedArrayBuffer,BlobPart 只认 ArrayBuffer;
|
||||
// 用 Uint8Array 包一层拷贝到普通 ArrayBuffer 上,绕开这条 TS 类型不兼容。
|
||||
form.append('attachment', new Blob([new Uint8Array(opts.data)], { type: 'application/gzip' }), opts.name)
|
||||
// 不能带 Content-Type:multipart boundary 由 fetch 按 FormData 自动生成。
|
||||
const res = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `token ${opts.token}`, Accept: 'application/json' },
|
||||
body: form,
|
||||
})
|
||||
await ensureOk(res)
|
||||
return (await res.json()) as GiteaAttachment
|
||||
}
|
||||
|
||||
/**
|
||||
* `browser_download_url` 走的是 web 路由(/attachments/<uuid>),Gitea 的 web
|
||||
* 鉴权组同样接受 `Authorization: token`。未登录时它会 302 到登录页返回 HTML,
|
||||
* 所以除 ok 外还要拒掉 HTML 响应,别把登录页当 tgz 存下来。
|
||||
*/
|
||||
export async function downloadAttachment(opts: { token: string, url: string }): Promise<Buffer> {
|
||||
const res = await fetch(opts.url, { headers: { Authorization: `token ${opts.token}` }, redirect: 'follow' })
|
||||
await ensureOk(res)
|
||||
const contentType = res.headers.get('content-type') ?? ''
|
||||
if (contentType.includes('text/html'))
|
||||
throw new GiteaApiError(res.status, `附件下载被重定向到页面(鉴权失败?):${opts.url}`)
|
||||
return Buffer.from(await res.arrayBuffer())
|
||||
}
|
||||
|
||||
export async function deleteIssueAttachment(opts: {
|
||||
host: string
|
||||
token: string
|
||||
owner: string
|
||||
repo: string
|
||||
index: number
|
||||
attachmentId: number
|
||||
}): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/issues/${opts.index}/assets/${opts.attachmentId}`,
|
||||
{ method: 'DELETE', headers: authHeaders(opts.token) },
|
||||
)
|
||||
await ensureOk(res)
|
||||
}
|
||||
Reference in new issue
Block a user