✨ feat(vscode): Gitea API 增加 assignees 列表与工单附件读写
Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
* distinguish 401 (token invalid) from other failure modes.
|
||||
*/
|
||||
|
||||
import { Buffer } from 'node:buffer'
|
||||
|
||||
import { logger } from '../logging/logger'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
@@ -641,3 +643,112 @@ export async function getRawFile(opts: {
|
||||
return ''
|
||||
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
|
||||
token: string
|
||||
owner: string
|
||||
repo: string
|
||||
}): Promise<GiteaUser[]> {
|
||||
const res = await fetch(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/assignees`,
|
||||
{ headers: authHeaders(opts.token) },
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
export async function getIssueAttachment(opts: {
|
||||
host: string
|
||||
token: string
|
||||
owner: string
|
||||
repo: string
|
||||
index: number
|
||||
attachmentId: number
|
||||
}): Promise<GiteaAttachment> {
|
||||
const res = await fetch(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/issues/${opts.index}/assets/${opts.attachmentId}`,
|
||||
{ headers: authHeaders(opts.token) },
|
||||
)
|
||||
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