8dfc4c1
import { OPS } from './ops'
export interface ApiCallConfig {
pathParams?: Record<string, string | number>
params?: Record<string, unknown>
data?: unknown
headers?: Record<string, string>
signal?: AbortSignal
}
interface RawResponse {
status: number
data: unknown
headers?: Headers
}
export interface ApiEnvelopeShape<T = unknown> {
code: number
message: string
data?: T | null
error?: string | null
error_code?: string | null
request_id?: string
}
function text(value: unknown): string {
if (typeof value === 'string') return value
if (value && typeof value === 'object') {
const body = value as Record<string, unknown>
for (const key of ['error', 'message']) {
const candidate = body[key]
if (typeof candidate === 'string' && candidate.trim()) return candidate
}
}
return ''
}
function envelope(response: RawResponse): ApiEnvelopeShape {
const body = response.data
if (body && typeof body === 'object') {
const record = body as Record<string, unknown>
if (typeof record.code === 'number') return record as unknown as ApiEnvelopeShape
}
const status = response.status
return {
code: status,
message: text(body) || (status >= 500 ? '服务未响应(网关错误)' : status >= 400 ? '服务返回了非接口数据' : ''),
data: null,
error: null,
error_code: null,
}
}
function fail(error: unknown): never {
if (error instanceof DOMException && error.name === 'AbortError') throw error
const wrapped = new Error(error instanceof Error ? error.message : 'network request failed')
wrapped.name = 'NetworkError'
throw wrapped
}
function query(params: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
if (!params) return undefined
const out: Record<string, unknown> = {}
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== '') out[key] = value
}
return Object.keys(out).length ? out : undefined
}
function normalize(key: string): string {
return key.toLowerCase().replace(/[-_]/g, '')
}
const ALIASES: Record<string, string> = {
'space.listspaces': 'space.space_list',
'space.createspace': 'space.space_create',
'space.getspace': 'space.space_get',
'space.updatespace': 'space.space_update',
'space.deletespace': 'space.space_delete',
'space.restorespace': 'space.space_restore',
'space.listspacemembers': 'space.space_member_list',
'space.addmember': 'space.space_member_add',
'space.getmember': 'space.space_member_get',
'space.updatemember': 'space.space_member_update',
'space.updateselfmember': 'space.space_member_update_self',
'space.removemember': 'space.space_member_remove',
'space.listbans': 'space.space_ban_list',
'space.getban': 'space.space_ban_get',
'space.updateban': 'space.space_ban_update',
'space.unbanuser': 'space.space_ban_delete',
'space.listinvites': 'space.space_invite_list',
'space.createinvite': 'space.space_invite_create',
'space.revokeinvite': 'space.space_invite_revoke',
'remotes.fetch': 'remotes.fetch_remote',
'tag_protection.listpolicies': 'tag_protection.list_tag_policies',
'tag_protection.createpolicy': 'tag_protection.create_tag_policy',
'tag_protection.getpolicy': 'tag_protection.get_tag_policy',
'tag_protection.updatepolicy': 'tag_protection.update_tag_policy',
'tag_protection.deletepolicy': 'tag_protection.delete_tag_policy',
}
const RESOLVED = new Map<string, { key: string; entry: (typeof OPS)[string] }>()
for (const [key, entry] of Object.entries(OPS)) {
const table = RESOLVED as Map<string, { key: string; entry: (typeof OPS)[string] }>
table.set(`${normalize(key)}`, { key, entry })
}
for (const [from, to] of Object.entries(ALIASES)) {
const hit = RESOLVED.get(normalize(to))
if (hit) RESOLVED.set(normalize(from), hit)
}
export const MISSING_OPS = new Set<string>()
async function invoke(tag: string, op: string, config: ApiCallConfig = {}): Promise<ApiEnvelopeShape> {
const lookup = RESOLVED.get(`${normalize(tag)}.${normalize(op)}`)
if (!lookup) {
MISSING_OPS.add(`${tag}.${op}`)
return {
code: 501,
message: `未提供接口:${tag}.${op}`,
data: null,
error: null,
error_code: 'not_implemented',
}
}
const { entry } = lookup
const names = Object.keys(config.pathParams ?? {}).slice(0, entry.path)
const args: unknown[] = []
if (entry.path > 0 && names.length !== entry.path) {
return {
code: 400,
message: `缺少路径参数:${tag}.${op}`,
data: null,
error: null,
error_code: 'bad_request',
}
}
for (const name of names) args.push(config.pathParams?.[name])
const queryArgs = entry.query ? query(config.params) : undefined
if (entry.bodyFirst) {
if (entry.body) args.push(config.data)
if (entry.query) args.push(queryArgs)
} else {
if (entry.query) args.push(queryArgs)
if (entry.body) args.push(config.data)
}
const options: RequestInit = { credentials: 'same-origin' }
if (config.headers) options.headers = config.headers
if (config.signal) options.signal = config.signal
args.push(options)
try {
const response = (await entry.fn(...args)) as RawResponse
const body = envelope(response)
if (response.status >= 400 && !body.error_code && typeof body.error === 'string') {
body.error_code = null
}
return { ...body, code: response.status >= 400 ? response.status : body.code }
} catch (error) {
fail(error)
}
}
type ProxyFn = (config?: ApiCallConfig) => Promise<ApiEnvelopeShape>
export const Apis: Record<string, Record<string, ProxyFn>> = new Proxy(
{},
{
get: (_target, tag: string) =>
new Proxy(
{},
{
get: (_scope, op: string) => (config?: ApiCallConfig) => invoke(tag, op, config),
},
),
},
) as Record<string, Record<string, ProxyFn>>
export function trackFetch(input: string, init?: RequestInit): Promise<Response> {
return fetch(input, { credentials: 'same-origin', ...init })
}
export function newTraceparent(): string {
const hex = (n: number) => Array.from({ length: n }, () => Math.floor(Math.random() * 16).toString(16)).join('')
return `00-${hex(32)}-${hex(16)}-01`
}
export type * from '@kenapi/apiclient/index.schemas'
export type {
AddMemberBody,
BanMemberBody,
CreateInviteBody,
CreateOrganizationBody,
EnvelopeCreateInviteResponse,
EnvelopePermissionCheck,
InviteSummary,
MemberSummary,
OrganizationSummary,
RedeemInviteBody,
SpaceJoinRequestState,
SpaceMemberRole,
UpdateBanBody,
UpdateMemberBody,
UpdateOrganizationBody,
} from '@kenapi/apiclient/index.schemas'
export * from './ops'
export type EnvelopeInviteSummary = import('@kenapi/apiclient/index.schemas').EnvelopeCreateInviteResponse
export interface RoomMemberRow {
user: { user_id: number; username: string; display_name: string; avatar_url: string }
user_id?: number
username?: string
display_name?: string | null
avatar_url?: string | null
role?: string
nick?: string | null
role_keys?: string[]
joined_at?: string | null
muted?: boolean
permissions?: string[]
}
export type EnvelopeVecRoomMemberDto = { data?: RoomMemberRow[] | null }
export type CreateInviteCmd = import('@kenapi/apiclient/index.schemas').CreateInviteBody
export type UpdateMemberCmd = import('@kenapi/apiclient/index.schemas').UpdateMemberBody
export type UpdateSpaceCmd = import('@kenapi/apiclient/index.schemas').UpdateOrganizationBody
export type CreateSpaceCmd = import('@kenapi/apiclient/index.schemas').CreateOrganizationBody
export type AcceptInviteCmd = import('@kenapi/apiclient/index.schemas').RedeemInviteBody
export type AddMemberCmd = import('@kenapi/apiclient/index.schemas').AddMemberBody
export type BanUserCmd = import('@kenapi/apiclient/index.schemas').BanMemberBody
export type UpdateBanCmd = import('@kenapi/apiclient/index.schemas').UpdateBanBody
export type SpaceJoinRequestStatus = import('@kenapi/apiclient/index.schemas').SpaceJoinRequestState
export default Apis