/** * Thin API client for the org admin backend. Same-origin cookie auth. */ export class ApiError extends Error { code: string; status: number; constructor(code: string, message: string, status: number) { super(message); this.name = 'ApiError'; this.code = code; this.status = status; } } async function request(method: string, url: string, body?: unknown): Promise { const init: RequestInit = { method, credentials: 'same-origin', headers: body !== undefined ? { 'content-type': 'application/json' } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined, }; const res = await fetch(url, init); const text = await res.text(); let data: unknown = null; if (text !== '') { try { data = JSON.parse(text); } catch { data = text; } } if (!res.ok) { const err = (data as { error?: { code?: string; message?: string } } | null)?.error; throw new ApiError(err?.code ?? 'http_error', err?.message ?? `HTTP ${res.status}`, res.status); } return data; } const get = (u: string) => request('GET', u); const post = (u: string, b?: unknown) => request('POST', u, b); const put = (u: string, b?: unknown) => request('PUT', u, b); const patch = (u: string, b?: unknown) => request('PATCH', u, b); const del = (u: string) => request('DELETE', u); const orgBase = (slug: string) => `/api/org/${encodeURIComponent(slug)}`; // --- Types --- export interface OrgMembership { id: string; slug: string; name: string; status: string; role: 'OWNER' | 'ADMIN' | 'MEMBER'; } export interface MeResponse { user: { id: string; feishuOpenId: string; displayName: string; avatarUrl: string | null; }; organizations: OrgMembership[]; } export interface OrgMember { userId: string; feishuOpenId: string; displayName: string; avatarUrl: string | null; role: 'OWNER' | 'ADMIN' | 'MEMBER'; createdAt: string; } export interface TeamRow { id: string; slug: string; name: string; description: string | null; memberCount: number; createdAt: string; } export interface TeamMemberRow { userId: string; feishuOpenId: string; displayName: string; createdAt: string; } export interface ExplorerFolder { id: string; name: string; parentId: string | null; sortKey: string; projectCount: number; childFolderCount: number; } export interface ExplorerProject { id: string; name: string; folderId: string | null; createdAt: string; binding: { chatId: string; createdAt: string } | null; } export interface ExplorerData { folders: ExplorerFolder[]; projects: ExplorerProject[]; } export interface ProjectDetail { id: string; name: string; folderId: string | null; folder: { id: string; name: string } | null; workspaceDir: string; createdAt: string; archivedAt: string | null; createdBy: { id: string; displayName: string; feishuOpenId: string } | null; binding: { chatId: string; createdAt: string } | null; actorIsOrgAdmin?: boolean; actorCanManageProject?: boolean; } export interface TeamAccessEntry { grantId: string; projectId: string; organizationId: string; teamId: string; teamSlug: string; teamName: string; role: 'READ' | 'EDIT' | 'MANAGE'; } export interface SessionSummary { id: string; provider: string; roleId: string; model: string; title: string | null; runCount: number; createdAt: string; updatedAt: string; } export interface ProviderConnectionRow { id: string; providerId: string; mode: 'BYOK' | 'PLATFORM_MANAGED'; status: 'DRAFT' | 'ACTIVE' | 'DISABLED'; activeVersion: number | null; keyId: string | null; createdAt: string; updatedAt: string; } export interface FeishuApplicationConnection { id: string; appFingerprint: string; status: 'DRAFT' | 'ACTIVE' | 'DISABLED'; activeVersion: number | null; keyId: string | null; createdAt: string; updatedAt: string; } export interface UsageTotals { runCount: number; runsWithCost: number; runsWithoutCost: number; inputTokens: number; outputTokens: number; costUsd: number | null; } export interface ProjectUsageRow extends UsageTotals { projectId: string; projectName: string; folderId: string | null; } export interface UsageReport { from: string | null; to: string | null; projects: ProjectUsageRow[]; totals: UsageTotals; } export type CapacityDimension = | 'requestRate' | 'requestBodySize' | 'agentConcurrency' | 'admissionQueueLength' | 'admissionQueueWait' | 'fileSize' | 'attachmentCount' | 'archiveExpansion' | 'projectStorage' | 'organizationStorage' | 'memberCount' | 'projectCount' | 'teamCount' | 'folderCount' | 'sessionCount' | 'runWallTime' | 'runTurns' | 'runToolCalls' | 'toolWallTime' | 'runOutputSize' | 'processMemory' | 'processCpu' | 'processCount'; export interface CapacityDimensionRow { dimension: CapacityDimension; platformCeiling: number | null; organizationLimit: number | null; effective: number | null; } export interface CapacityPolicyView { dimensions: CapacityDimensionRow[]; } export interface AgentRoleRow { id: string; roleId: string; label: string; defaultModel: string | null; systemPrompt: string | null; tools: readonly string[] | null; sortOrder: number; isDefault: boolean; disabledAt: string | null; createdAt: string; updatedAt: string; skillNames: readonly string[]; } export interface AgentSkillRow { id: string; name: string; version: string; description: string | null; contentDigest: string; disabledAt: string | null; createdAt: string; updatedAt: string; boundRoleIds: readonly string[]; } export interface SkillFileEntry { path: string; content: string; } export interface InstalledSkillResult { id: string; name: string; contentDigest: string; } export interface AgentModelRow { id: string; label: string; toolCapable: boolean; } // --- API --- export const api = { me: () => get('/api/me') as Promise, logout: () => post('/auth/logout'), org: (slug: string) => get(orgBase(slug)) as Promise<{ organization: { id: string; slug: string; name: string; status: string }; actorRole: string; }>, settings: (slug: string) => get(`${orgBase(slug)}/settings`) as Promise<{ membersCanCreateProjects: boolean }>, setSettings: (slug: string, body: { membersCanCreateProjects: boolean }) => patch(`${orgBase(slug)}/settings`, body) as Promise<{ membersCanCreateProjects: boolean }>, members: (slug: string) => get(`${orgBase(slug)}/members`) as Promise<{ members: OrgMember[] }>, addMember: (slug: string, body: { feishuOpenId: string; displayName?: string; role: string }) => post(`${orgBase(slug)}/members`, body) as Promise, setMemberRole: (slug: string, userId: string, role: string) => patch(`${orgBase(slug)}/members/${userId}`, { role }), revokeMember: (slug: string, userId: string) => post(`${orgBase(slug)}/members/${userId}/revoke`), teams: (slug: string) => get(`${orgBase(slug)}/teams`) as Promise<{ teams: TeamRow[] }>, createTeam: (slug: string, body: { slug: string; name: string; description?: string }) => post(`${orgBase(slug)}/teams`, body) as Promise, updateTeam: (slug: string, teamId: string, body: { name?: string; description?: string | null }) => patch(`${orgBase(slug)}/teams/${teamId}`, body) as Promise, archiveTeam: (slug: string, teamId: string) => post(`${orgBase(slug)}/teams/${teamId}/archive`), teamMembers: (slug: string, teamId: string) => get(`${orgBase(slug)}/teams/${teamId}/members`) as Promise<{ members: TeamMemberRow[] }>, addTeamMember: (slug: string, teamId: string, body: { userId?: string; feishuOpenId?: string }) => post(`${orgBase(slug)}/teams/${teamId}/members`, body) as Promise, revokeTeamMember: (slug: string, teamId: string, userId: string) => post(`${orgBase(slug)}/teams/${teamId}/members/${userId}/revoke`), explorer: (slug: string) => get(`${orgBase(slug)}/explorer`) as Promise, myProjects: (slug: string) => get(`${orgBase(slug)}/my-projects`) as Promise<{ projects: ExplorerProject[] }>, createFolder: (slug: string, body: { name: string; parentId?: string; sortKey?: string }) => post(`${orgBase(slug)}/folders`, body) as Promise<{ id: string; name: string; parentId: string | null; sortKey: string; }>, renameFolder: (slug: string, folderId: string, body: { name?: string; sortKey?: string; parentId?: string | null }) => patch(`${orgBase(slug)}/folders/${folderId}`, body) as Promise<{ id: string; name: string; parentId: string | null; sortKey: string; }>, archiveFolder: (slug: string, folderId: string) => post(`${orgBase(slug)}/folders/${folderId}/archive`) as Promise<{ archived: true; folderId: string }>, createProject: (slug: string, body: { name: string; folderId?: string }) => post(`${orgBase(slug)}/projects`, body) as Promise<{ id: string; name: string }>, project: (slug: string, projectId: string) => get(`${orgBase(slug)}/projects/${projectId}`) as Promise, renameProject: (slug: string, projectId: string, name: string) => patch(`${orgBase(slug)}/projects/${projectId}`, { name }), moveProject: (slug: string, projectId: string, folderId: string | null) => patch(`${orgBase(slug)}/projects/${projectId}/folder`, { folderId }), archiveProject: (slug: string, projectId: string) => post(`${orgBase(slug)}/projects/${projectId}/archive`), archiveBinding: (slug: string, projectId: string) => post(`${orgBase(slug)}/projects/${projectId}/binding/archive`), teamAccess: (slug: string, projectId: string) => get(`${orgBase(slug)}/projects/${projectId}/team-access`) as Promise<{ access: TeamAccessEntry[] }>, grantTeamAccess: (slug: string, projectId: string, body: { teamId?: string; teamSlug?: string; role: string }) => put(`${orgBase(slug)}/projects/${projectId}/team-access`, body) as Promise, revokeTeamAccess: (slug: string, projectId: string, teamId: string) => del(`${orgBase(slug)}/projects/${projectId}/team-access/${teamId}`), sessions: (slug: string, projectId: string, limit?: number) => get(`${orgBase(slug)}/projects/${projectId}/sessions${limit !== undefined ? `?limit=${limit}` : ''}`) as Promise<{ sessions: SessionSummary[]; }>, usage: (slug: string, params?: { from?: string; to?: string; folderId?: string }) => { const q = new URLSearchParams(); if (params?.from) q.set('from', params.from); if (params?.to) q.set('to', params.to); if (params?.folderId) q.set('folderId', params.folderId); const qs = q.toString(); return get(`${orgBase(slug)}/usage${qs ? `?${qs}` : ''}`) as Promise; }, providerConnections: (slug: string) => get(`${orgBase(slug)}/provider-connections`) as Promise<{ connections: ProviderConnectionRow[] }>, rotateProviderConnection: ( slug: string, providerId: string, body: { baseUrl: string; authToken: string; anthropicApiKey?: string }, ) => put( `${orgBase(slug)}/provider-connections/${encodeURIComponent(providerId)}`, body, ) as Promise, feishuApplication: (slug: string) => get(`${orgBase(slug)}/feishu-application-connection`) as Promise<{ connection: FeishuApplicationConnection | null; }>, rotateFeishuApplication: ( slug: string, body: { appId: string; appSecret: string; botOpenId: string; verificationToken?: string; encryptKey?: string; }, ) => put(`${orgBase(slug)}/feishu-application-connection`, body) as Promise, disableFeishuApplication: (slug: string) => del(`${orgBase(slug)}/feishu-application-connection`) as Promise, capacityPolicy: (slug: string) => get(`${orgBase(slug)}/capacity-policy`) as Promise, setCapacityPolicy: (slug: string, body: { limits: Partial> }) => put(`${orgBase(slug)}/capacity-policy`, body) as Promise, agentRoles: (slug: string) => get(`${orgBase(slug)}/agent-roles`) as Promise<{ roles: AgentRoleRow[] }>, upsertAgentRole: ( slug: string, roleId: string, body: { label: string; defaultModel?: string | null; systemPrompt?: string | null; tools?: readonly string[] | null; sortOrder?: number; isDefault?: boolean; }, ) => put(`${orgBase(slug)}/agent-roles/${encodeURIComponent(roleId)}`, body) as Promise, setAgentRoleSkills: (slug: string, roleId: string, skillNames: readonly string[]) => put(`${orgBase(slug)}/agent-roles/${encodeURIComponent(roleId)}/skills`, { skillNames }) as Promise<{ skillNames: string[]; }>, agentSkills: (slug: string) => get(`${orgBase(slug)}/agent-skills`) as Promise<{ skills: AgentSkillRow[] }>, agentSkillFiles: (slug: string, name: string) => get(`${orgBase(slug)}/agent-skills/${encodeURIComponent(name)}/files`) as Promise<{ files: SkillFileEntry[] }>, installAgentSkill: (slug: string, name: string, body: { version: string; files: readonly SkillFileEntry[] }) => put(`${orgBase(slug)}/agent-skills/${encodeURIComponent(name)}`, body) as Promise, patchAgentSkill: (slug: string, name: string, body: { description?: string; disabled?: boolean }) => patch(`${orgBase(slug)}/agent-skills/${encodeURIComponent(name)}`, body) as Promise<{ disabled?: boolean; updated?: boolean }>, agentModels: (slug: string) => get(`${orgBase(slug)}/agent-models`) as Promise<{ models: AgentModelRow[] }>, };