✨ 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.
|
* distinguish 401 (token invalid) from other failure modes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Buffer } from 'node:buffer'
|
||||||
|
|
||||||
import { logger } from '../logging/logger'
|
import { logger } from '../logging/logger'
|
||||||
|
|
||||||
const PAGE_SIZE = 50
|
const PAGE_SIZE = 50
|
||||||
@@ -641,3 +643,112 @@ export async function getRawFile(opts: {
|
|||||||
return ''
|
return ''
|
||||||
return res.text()
|
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