multica/apps/web/shared/api/client.ts
Naiyuan Qing f70b34a50f fix: resolve merge conflicts with main, preserve PAT functionality
- Resolve conflicts in CLAUDE.md, client.ts, settings/page.tsx
- Migrate PAT types and API methods to @/shared/types + @/shared/api architecture
- Restore simplified login flow (login page, auth store, tests)
- Fix issue detail comment submit test (use fireEvent + useRef for mock)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:19:24 +08:00

394 lines
11 KiB
TypeScript

import type {
Issue,
CreateIssueRequest,
UpdateIssueRequest,
ListIssuesResponse,
UpdateMeRequest,
CreateMemberRequest,
UpdateMemberRequest,
ListIssuesParams,
Agent,
CreateAgentRequest,
UpdateAgentRequest,
AgentTask,
AgentRuntime,
DaemonPairingSession,
ApproveDaemonPairingSessionRequest,
InboxItem,
Comment,
Workspace,
MemberWithUser,
User,
Skill,
CreateSkillRequest,
UpdateSkillRequest,
SetAgentSkillsRequest,
PersonalAccessToken,
CreatePersonalAccessTokenRequest,
CreatePersonalAccessTokenResponse,
} from "@/shared/types";
import { type Logger, noopLogger } from "@/shared/logger";
export interface LoginResponse {
token: string;
user: User;
}
export class ApiClient {
private baseUrl: string;
private token: string | null = null;
private workspaceId: string | null = null;
private logger: Logger;
constructor(baseUrl: string, options?: { logger?: Logger }) {
this.baseUrl = baseUrl;
this.logger = options?.logger ?? noopLogger;
}
setToken(token: string | null) {
this.token = token;
}
setWorkspaceId(id: string | null) {
this.workspaceId = id;
}
private async fetch<T>(path: string, init?: RequestInit): Promise<T> {
const rid = crypto.randomUUID().slice(0, 8);
const start = Date.now();
const method = init?.method ?? "GET";
const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-Request-ID": rid,
...((init?.headers as Record<string, string>) ?? {}),
};
if (this.token) {
headers["Authorization"] = `Bearer ${this.token}`;
}
if (this.workspaceId) {
headers["X-Workspace-ID"] = this.workspaceId;
}
this.logger.info(`${method} ${path}`, { rid });
const res = await fetch(`${this.baseUrl}${path}`, {
...init,
headers,
});
if (!res.ok) {
if (res.status === 401 && typeof window !== "undefined") {
localStorage.removeItem("multica_token");
localStorage.removeItem("multica_workspace_id");
this.token = null;
this.workspaceId = null;
if (window.location.pathname !== "/login") {
window.location.href = "/login";
}
}
let message = `API error: ${res.status} ${res.statusText}`;
try {
const data = await res.json() as { error?: string };
if (typeof data.error === "string" && data.error) {
message = data.error;
}
} catch {
// Ignore non-JSON error bodies.
}
this.logger.error(`${res.status} ${path}`, { rid, duration: `${Date.now() - start}ms`, error: message });
throw new Error(message);
}
this.logger.info(`${res.status} ${path}`, { rid, duration: `${Date.now() - start}ms` });
// Handle 204 No Content
if (res.status === 204) {
return undefined as T;
}
return res.json() as Promise<T>;
}
// Auth
async login(email: string, name?: string): Promise<LoginResponse> {
return this.fetch("/auth/login", {
method: "POST",
body: JSON.stringify({ email, name }),
});
}
async getMe(): Promise<User> {
return this.fetch("/api/me");
}
async updateMe(data: UpdateMeRequest): Promise<User> {
return this.fetch("/api/me", {
method: "PATCH",
body: JSON.stringify(data),
});
}
// Issues
async listIssues(params?: ListIssuesParams): Promise<ListIssuesResponse> {
const search = new URLSearchParams();
if (params?.limit) search.set("limit", String(params.limit));
if (params?.offset) search.set("offset", String(params.offset));
const wsId = params?.workspace_id ?? this.workspaceId;
if (wsId) search.set("workspace_id", wsId);
if (params?.status) search.set("status", params.status);
if (params?.priority) search.set("priority", params.priority);
if (params?.assignee_id) search.set("assignee_id", params.assignee_id);
return this.fetch(`/api/issues?${search}`);
}
async getIssue(id: string): Promise<Issue> {
return this.fetch(`/api/issues/${id}`);
}
async createIssue(data: CreateIssueRequest): Promise<Issue> {
const search = new URLSearchParams();
if (this.workspaceId) search.set("workspace_id", this.workspaceId);
return this.fetch(`/api/issues?${search}`, {
method: "POST",
body: JSON.stringify(data),
});
}
async updateIssue(id: string, data: UpdateIssueRequest): Promise<Issue> {
return this.fetch(`/api/issues/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
async deleteIssue(id: string): Promise<void> {
await this.fetch(`/api/issues/${id}`, { method: "DELETE" });
}
// Comments
async listComments(issueId: string): Promise<Comment[]> {
return this.fetch(`/api/issues/${issueId}/comments`);
}
async createComment(issueId: string, content: string, type?: string): Promise<Comment> {
return this.fetch(`/api/issues/${issueId}/comments`, {
method: "POST",
body: JSON.stringify({ content, type: type ?? "comment" }),
});
}
async updateComment(commentId: string, content: string): Promise<Comment> {
return this.fetch(`/api/comments/${commentId}`, {
method: "PUT",
body: JSON.stringify({ content }),
});
}
async deleteComment(commentId: string): Promise<void> {
await this.fetch(`/api/comments/${commentId}`, { method: "DELETE" });
}
// Agents
async listAgents(params?: { workspace_id?: string }): Promise<Agent[]> {
const search = new URLSearchParams();
const wsId = params?.workspace_id ?? this.workspaceId;
if (wsId) search.set("workspace_id", wsId);
return this.fetch(`/api/agents?${search}`);
}
async getAgent(id: string): Promise<Agent> {
return this.fetch(`/api/agents/${id}`);
}
async createAgent(data: CreateAgentRequest): Promise<Agent> {
return this.fetch("/api/agents", {
method: "POST",
body: JSON.stringify(data),
});
}
async updateAgent(id: string, data: UpdateAgentRequest): Promise<Agent> {
return this.fetch(`/api/agents/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
async deleteAgent(id: string): Promise<void> {
await this.fetch(`/api/agents/${id}`, { method: "DELETE" });
}
async listRuntimes(params?: { workspace_id?: string }): Promise<AgentRuntime[]> {
const search = new URLSearchParams();
const wsId = params?.workspace_id ?? this.workspaceId;
if (wsId) search.set("workspace_id", wsId);
return this.fetch(`/api/runtimes?${search}`);
}
async listAgentTasks(agentId: string): Promise<AgentTask[]> {
return this.fetch(`/api/agents/${agentId}/tasks`);
}
async getDaemonPairingSession(token: string): Promise<DaemonPairingSession> {
return this.fetch(`/api/daemon/pairing-sessions/${token}`);
}
async approveDaemonPairingSession(
token: string,
data: ApproveDaemonPairingSessionRequest,
): Promise<DaemonPairingSession> {
return this.fetch(`/api/daemon/pairing-sessions/${token}/approve`, {
method: "POST",
body: JSON.stringify(data),
});
}
// Inbox
async listInbox(): Promise<InboxItem[]> {
return this.fetch("/api/inbox");
}
async markInboxRead(id: string): Promise<InboxItem> {
return this.fetch(`/api/inbox/${id}/read`, { method: "POST" });
}
async archiveInbox(id: string): Promise<InboxItem> {
return this.fetch(`/api/inbox/${id}/archive`, { method: "POST" });
}
async getUnreadInboxCount(): Promise<{ count: number }> {
return this.fetch("/api/inbox/unread-count");
}
async markAllInboxRead(): Promise<{ count: number }> {
return this.fetch("/api/inbox/mark-all-read", { method: "POST" });
}
async archiveAllInbox(): Promise<{ count: number }> {
return this.fetch("/api/inbox/archive-all", { method: "POST" });
}
async archiveAllReadInbox(): Promise<{ count: number }> {
return this.fetch("/api/inbox/archive-all-read", { method: "POST" });
}
async archiveCompletedInbox(): Promise<{ count: number }> {
return this.fetch("/api/inbox/archive-completed", { method: "POST" });
}
// Workspaces
async listWorkspaces(): Promise<Workspace[]> {
return this.fetch("/api/workspaces");
}
async getWorkspace(id: string): Promise<Workspace> {
return this.fetch(`/api/workspaces/${id}`);
}
async createWorkspace(data: { name: string; slug: string; description?: string; context?: string }): Promise<Workspace> {
return this.fetch("/api/workspaces", {
method: "POST",
body: JSON.stringify(data),
});
}
async updateWorkspace(id: string, data: { name?: string; description?: string; context?: string; settings?: Record<string, unknown> }): Promise<Workspace> {
return this.fetch(`/api/workspaces/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
// Members
async listMembers(workspaceId: string): Promise<MemberWithUser[]> {
return this.fetch(`/api/workspaces/${workspaceId}/members`);
}
async createMember(workspaceId: string, data: CreateMemberRequest): Promise<MemberWithUser> {
return this.fetch(`/api/workspaces/${workspaceId}/members`, {
method: "POST",
body: JSON.stringify(data),
});
}
async updateMember(workspaceId: string, memberId: string, data: UpdateMemberRequest): Promise<MemberWithUser> {
return this.fetch(`/api/workspaces/${workspaceId}/members/${memberId}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
async deleteMember(workspaceId: string, memberId: string): Promise<void> {
await this.fetch(`/api/workspaces/${workspaceId}/members/${memberId}`, {
method: "DELETE",
});
}
async leaveWorkspace(workspaceId: string): Promise<void> {
await this.fetch(`/api/workspaces/${workspaceId}/leave`, {
method: "POST",
});
}
async deleteWorkspace(workspaceId: string): Promise<void> {
await this.fetch(`/api/workspaces/${workspaceId}`, {
method: "DELETE",
});
}
// Skills
async listSkills(): Promise<Skill[]> {
return this.fetch("/api/skills");
}
async getSkill(id: string): Promise<Skill> {
return this.fetch(`/api/skills/${id}`);
}
async createSkill(data: CreateSkillRequest): Promise<Skill> {
return this.fetch("/api/skills", {
method: "POST",
body: JSON.stringify(data),
});
}
async updateSkill(id: string, data: UpdateSkillRequest): Promise<Skill> {
return this.fetch(`/api/skills/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
async deleteSkill(id: string): Promise<void> {
await this.fetch(`/api/skills/${id}`, { method: "DELETE" });
}
async listAgentSkills(agentId: string): Promise<Skill[]> {
return this.fetch(`/api/agents/${agentId}/skills`);
}
async setAgentSkills(agentId: string, data: SetAgentSkillsRequest): Promise<void> {
await this.fetch(`/api/agents/${agentId}/skills`, {
method: "PUT",
body: JSON.stringify(data),
});
}
// Personal Access Tokens
async listPersonalAccessTokens(): Promise<PersonalAccessToken[]> {
return this.fetch("/api/tokens");
}
async createPersonalAccessToken(data: CreatePersonalAccessTokenRequest): Promise<CreatePersonalAccessTokenResponse> {
return this.fetch("/api/tokens", {
method: "POST",
body: JSON.stringify(data),
});
}
async revokePersonalAccessToken(id: string): Promise<void> {
await this.fetch(`/api/tokens/${id}`, { method: "DELETE" });
}
}