import { ApiClientError, createApiClient } from "@geo/http-client"; import type { ApiEnvelope, ArticleDetail, ArticleImageUploadResponse, ArticleListParams, ArticleListResponse, CreateArticleRequest, ArticleVersion, AuthTokens, Brand, BrandLibrarySummary, BrandRequest, Competitor, CompetitorRequest, CreatePluginSessionRequest, CreatePluginSessionResponse, CreatePublishJobRequest, CreatePublishJobResponse, CreatePublishBatchRequest, CreatePublishBatchResponse, GenerateFromRuleRequest, GenerateFromRuleResponse, InstantTaskListParams, InstantTaskListResponse, JsonValue, KnowledgeGroup, KnowledgeGroupRequest, KnowledgeItem, KnowledgeItemDetail, KnowledgeTextItemRequest, KnowledgeURLItemRequest, KolAssistRequest, KolAssistStreamEvent, KolAssistSubmitResponse, KolAssistTask, KolPackageSummary, KolPackageDetail, PublishKolPromptRequest, KolSubscription, KolSubscriptionPromptCard, KolProfile, KolPromptDetail, KolSubscriptionPromptSchema, KolGenerateRequest, KolGenerateResponse, KolDashboardOverview, KolDashboardPackageStat, KolDashboardTrendPoint, Keyword, KeywordRequest, KolWorkspaceCard, LoginRequest, LoginResponse, MediaPlatform, MonitoringCollectNowResponse, MonitoringDashboardCompositeResponse, MonitoringQuestionDetailResponse, PlatformAccount, PublishRecord, RegisterPluginInstallationRequest, RegisterPluginInstallationResponse, PromptRule, PromptRuleGroup, PromptRuleGroupRequest, PromptRuleListParams, PromptRuleListResponse, PromptRuleRequest, PromptRuleSimple, PromptRuleStatusRequest, QuotaSummary, Question, QuestionRequest, RecentArticle, RefreshResponse, ScheduleTask, ScheduleTaskListParams, ScheduleTaskListResponse, ScheduleTaskRequest, ScheduleTaskStatusRequest, TemplateDetail, TemplateAnalyzeTaskRequest, TemplateAnalyzeTaskResultResponse, TemplateAssistTaskCreateResponse, TemplateOutlineTaskRequest, TemplateOutlineTaskResultResponse, TemplateSaveDraftRequest, TemplateSaveDraftResponse, TemplateTitleTaskRequest, TemplateTitleTaskResultResponse, TemplateGenerateRequest, TemplateGenerateResponse, TemplateListItem, TemplateCard, ImageFolder, ImageAssetItem, ImageAssetListResponse, ImageUploadResponse, ImageReferenceDetail, FolderDeletePreview, ImageStorageUsage, ImageUpdateRequest, UpdateQuestionRequest, UpdateArticleRequest, CreateKolPackageRequest, CreateKolPromptRequest, UpdateKolPackageRequest, UpdateKolPromptRequest, DesktopAccountInfo, UserInfo, WorkspaceOverview, } from "@geo/shared-types"; import { clearStoredSession, getStoredAccessToken, getStoredRefreshToken, setStoredTokens, } from "./session"; import { convertImageFileToWebp, markImageUploadRequestFailed, markImageUploadRequestFinished, markImageUploadRequestStarted, } from "./image-webp"; const baseURL = import.meta.env.VITE_API_BASE_URL ?? ""; const generationStreamEnabled = import.meta.env.VITE_GENERATION_STREAM_ENABLED === "true"; const publicClient = createApiClient({ baseURL }); export function getApiBaseURL(): string { if (baseURL) { return baseURL; } if (typeof window !== "undefined") { return window.location.origin; } return ""; } export function resolveApiURL(value?: string | null): string { const trimmed = String(value ?? "").trim(); if (!trimmed) { return ""; } if (/^https?:\/\//i.test(trimmed)) { return trimmed; } if (/^\/\//.test(trimmed)) { return `https:${trimmed}`; } const apiBaseURL = getApiBaseURL(); if (!apiBaseURL) { return trimmed; } return new URL(trimmed, apiBaseURL.endsWith("/") ? apiBaseURL : `${apiBaseURL}/`).toString(); } export const apiClient = createApiClient({ baseURL, auth: { getAccessToken: getStoredAccessToken, getRefreshToken: getStoredRefreshToken, setTokens: setStoredTokens, clearTokens: clearStoredSession, async refresh(refreshToken: string): Promise { const data = await publicClient.post( "/api/auth/refresh", { refresh_token: refreshToken }, ); return { accessToken: data.access_token, refreshToken: data.refresh_token, }; }, }, }); export const authApi = { login(payload: LoginRequest) { return publicClient.post("/api/auth/login", payload); }, me() { return apiClient.get("/api/auth/me"); }, logout() { return apiClient.post("/api/auth/logout"); }, }; export const workspaceApi = { overview() { return apiClient.get("/api/tenant/workspace/overview"); }, recentArticles() { return apiClient.get("/api/tenant/workspace/recent-articles"); }, quotaSummary() { return apiClient.get("/api/tenant/workspace/quota-summary"); }, templateCards() { return apiClient.get("/api/tenant/workspace/template-cards"); }, kolCards() { return apiClient.get("/api/tenant/workspace/kol-cards"); }, }; export const templatesApi = { list() { return apiClient.get("/api/tenant/templates"); }, detail(id: number) { return apiClient.get(`/api/tenant/templates/${id}`); }, createAnalyzeTask(id: number, payload: TemplateAnalyzeTaskRequest) { return apiClient.post( `/api/tenant/templates/${id}/analyze-tasks`, payload, ); }, getAnalyzeTaskResult(id: number, taskId: string) { return apiClient.get( `/api/tenant/templates/${id}/analyze_task_result`, { params: { task_id: taskId } }, ); }, createTitleTask(id: number, payload: TemplateTitleTaskRequest) { return apiClient.post( `/api/tenant/templates/${id}/title-tasks`, payload, ); }, getTitleTaskResult(id: number, taskId: string) { return apiClient.get( `/api/tenant/templates/${id}/gen_title_task_result`, { params: { task_id: taskId } }, ); }, createOutlineTask(id: number, payload: TemplateOutlineTaskRequest) { return apiClient.post( `/api/tenant/templates/${id}/outline-tasks`, payload, ); }, getOutlineTaskResult(id: number, taskId: string) { return apiClient.get( `/api/tenant/templates/${id}/gen_outline_task_result`, { params: { task_id: taskId } }, ); }, createAssistTask(id: number, payload: TemplateAnalyzeTaskRequest) { return this.createAnalyzeTask(id, payload); }, getAssistTask(id: number, taskId: string) { return this.getAnalyzeTaskResult(id, taskId); }, generate(id: number, payload: TemplateGenerateRequest) { return apiClient.post( `/api/tenant/templates/${id}/generate`, payload, ); }, saveDraft(id: number, payload: TemplateSaveDraftRequest) { return apiClient.post( `/api/tenant/templates/${id}/drafts`, payload, ); }, }; export const kolManageApi = { profile() { return apiClient.get("/api/tenant/kol/manage/profile"); }, updateProfile(body: { display_name: string; bio?: string | null; avatar_url?: string | null; }) { return apiClient.put( "/api/tenant/kol/manage/profile", body, ); }, async uploadAvatar(file: File) { const normalizedFile = await convertImageFileToWebp(file); const progressToken = markImageUploadRequestStarted(normalizedFile.name); try { const formData = new FormData(); formData.append("file", normalizedFile, normalizedFile.name); const response = await apiClient.raw.post>("/api/tenant/kol/manage/profile/avatar", formData); markImageUploadRequestFinished(progressToken); return response.data.data; } catch (error) { markImageUploadRequestFailed(progressToken); throw error; } }, listPackages() { return apiClient.get("/api/tenant/kol/manage/packages"); }, createPackage(body: CreateKolPackageRequest) { return apiClient.post( "/api/tenant/kol/manage/packages", body, ); }, updatePackage(id: number, body: UpdateKolPackageRequest) { return apiClient.put( `/api/tenant/kol/manage/packages/${id}`, body, ); }, publishPackage(id: number) { return apiClient.put>( `/api/tenant/kol/manage/packages/${id}/publish`, {}, ); }, archivePackage(id: number) { return apiClient.put>( `/api/tenant/kol/manage/packages/${id}/archive`, {}, ); }, deletePackage(id: number) { return apiClient.remove(`/api/tenant/kol/manage/packages/${id}`); }, listPrompts(packageId: number) { return apiClient.get(`/api/tenant/kol/manage/packages/${packageId}/prompts`); }, createPrompt(packageId: number, body: CreateKolPromptRequest) { return apiClient.post( `/api/tenant/kol/manage/packages/${packageId}/prompts`, body, ); }, getPrompt(id: number) { return apiClient.get(`/api/tenant/kol/manage/prompts/${id}`); }, updatePromptMetadata(id: number, body: UpdateKolPromptRequest) { return apiClient.put( `/api/tenant/kol/manage/prompts/${id}`, body, ); }, deletePrompt(id: number) { return apiClient.remove(`/api/tenant/kol/manage/prompts/${id}`); }, savePrompt(promptId: number, body: PublishKolPromptRequest) { return apiClient.post( `/api/tenant/kol/manage/prompts/${promptId}/save`, body, ); }, publishPrompt(promptId: number, body: PublishKolPromptRequest) { return apiClient.post( `/api/tenant/kol/manage/prompts/${promptId}/publish`, body, ); }, activatePrompt(promptId: number) { return apiClient.put>( `/api/tenant/kol/manage/prompts/${promptId}/activate`, {}, ); }, archivePrompt(promptId: number) { return apiClient.put>( `/api/tenant/kol/manage/prompts/${promptId}/archive`, {}, ); }, submitAssist(body: KolAssistRequest) { return apiClient.post( "/api/tenant/kol/manage/assist", body, ); }, streamAssist( body: KolAssistRequest, handlers: { onEvent: (event: KolAssistStreamEvent) => void; onClose?: () => void; }, signal: AbortSignal, ) { return subscribeSSE( "/api/tenant/kol/manage/assist/stream", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }, handlers, signal, ); }, getAssist(id: string) { return apiClient.get(`/api/tenant/kol/manage/assist/${id}`); }, }; export const kolMarketplaceApi = { list(params: { industry?: string; keyword?: string; limit?: number; offset?: number }) { return apiClient.get("/api/tenant/kol/marketplace/packages", { params }); }, detail(id: number) { return apiClient.get(`/api/tenant/kol/marketplace/packages/${id}`); }, subscribe(id: number) { return apiClient.post>( `/api/tenant/kol/marketplace/packages/${id}/subscribe`, {}, ); }, mySubscriptions() { return apiClient.get("/api/tenant/kol/subscriptions"); }, mySubscriptionPrompts() { return apiClient.get("/api/tenant/kol/subscription-prompts"); }, }; export const kolGenerateApi = { schema(id: number | string) { return apiClient.get(`/api/tenant/kol/subscription-prompts/${id}/schema`); }, submit(id: number | string, body: KolGenerateRequest) { return apiClient.post( `/api/tenant/kol/subscription-prompts/${id}/generate`, body, ); }, }; export const kolDashboardApi = { overview() { return apiClient.get("/api/tenant/kol/dashboard/overview"); }, packages() { return apiClient.get("/api/tenant/kol/dashboard/packages"); }, trend(periodDays: number) { return apiClient.get("/api/tenant/kol/dashboard/trend", { params: { period_days: periodDays }, }); }, }; export const articlesApi = { list(params: ArticleListParams) { return apiClient.get("/api/tenant/articles", { params }); }, create(payload: CreateArticleRequest = {}) { return apiClient.post("/api/tenant/articles", payload); }, detail(id: number) { return apiClient.get(`/api/tenant/articles/${id}`); }, update(id: number, payload: UpdateArticleRequest) { return apiClient.put(`/api/tenant/articles/${id}`, payload); }, async uploadImage(id: number, file: File) { const normalizedFile = await convertImageFileToWebp(file); const progressToken = markImageUploadRequestStarted(normalizedFile.name); try { const formData = new FormData(); formData.append("file", normalizedFile, normalizedFile.name); const response = await apiClient.raw.post>( `/api/tenant/articles/${id}/images`, formData, ); markImageUploadRequestFinished(progressToken); return { ...response.data.data, url: resolveApiURL(response.data.data.url), }; } catch (error) { markImageUploadRequestFailed(progressToken); throw error; } }, versions(id: number) { return apiClient.get(`/api/tenant/articles/${id}/versions`); }, publishBatch(id: number, payload: CreatePublishBatchRequest) { return apiClient.post( `/api/tenant/articles/${id}/publish-batches`, payload, ); }, publishRecords(id: number) { return apiClient.get(`/api/tenant/articles/${id}/publish-records`); }, remove(id: number) { return apiClient.remove(`/api/tenant/articles/${id}`); }, }; export const knowledgeApi = { listGroups() { return apiClient.get("/api/tenant/knowledge/groups"); }, createGroup(payload: KnowledgeGroupRequest) { return apiClient.post("/api/tenant/knowledge/groups", payload); }, updateGroup(id: number, payload: KnowledgeGroupRequest) { return apiClient.put(`/api/tenant/knowledge/groups/${id}`, payload); }, removeGroup(id: number) { return apiClient.remove(`/api/tenant/knowledge/groups/${id}`); }, listItems(groupId?: number) { return apiClient.get("/api/tenant/knowledge/items", { params: groupId ? { group_id: groupId } : undefined, }); }, detail(id: number) { return apiClient.get(`/api/tenant/knowledge/items/${id}`); }, createTextItem(payload: KnowledgeTextItemRequest) { return apiClient.post( "/api/tenant/knowledge/items/text", payload, ); }, createURLItem(payload: KnowledgeURLItemRequest) { return apiClient.post( "/api/tenant/knowledge/items/url", payload, ); }, async createFileItem(payload: { group_id: number; name?: string; file: File }) { const form = new FormData(); form.set("group_id", String(payload.group_id)); if (payload.name?.trim()) { form.set("name", payload.name.trim()); } form.set("file", payload.file); const { data } = await apiClient.raw.post("/api/tenant/knowledge/items/file", form); return data.data as KnowledgeItem; }, removeItem(id: number) { return apiClient.remove(`/api/tenant/knowledge/items/${id}`); }, }; export const imagesApi = { listFolders() { return apiClient.get("/api/tenant/images/folders"); }, createFolder(name: string) { return apiClient.post("/api/tenant/images/folders", { name }); }, updateFolder(id: number, name: string) { return apiClient.put(`/api/tenant/images/folders/${id}`, { name }); }, folderDeletePreview(id: number) { return apiClient.get(`/api/tenant/images/folders/${id}/delete-preview`); }, deleteFolder(id: number, force?: boolean) { return apiClient.remove(`/api/tenant/images/folders/${id}`, { params: force ? { force: 1 } : undefined }); }, list(params?: { folder_id?: number | null; q?: string; page?: number; page_size?: number }) { return apiClient.get("/api/tenant/images", { params }); }, async upload(file: File, folderId?: number | null) { const normalizedFile = await convertImageFileToWebp(file); const progressToken = markImageUploadRequestStarted(normalizedFile.name); try { const formData = new FormData(); formData.append("file", normalizedFile, normalizedFile.name); if (folderId != null) { formData.append("folder_id", String(folderId)); } const response = await apiClient.raw.post>( "/api/tenant/images", formData, ); markImageUploadRequestFinished(progressToken); return { ...response.data.data, url: resolveApiURL(response.data.data.url), }; } catch (error) { markImageUploadRequestFailed(progressToken); throw error; } }, update(id: number, payload: ImageUpdateRequest) { return apiClient.put(`/api/tenant/images/${id}`, payload); }, getReferences(id: number) { return apiClient.get(`/api/tenant/images/${id}/references`); }, delete(id: number, force?: boolean) { return apiClient.remove(`/api/tenant/images/${id}`, { params: force ? { force: 1 } : undefined }); }, storageUsage() { return apiClient.get("/api/tenant/images/storage-usage"); }, }; export interface ArticleGenerationStreamEvent { type: string; article_id: number; task_id?: number; title?: string; status?: string; delta?: string; content?: string; error?: string; done?: boolean; updated_at?: string; } export interface ArticleSelectionOptimizeRequest { title: string; markdown_content: string; selected_text: string; instruction: string; } export interface ArticleSelectionOptimizeStreamEvent { type: string; article_id: number; status?: string; delta?: string; content?: string; error?: string; model?: string; done?: boolean; } export function isGenerationStreamEnabled(): boolean { return generationStreamEnabled; } export async function subscribeArticleGeneration( articleId: number, handlers: { onEvent: (event: ArticleGenerationStreamEvent) => void; onClose?: () => void; }, signal: AbortSignal, ): Promise { return subscribeSSE( `/api/tenant/articles/${articleId}/stream`, { method: "GET", }, handlers, signal, ); } export async function streamArticleSelectionOptimize( articleId: number, payload: ArticleSelectionOptimizeRequest, handlers: { onEvent: (event: ArticleSelectionOptimizeStreamEvent) => void; onClose?: () => void; }, signal: AbortSignal, ): Promise { return subscribeSSE( `/api/tenant/articles/${articleId}/selection-optimize/stream`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }, handlers, signal, ); } type SSEEventShape = { type: string; }; async function subscribeSSE( path: string, init: RequestInit, handlers: { onEvent: (event: TEvent) => void; onClose?: () => void; }, signal: AbortSignal, ): Promise { const accessToken = getStoredAccessToken(); if (!accessToken) { throw new Error("missing access token"); } const requestHeaders = new Headers(init.headers); requestHeaders.set("Accept", "text/event-stream"); requestHeaders.set("Authorization", `Bearer ${accessToken}`); const response = await fetch(`${baseURL}${path}`, { ...init, headers: requestHeaders, signal, }); if (!response.ok) { throw await buildSSERequestError(response); } if (!response.body) { throw new Error("stream response body is empty"); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; try { while (true) { const { done, value } = await reader.read(); if (done) { break; } buffer += decoder.decode(value, { stream: true }); buffer = consumeSSEBuffer(buffer, handlers.onEvent); } buffer += decoder.decode(); consumeSSEBuffer(buffer, handlers.onEvent); } finally { reader.releaseLock(); handlers.onClose?.(); } } type SSEErrorEnvelope = { code?: number; message?: string; detail?: string; request_id?: string; }; async function buildSSERequestError(response: Response): Promise { try { const payload = (await response.clone().json()) as SSEErrorEnvelope | null; if (payload && typeof payload === "object" && !Array.isArray(payload)) { return new ApiClientError({ message: payload.message ?? "unknown_error", code: payload.code, status: response.status, detail: payload.detail, requestId: payload.request_id, }); } } catch { // Ignore JSON parse errors and fall back to a generic error below. } let detail: string | undefined; try { const text = (await response.text()).trim(); detail = text || undefined; } catch { detail = undefined; } return new ApiClientError({ message: "unknown_error", status: response.status, detail: detail ?? `stream request failed with status ${response.status}`, }); } export const brandsApi = { list() { return apiClient.get("/api/tenant/brands"); }, getLibrarySummary() { return apiClient.get("/api/tenant/brands/library-summary"); }, create(payload: BrandRequest) { return apiClient.post("/api/tenant/brands", payload); }, detail(id: number) { return apiClient.get(`/api/tenant/brands/${id}`); }, update(id: number, payload: BrandRequest) { return apiClient.put(`/api/tenant/brands/${id}`, payload); }, remove(id: number) { return apiClient.remove(`/api/tenant/brands/${id}`); }, listKeywords(brandId: number) { return apiClient.get(`/api/tenant/brands/${brandId}/keywords`); }, createKeyword(brandId: number, payload: KeywordRequest) { return apiClient.post( `/api/tenant/brands/${brandId}/keywords`, payload, ); }, updateKeyword(brandId: number, keywordId: number, payload: KeywordRequest) { return apiClient.put( `/api/tenant/brands/${brandId}/keywords/${keywordId}`, payload, ); }, removeKeyword(brandId: number, keywordId: number) { return apiClient.remove(`/api/tenant/brands/${brandId}/keywords/${keywordId}`); }, listQuestions(brandId: number, keywordId?: number | null) { return apiClient.get(`/api/tenant/brands/${brandId}/questions`, { params: keywordId ? { keyword_id: keywordId } : undefined, }); }, createQuestion(brandId: number, payload: QuestionRequest) { return apiClient.post( `/api/tenant/brands/${brandId}/questions`, payload, ); }, updateQuestion(brandId: number, questionId: number, payload: UpdateQuestionRequest) { return apiClient.put( `/api/tenant/brands/${brandId}/questions/${questionId}`, payload, ); }, removeQuestion(brandId: number, questionId: number) { return apiClient.remove(`/api/tenant/brands/${brandId}/questions/${questionId}`); }, listCompetitors(brandId: number) { return apiClient.get(`/api/tenant/brands/${brandId}/competitors`); }, createCompetitor(brandId: number, payload: CompetitorRequest) { return apiClient.post( `/api/tenant/brands/${brandId}/competitors`, payload, ); }, updateCompetitor(brandId: number, competitorId: number, payload: CompetitorRequest) { return apiClient.put( `/api/tenant/brands/${brandId}/competitors/${competitorId}`, payload, ); }, removeCompetitor(brandId: number, competitorId: number) { return apiClient.remove(`/api/tenant/brands/${brandId}/competitors/${competitorId}`); }, }; export const mediaApi = { platforms() { return apiClient.get("/api/tenant/media/platforms"); }, accounts() { return removedMediaApi("accounts"); }, createPluginSession(payload: CreatePluginSessionRequest) { void payload; return removedMediaApi("create_plugin_session"); }, registerPluginInstallation(payload: RegisterPluginInstallationRequest) { void payload; return removedMediaApi("register_plugin_installation"); }, unbindAccount(id: number) { void id; return removedMediaApi("unbind_account"); }, }; export const tenantAccountsApi = { list() { return apiClient.get("/api/tenant/accounts"); }, requestDelete(id: string, options?: { undo?: boolean }) { const query = options?.undo ? "?undo=true" : ""; return apiClient.post(`/api/tenant/accounts/${encodeURIComponent(id)}/request-delete${query}`); }, }; export const publishJobsApi = { create(payload: CreatePublishJobRequest) { return apiClient.post("/api/tenant/publish-jobs", payload); }, }; function removedMediaApi(action: string): Promise { return Promise.reject(new ApiClientError({ message: "desktop_media_api_removed", detail: `legacy media API action "${action}" has been removed; use desktop client accounts and publish jobs instead`, })); } export const monitoringApi = { dashboardComposite(params: { brand_id?: number; keyword_id?: number | null; days?: number; business_date?: string; ai_platform_id?: string; }) { return apiClient.get("/api/tenant/monitoring/dashboard/composite", { params, }); }, questionDetail( brandId: number, questionId: number, params: { date_from?: string; date_to?: string; question_hash?: string; ai_platform_id?: string; }, ) { return apiClient.get( `/api/tenant/monitoring/brands/${brandId}/questions/${questionId}/detail`, { params }, ); }, collectNow(brandId: number, payload?: { keyword_id?: number | null }) { return apiClient.post( `/api/tenant/monitoring/brands/${brandId}/collect-now`, payload ?? {}, ); }, }; export const promptRulesApi = { list(params?: PromptRuleListParams) { return apiClient.get("/api/tenant/prompt-rules", { params }); }, listSimple() { return apiClient.get("/api/tenant/prompt-rules/simple"); }, detail(id: number) { return apiClient.get(`/api/tenant/prompt-rules/${id}`); }, create(payload: PromptRuleRequest) { return apiClient.post("/api/tenant/prompt-rules", payload); }, update(id: number, payload: PromptRuleRequest) { return apiClient.put(`/api/tenant/prompt-rules/${id}`, payload); }, remove(id: number) { return apiClient.remove(`/api/tenant/prompt-rules/${id}`); }, toggleStatus(id: number, payload: PromptRuleStatusRequest) { return apiClient.put(`/api/tenant/prompt-rules/${id}/status`, payload); }, listGroups() { return apiClient.get("/api/tenant/prompt-rules/groups"); }, createGroup(payload: PromptRuleGroupRequest) { return apiClient.post("/api/tenant/prompt-rules/groups", payload); }, updateGroup(id: number, payload: PromptRuleGroupRequest) { return apiClient.put(`/api/tenant/prompt-rules/groups/${id}`, payload); }, removeGroup(id: number) { return apiClient.remove(`/api/tenant/prompt-rules/groups/${id}`); }, }; export const schedulesApi = { list(params?: ScheduleTaskListParams) { return apiClient.get("/api/tenant/schedules", { params }); }, detail(id: number) { return apiClient.get(`/api/tenant/schedules/${id}`); }, create(payload: ScheduleTaskRequest) { return apiClient.post("/api/tenant/schedules", payload); }, update(id: number, payload: ScheduleTaskRequest) { return apiClient.put(`/api/tenant/schedules/${id}`, payload); }, remove(id: number) { return apiClient.remove(`/api/tenant/schedules/${id}`); }, toggleStatus(id: number, payload: ScheduleTaskStatusRequest) { return apiClient.put(`/api/tenant/schedules/${id}/status`, payload); }, }; export const generateApi = { fromRule(payload: GenerateFromRuleRequest) { return apiClient.post( "/api/tenant/articles/generate-from-rule", payload, ); }, }; export const instantTasksApi = { list(params?: InstantTaskListParams) { return apiClient.get("/api/tenant/instant-tasks", { params }); }, }; export function normalizeInputParams( payload: Record, articleId?: number, wizardState?: Record, ): TemplateGenerateRequest { return { article_id: articleId, input_params: payload, wizard_state: wizardState, }; } function consumeSSEBuffer( buffer: string, onEvent: (event: TEvent) => void, ): string { let next = buffer; for (;;) { const delimiterIndex = next.indexOf("\n\n"); if (delimiterIndex === -1) { break; } const rawEvent = next.slice(0, delimiterIndex); next = next.slice(delimiterIndex + 2); const parsed = parseSSEEvent(rawEvent); if (parsed) { onEvent(parsed); } } return next; } function parseSSEEvent(raw: string): TEvent | null { const lines = raw.split(/\r?\n/); let event = "message"; const dataLines: string[] = []; for (const line of lines) { if (line.startsWith("event:")) { event = line.slice(6).trim(); continue; } if (line.startsWith("data:")) { dataLines.push(line.slice(5).trimStart()); } } if (dataLines.length === 0) { return null; } const data = JSON.parse(dataLines.join("\n")) as Record | null; if (!data || Array.isArray(data)) { return null; } return { ...data, type: event, } as TEvent; }