Files
geo/apps/admin-web/src/lib/api.ts
T
root a617d39a4a feat(desktop): drop parked review flow, add publish management
SaaS 侧人工审核后才创建 publish job,desktop 不再做二次审核:移除
manual/waiting_user/parked/from_parked 状态机与 LeaseFromParked 查询,
desktop client 只执行发布并新增"发布管理"页(待发布队列 / 历史 / 再次
发送)。同时抽离 @geo/publisher-platforms 共享适配器包、新增 Redis-based
desktop_presence 与 publish_record_support,刷新 admin-web 发布弹窗与
媒体库;plan A / spec 文档同步口径。
2026-04-20 09:52:48 +08:00

1089 lines
32 KiB
TypeScript

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<AuthTokens> {
const data = await publicClient.post<RefreshResponse, { refresh_token: string }>(
"/api/auth/refresh",
{ refresh_token: refreshToken },
);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
};
},
},
});
export const authApi = {
login(payload: LoginRequest) {
return publicClient.post<LoginResponse, LoginRequest>("/api/auth/login", payload);
},
me() {
return apiClient.get<UserInfo>("/api/auth/me");
},
logout() {
return apiClient.post<null>("/api/auth/logout");
},
};
export const workspaceApi = {
overview() {
return apiClient.get<WorkspaceOverview>("/api/tenant/workspace/overview");
},
recentArticles() {
return apiClient.get<RecentArticle[]>("/api/tenant/workspace/recent-articles");
},
quotaSummary() {
return apiClient.get<QuotaSummary>("/api/tenant/workspace/quota-summary");
},
templateCards() {
return apiClient.get<TemplateCard[]>("/api/tenant/workspace/template-cards");
},
kolCards() {
return apiClient.get<KolWorkspaceCard[]>("/api/tenant/workspace/kol-cards");
},
};
export const templatesApi = {
list() {
return apiClient.get<TemplateListItem[]>("/api/tenant/templates");
},
detail(id: number) {
return apiClient.get<TemplateDetail>(`/api/tenant/templates/${id}`);
},
createAnalyzeTask(id: number, payload: TemplateAnalyzeTaskRequest) {
return apiClient.post<TemplateAssistTaskCreateResponse, TemplateAnalyzeTaskRequest>(
`/api/tenant/templates/${id}/analyze-tasks`,
payload,
);
},
getAnalyzeTaskResult(id: number, taskId: string) {
return apiClient.get<TemplateAnalyzeTaskResultResponse>(
`/api/tenant/templates/${id}/analyze_task_result`,
{ params: { task_id: taskId } },
);
},
createTitleTask(id: number, payload: TemplateTitleTaskRequest) {
return apiClient.post<TemplateAssistTaskCreateResponse, TemplateTitleTaskRequest>(
`/api/tenant/templates/${id}/title-tasks`,
payload,
);
},
getTitleTaskResult(id: number, taskId: string) {
return apiClient.get<TemplateTitleTaskResultResponse>(
`/api/tenant/templates/${id}/gen_title_task_result`,
{ params: { task_id: taskId } },
);
},
createOutlineTask(id: number, payload: TemplateOutlineTaskRequest) {
return apiClient.post<TemplateAssistTaskCreateResponse, TemplateOutlineTaskRequest>(
`/api/tenant/templates/${id}/outline-tasks`,
payload,
);
},
getOutlineTaskResult(id: number, taskId: string) {
return apiClient.get<TemplateOutlineTaskResultResponse>(
`/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<TemplateGenerateResponse, TemplateGenerateRequest>(
`/api/tenant/templates/${id}/generate`,
payload,
);
},
saveDraft(id: number, payload: TemplateSaveDraftRequest) {
return apiClient.post<TemplateSaveDraftResponse, TemplateSaveDraftRequest>(
`/api/tenant/templates/${id}/drafts`,
payload,
);
},
};
export const kolManageApi = {
profile() {
return apiClient.get<KolProfile>("/api/tenant/kol/manage/profile");
},
updateProfile(body: {
display_name: string;
bio?: string | null;
avatar_url?: string | null;
}) {
return apiClient.put<KolProfile, typeof body>(
"/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<ApiEnvelope<{
url: string;
object_key: string;
content_type: string;
size_bytes: number;
}>>("/api/tenant/kol/manage/profile/avatar", formData);
markImageUploadRequestFinished(progressToken);
return response.data.data;
} catch (error) {
markImageUploadRequestFailed(progressToken);
throw error;
}
},
listPackages() {
return apiClient.get<KolPackageSummary[]>("/api/tenant/kol/manage/packages");
},
createPackage(body: CreateKolPackageRequest) {
return apiClient.post<KolPackageSummary, CreateKolPackageRequest>(
"/api/tenant/kol/manage/packages",
body,
);
},
updatePackage(id: number, body: UpdateKolPackageRequest) {
return apiClient.put<KolPackageSummary, UpdateKolPackageRequest>(
`/api/tenant/kol/manage/packages/${id}`,
body,
);
},
publishPackage(id: number) {
return apiClient.put<KolPackageSummary, Record<string, never>>(
`/api/tenant/kol/manage/packages/${id}/publish`,
{},
);
},
archivePackage(id: number) {
return apiClient.put<KolPackageSummary, Record<string, never>>(
`/api/tenant/kol/manage/packages/${id}/archive`,
{},
);
},
deletePackage(id: number) {
return apiClient.remove<null>(`/api/tenant/kol/manage/packages/${id}`);
},
listPrompts(packageId: number) {
return apiClient.get<KolPromptDetail[]>(`/api/tenant/kol/manage/packages/${packageId}/prompts`);
},
createPrompt(packageId: number, body: CreateKolPromptRequest) {
return apiClient.post<KolPromptDetail, CreateKolPromptRequest>(
`/api/tenant/kol/manage/packages/${packageId}/prompts`,
body,
);
},
getPrompt(id: number) {
return apiClient.get<KolPromptDetail>(`/api/tenant/kol/manage/prompts/${id}`);
},
updatePromptMetadata(id: number, body: UpdateKolPromptRequest) {
return apiClient.put<KolPromptDetail, UpdateKolPromptRequest>(
`/api/tenant/kol/manage/prompts/${id}`,
body,
);
},
deletePrompt(id: number) {
return apiClient.remove<null>(`/api/tenant/kol/manage/prompts/${id}`);
},
savePrompt(promptId: number, body: PublishKolPromptRequest) {
return apiClient.post<KolPromptDetail, PublishKolPromptRequest>(
`/api/tenant/kol/manage/prompts/${promptId}/save`,
body,
);
},
publishPrompt(promptId: number, body: PublishKolPromptRequest) {
return apiClient.post<KolPromptDetail, PublishKolPromptRequest>(
`/api/tenant/kol/manage/prompts/${promptId}/publish`,
body,
);
},
activatePrompt(promptId: number) {
return apiClient.put<KolPromptDetail, Record<string, never>>(
`/api/tenant/kol/manage/prompts/${promptId}/activate`,
{},
);
},
archivePrompt(promptId: number) {
return apiClient.put<KolPromptDetail, Record<string, never>>(
`/api/tenant/kol/manage/prompts/${promptId}/archive`,
{},
);
},
submitAssist(body: KolAssistRequest) {
return apiClient.post<KolAssistSubmitResponse, KolAssistRequest>(
"/api/tenant/kol/manage/assist",
body,
);
},
streamAssist(
body: KolAssistRequest,
handlers: {
onEvent: (event: KolAssistStreamEvent) => void;
onClose?: () => void;
},
signal: AbortSignal,
) {
return subscribeSSE<KolAssistStreamEvent>(
"/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<KolAssistTask>(`/api/tenant/kol/manage/assist/${id}`);
},
};
export const kolMarketplaceApi = {
list(params: { industry?: string; keyword?: string; limit?: number; offset?: number }) {
return apiClient.get<KolPackageSummary[]>("/api/tenant/kol/marketplace/packages", { params });
},
detail(id: number) {
return apiClient.get<KolPackageDetail>(`/api/tenant/kol/marketplace/packages/${id}`);
},
subscribe(id: number) {
return apiClient.post<KolSubscription, Record<string, never>>(
`/api/tenant/kol/marketplace/packages/${id}/subscribe`,
{},
);
},
mySubscriptions() {
return apiClient.get<KolSubscription[]>("/api/tenant/kol/subscriptions");
},
mySubscriptionPrompts() {
return apiClient.get<KolSubscriptionPromptCard[]>("/api/tenant/kol/subscription-prompts");
},
};
export const kolGenerateApi = {
schema(id: number | string) {
return apiClient.get<KolSubscriptionPromptSchema>(`/api/tenant/kol/subscription-prompts/${id}/schema`);
},
submit(id: number | string, body: KolGenerateRequest) {
return apiClient.post<KolGenerateResponse, KolGenerateRequest>(
`/api/tenant/kol/subscription-prompts/${id}/generate`,
body,
);
},
};
export const kolDashboardApi = {
overview() {
return apiClient.get<KolDashboardOverview>("/api/tenant/kol/dashboard/overview");
},
packages() {
return apiClient.get<KolDashboardPackageStat[]>("/api/tenant/kol/dashboard/packages");
},
trend(periodDays: number) {
return apiClient.get<KolDashboardTrendPoint[]>("/api/tenant/kol/dashboard/trend", {
params: { period_days: periodDays },
});
},
};
export const articlesApi = {
list(params: ArticleListParams) {
return apiClient.get<ArticleListResponse>("/api/tenant/articles", { params });
},
create(payload: CreateArticleRequest = {}) {
return apiClient.post<ArticleDetail, CreateArticleRequest>("/api/tenant/articles", payload);
},
detail(id: number) {
return apiClient.get<ArticleDetail>(`/api/tenant/articles/${id}`);
},
update(id: number, payload: UpdateArticleRequest) {
return apiClient.put<ArticleDetail, UpdateArticleRequest>(`/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<ApiEnvelope<ArticleImageUploadResponse>>(
`/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<ArticleVersion[]>(`/api/tenant/articles/${id}/versions`);
},
publishBatch(id: number, payload: CreatePublishBatchRequest) {
return apiClient.post<CreatePublishBatchResponse, CreatePublishBatchRequest>(
`/api/tenant/articles/${id}/publish-batches`,
payload,
);
},
publishRecords(id: number) {
return apiClient.get<PublishRecord[]>(`/api/tenant/articles/${id}/publish-records`);
},
remove(id: number) {
return apiClient.remove<null>(`/api/tenant/articles/${id}`);
},
};
export const knowledgeApi = {
listGroups() {
return apiClient.get<KnowledgeGroup[]>("/api/tenant/knowledge/groups");
},
createGroup(payload: KnowledgeGroupRequest) {
return apiClient.post<KnowledgeGroup, KnowledgeGroupRequest>("/api/tenant/knowledge/groups", payload);
},
updateGroup(id: number, payload: KnowledgeGroupRequest) {
return apiClient.put<null, KnowledgeGroupRequest>(`/api/tenant/knowledge/groups/${id}`, payload);
},
removeGroup(id: number) {
return apiClient.remove<null>(`/api/tenant/knowledge/groups/${id}`);
},
listItems(groupId?: number) {
return apiClient.get<KnowledgeItem[]>("/api/tenant/knowledge/items", {
params: groupId ? { group_id: groupId } : undefined,
});
},
detail(id: number) {
return apiClient.get<KnowledgeItemDetail>(`/api/tenant/knowledge/items/${id}`);
},
createTextItem(payload: KnowledgeTextItemRequest) {
return apiClient.post<KnowledgeItem, KnowledgeTextItemRequest>(
"/api/tenant/knowledge/items/text",
payload,
);
},
createURLItem(payload: KnowledgeURLItemRequest) {
return apiClient.post<KnowledgeItem, KnowledgeURLItemRequest>(
"/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<null>(`/api/tenant/knowledge/items/${id}`);
},
};
export const imagesApi = {
listFolders() {
return apiClient.get<ImageFolder[]>("/api/tenant/images/folders");
},
createFolder(name: string) {
return apiClient.post<ImageFolder, { name: string }>("/api/tenant/images/folders", { name });
},
updateFolder(id: number, name: string) {
return apiClient.put<null, { name: string }>(`/api/tenant/images/folders/${id}`, { name });
},
folderDeletePreview(id: number) {
return apiClient.get<FolderDeletePreview>(`/api/tenant/images/folders/${id}/delete-preview`);
},
deleteFolder(id: number, force?: boolean) {
return apiClient.remove<null>(`/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<ImageAssetListResponse>("/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<ApiEnvelope<ImageUploadResponse>>(
"/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<null, ImageUpdateRequest>(`/api/tenant/images/${id}`, payload);
},
getReferences(id: number) {
return apiClient.get<ImageReferenceDetail>(`/api/tenant/images/${id}/references`);
},
delete(id: number, force?: boolean) {
return apiClient.remove<null>(`/api/tenant/images/${id}`, { params: force ? { force: 1 } : undefined });
},
storageUsage() {
return apiClient.get<ImageStorageUsage>("/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<void> {
return subscribeSSE<ArticleGenerationStreamEvent>(
`/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<void> {
return subscribeSSE<ArticleSelectionOptimizeStreamEvent>(
`/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<TEvent extends SSEEventShape>(
path: string,
init: RequestInit,
handlers: {
onEvent: (event: TEvent) => void;
onClose?: () => void;
},
signal: AbortSignal,
): Promise<void> {
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<TEvent>(buffer, handlers.onEvent);
}
buffer += decoder.decode();
consumeSSEBuffer<TEvent>(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<ApiClientError> {
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<Brand[]>("/api/tenant/brands");
},
getLibrarySummary() {
return apiClient.get<BrandLibrarySummary>("/api/tenant/brands/library-summary");
},
create(payload: BrandRequest) {
return apiClient.post<Brand, BrandRequest>("/api/tenant/brands", payload);
},
detail(id: number) {
return apiClient.get<Brand>(`/api/tenant/brands/${id}`);
},
update(id: number, payload: BrandRequest) {
return apiClient.put<null, BrandRequest>(`/api/tenant/brands/${id}`, payload);
},
remove(id: number) {
return apiClient.remove<null>(`/api/tenant/brands/${id}`);
},
listKeywords(brandId: number) {
return apiClient.get<Keyword[]>(`/api/tenant/brands/${brandId}/keywords`);
},
createKeyword(brandId: number, payload: KeywordRequest) {
return apiClient.post<Keyword, KeywordRequest>(
`/api/tenant/brands/${brandId}/keywords`,
payload,
);
},
updateKeyword(brandId: number, keywordId: number, payload: KeywordRequest) {
return apiClient.put<null, KeywordRequest>(
`/api/tenant/brands/${brandId}/keywords/${keywordId}`,
payload,
);
},
removeKeyword(brandId: number, keywordId: number) {
return apiClient.remove<null>(`/api/tenant/brands/${brandId}/keywords/${keywordId}`);
},
listQuestions(brandId: number, keywordId?: number | null) {
return apiClient.get<Question[]>(`/api/tenant/brands/${brandId}/questions`, {
params: keywordId ? { keyword_id: keywordId } : undefined,
});
},
createQuestion(brandId: number, payload: QuestionRequest) {
return apiClient.post<Question, QuestionRequest>(
`/api/tenant/brands/${brandId}/questions`,
payload,
);
},
updateQuestion(brandId: number, questionId: number, payload: UpdateQuestionRequest) {
return apiClient.put<null, UpdateQuestionRequest>(
`/api/tenant/brands/${brandId}/questions/${questionId}`,
payload,
);
},
removeQuestion(brandId: number, questionId: number) {
return apiClient.remove<null>(`/api/tenant/brands/${brandId}/questions/${questionId}`);
},
listCompetitors(brandId: number) {
return apiClient.get<Competitor[]>(`/api/tenant/brands/${brandId}/competitors`);
},
createCompetitor(brandId: number, payload: CompetitorRequest) {
return apiClient.post<Competitor, CompetitorRequest>(
`/api/tenant/brands/${brandId}/competitors`,
payload,
);
},
updateCompetitor(brandId: number, competitorId: number, payload: CompetitorRequest) {
return apiClient.put<null, CompetitorRequest>(
`/api/tenant/brands/${brandId}/competitors/${competitorId}`,
payload,
);
},
removeCompetitor(brandId: number, competitorId: number) {
return apiClient.remove<null>(`/api/tenant/brands/${brandId}/competitors/${competitorId}`);
},
};
export const mediaApi = {
platforms() {
return apiClient.get<MediaPlatform[]>("/api/tenant/media/platforms");
},
accounts() {
return removedMediaApi<PlatformAccount[]>("accounts");
},
createPluginSession(payload: CreatePluginSessionRequest) {
void payload;
return removedMediaApi<CreatePluginSessionResponse>("create_plugin_session");
},
registerPluginInstallation(payload: RegisterPluginInstallationRequest) {
void payload;
return removedMediaApi<RegisterPluginInstallationResponse>("register_plugin_installation");
},
unbindAccount(id: number) {
void id;
return removedMediaApi<null>("unbind_account");
},
};
export const tenantAccountsApi = {
list() {
return apiClient.get<DesktopAccountInfo[]>("/api/tenant/accounts");
},
requestDelete(id: string, options?: { undo?: boolean }) {
const query = options?.undo ? "?undo=true" : "";
return apiClient.post<DesktopAccountInfo>(`/api/tenant/accounts/${encodeURIComponent(id)}/request-delete${query}`);
},
};
export const publishJobsApi = {
create(payload: CreatePublishJobRequest) {
return apiClient.post<CreatePublishJobResponse, CreatePublishJobRequest>("/api/tenant/publish-jobs", payload);
},
};
function removedMediaApi<T>(action: string): Promise<T> {
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<MonitoringDashboardCompositeResponse>("/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<MonitoringQuestionDetailResponse>(
`/api/tenant/monitoring/brands/${brandId}/questions/${questionId}/detail`,
{ params },
);
},
collectNow(brandId: number, payload?: { keyword_id?: number | null }) {
return apiClient.post<MonitoringCollectNowResponse, { keyword_id?: number | null }>(
`/api/tenant/monitoring/brands/${brandId}/collect-now`,
payload ?? {},
);
},
};
export const promptRulesApi = {
list(params?: PromptRuleListParams) {
return apiClient.get<PromptRuleListResponse>("/api/tenant/prompt-rules", { params });
},
listSimple() {
return apiClient.get<PromptRuleSimple[]>("/api/tenant/prompt-rules/simple");
},
detail(id: number) {
return apiClient.get<PromptRule>(`/api/tenant/prompt-rules/${id}`);
},
create(payload: PromptRuleRequest) {
return apiClient.post<PromptRule, PromptRuleRequest>("/api/tenant/prompt-rules", payload);
},
update(id: number, payload: PromptRuleRequest) {
return apiClient.put<null, PromptRuleRequest>(`/api/tenant/prompt-rules/${id}`, payload);
},
remove(id: number) {
return apiClient.remove<null>(`/api/tenant/prompt-rules/${id}`);
},
toggleStatus(id: number, payload: PromptRuleStatusRequest) {
return apiClient.put<null, PromptRuleStatusRequest>(`/api/tenant/prompt-rules/${id}/status`, payload);
},
listGroups() {
return apiClient.get<PromptRuleGroup[]>("/api/tenant/prompt-rules/groups");
},
createGroup(payload: PromptRuleGroupRequest) {
return apiClient.post<PromptRuleGroup, PromptRuleGroupRequest>("/api/tenant/prompt-rules/groups", payload);
},
updateGroup(id: number, payload: PromptRuleGroupRequest) {
return apiClient.put<null, PromptRuleGroupRequest>(`/api/tenant/prompt-rules/groups/${id}`, payload);
},
removeGroup(id: number) {
return apiClient.remove<null>(`/api/tenant/prompt-rules/groups/${id}`);
},
};
export const schedulesApi = {
list(params?: ScheduleTaskListParams) {
return apiClient.get<ScheduleTaskListResponse>("/api/tenant/schedules", { params });
},
detail(id: number) {
return apiClient.get<ScheduleTask>(`/api/tenant/schedules/${id}`);
},
create(payload: ScheduleTaskRequest) {
return apiClient.post<ScheduleTask, ScheduleTaskRequest>("/api/tenant/schedules", payload);
},
update(id: number, payload: ScheduleTaskRequest) {
return apiClient.put<null, ScheduleTaskRequest>(`/api/tenant/schedules/${id}`, payload);
},
remove(id: number) {
return apiClient.remove<null>(`/api/tenant/schedules/${id}`);
},
toggleStatus(id: number, payload: ScheduleTaskStatusRequest) {
return apiClient.put<null, ScheduleTaskStatusRequest>(`/api/tenant/schedules/${id}/status`, payload);
},
};
export const generateApi = {
fromRule(payload: GenerateFromRuleRequest) {
return apiClient.post<GenerateFromRuleResponse, GenerateFromRuleRequest>(
"/api/tenant/articles/generate-from-rule",
payload,
);
},
};
export const instantTasksApi = {
list(params?: InstantTaskListParams) {
return apiClient.get<InstantTaskListResponse>("/api/tenant/instant-tasks", { params });
},
};
export function normalizeInputParams(
payload: Record<string, JsonValue>,
articleId?: number,
wizardState?: Record<string, JsonValue>,
): TemplateGenerateRequest {
return {
article_id: articleId,
input_params: payload,
wizard_state: wizardState,
};
}
function consumeSSEBuffer<TEvent extends SSEEventShape>(
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<TEvent>(rawEvent);
if (parsed) {
onEvent(parsed);
}
}
return next;
}
function parseSSEEvent<TEvent extends SSEEventShape>(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<string, unknown> | null;
if (!data || Array.isArray(data)) {
return null;
}
return {
...data,
type: event,
} as TEvent;
}