Files
geo/apps/admin-web/src/lib/api.ts
T
root 9ed857e159
Frontend CI / Frontend (push) Successful in 3m19s
Backend CI / Backend (push) Successful in 14m30s
feat: paginate and search brand questions across selects
Make GET /api/tenant/brands/:id/questions return a paginated
QuestionListResponse (items/total/page/page_size) with optional `q`
full-text filter, validated page/page_size query params, and per-params
cache keys. Add a usePaginatedBrandQuestions composable backing the
imitation, template-wizard, and tracking question selects with
search-as-you-type and infinite scroll, plus ensure-loaded-by-id so a
deep-linked or pre-selected question is fetched even when off the first
page. Brands view now drives its questions table via server pagination.

Also redesign the Tracking hot-questions and cited-articles lists
(mention-rate badges/bars, metric groups, refreshed styling) and fall
back to citation-fact article_id/title when no high-confidence URL alias
matches, so cited articles surface even without an alias row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:01:23 +08:00

1711 lines
50 KiB
TypeScript

import { ApiClientError, createApiClient } from '@geo/http-client'
import type {
AIPointUsageListResponse,
ApiEnvelope,
ArticleDetail,
ArticleImageUploadResponse,
ArticleListParams,
ArticleListResponse,
ArticleVersion,
AuthTokens,
Brand,
BrandLibrarySummary,
BrandRequest,
ChangePasswordRequest,
ClassifiedQuestion,
ClassifyQuestionsRequest,
Competitor,
CompetitorRequest,
ComplianceAckRecord,
ComplianceCheckArticleRequest,
ComplianceCheckPlainRequest,
ComplianceCheckResult,
ComplianceManualReview,
ComplianceRuntimeStatus,
CreateAckRequest,
CreateArticleRequest,
CreateEnterpriseSiteConnectionRequest,
CreateKolPackageRequest,
CreateKolPromptRequest,
CreateMediaSupplyOrderRequest,
CreateMediaSupplyOrderResponse,
CreatePluginSessionRequest,
CreatePluginSessionResponse,
CreatePublishBatchRequest,
CreatePublishBatchResponse,
CreatePublishJobRequest,
CreatePublishJobResponse,
DesktopAccountInfo,
DesktopClientDownloadableRelease,
DesktopClientDownloadableReleaseListResponse,
DesktopClientReleaseCheckResponse,
DesktopClientReleaseDownloadURLResponse,
EnterpriseSiteCapability,
EnterpriseSiteCategory,
EnterpriseSiteConnection,
EnterpriseSitePublishResponse,
FolderDeletePreview,
GenerateFromRuleRequest,
GenerateFromRuleResponse,
GenerateImitationRequest,
GenerateImitationResponse,
ImageAssetListResponse,
ImageFolder,
ImageReferenceDetail,
ImageStorageUsage,
ImageUpdateRequest,
ImageUploadResponse,
InstantTaskListParams,
InstantTaskListResponse,
JsonValue,
Keyword,
KeywordRequest,
KnowledgeGroup,
KnowledgeGroupRequest,
KnowledgeItem,
KnowledgeItemDetail,
KnowledgeTextItemRequest,
KnowledgeURLItemRequest,
KolAssistRequest,
KolAssistStreamEvent,
KolAssistSubmitResponse,
KolAssistTask,
KolDashboardOverview,
KolDashboardPackageStat,
KolDashboardTrendPoint,
KolGenerateRequest,
KolGenerateResponse,
KolPackageDetail,
KolPackageSummary,
KolProfile,
KolPromptDetail,
KolSubscription,
KolSubscriptionPromptCard,
KolSubscriptionPromptSchema,
KolWorkspaceCard,
ListCustomerSupplierMediaResourcesResponse,
ListDesktopPublishTasksParams,
ListMediaSupplyOrdersParams,
ListMediaSupplyOrdersResponse,
ListMediaSupplyWalletLedgersParams,
ListMediaSupplyWalletLedgersResponse,
ListPublishRecordsParams,
ListSupplierMediaResourcesParams,
LoginRequest,
LoginResponse,
MaterializeQuestionsRequest,
MaterializeQuestionsResult,
MediaPlatform,
MediaSupplyOrderDetail,
MediaSupplySearchOptionsResponse,
MediaSupplySessionStatus,
MediaSupplyWalletStatus,
MonitoringCitationSummaryResponse,
MonitoringCollectNowResponse,
MonitoringDashboardCompositeResponse,
MonitoringQuestionDetailResponse,
PasswordPublicKeyResponse,
PlatformAccount,
PromptRule,
PromptRuleGroup,
PromptRuleGroupRequest,
PromptRuleListParams,
PromptRuleListResponse,
PromptRuleRequest,
PromptRuleSimple,
PromptRuleStatusRequest,
PublishEnterpriseSiteArticleRequest,
PublishKolPromptRequest,
PublishRecord,
PublishRecordListResponse,
Question,
QuestionCandidateResult,
QuestionCombinationFillRequest,
QuestionCombinationFillResult,
QuestionCombinationRequest,
QuestionDistillRequest,
QuestionListResponse,
QuestionRequest,
QuotaSummary,
RecentArticle,
RefreshResponse,
RegenerateArticleResponse,
RegisterPluginInstallationRequest,
RegisterPluginInstallationResponse,
ScheduleTask,
ScheduleTaskListParams,
ScheduleTaskListResponse,
ScheduleTaskRequest,
ScheduleTaskStatusRequest,
TemplateAnalyzeTaskRequest,
TemplateAnalyzeTaskResultResponse,
TemplateAssistTaskCreateResponse,
TemplateCard,
TemplateDetail,
TemplateGenerateRequest,
TemplateGenerateResponse,
TemplateListItem,
TemplateOutlineTaskRequest,
TemplateOutlineTaskResultResponse,
TemplateSaveDraftRequest,
TemplateSaveDraftResponse,
TemplateTitleTaskRequest,
TemplateTitleTaskResultResponse,
TenantPublishTaskListResponse,
UpdateArticleRequest,
UpdateEnterpriseSiteConnectionRequest,
UpdateKolPackageRequest,
UpdateKolPromptRequest,
UpdateQuestionRequest,
UserInfo,
WorkspaceOverview,
} from '@geo/shared-types'
import { readStoredCurrentBrandId } from './current-brand'
import {
convertImageFileToWebp,
markImageUploadRequestFailed,
markImageUploadRequestFinished,
markImageUploadRequestStarted,
} from './image-webp'
import {
clearStoredSession,
getStoredAccessToken,
getStoredRefreshToken,
markStoredMembershipBlocked,
readStoredSession,
setStoredTokens,
} from './session'
const rawBaseURL = import.meta.env.VITE_API_BASE_URL ?? ''
const baseURL = normalizeApiBaseURL(rawBaseURL)
const generationStreamEnabled = import.meta.env.VITE_GENERATION_STREAM_ENABLED === 'true'
const accessRefreshSkewMs = 2 * 60 * 1000
const aiDistillRequestTimeoutMs = 45_000
const publicClient = createApiClient({ baseURL })
let storedRefreshPromise: Promise<AuthTokens> | null = null
function normalizeApiBaseURL(value: string): string {
const trimmed = value.trim().replace(/\/+$/, '')
if (!trimmed) {
return ''
}
return trimmed.replace(/\/api$/i, '')
}
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()
}
function toAuthTokens(data: RefreshResponse): AuthTokens {
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: data.expires_at,
}
}
function authRequiredError(): ApiClientError {
return new ApiClientError({
message: 'not_authenticated',
code: 40100,
status: 401,
handled: true,
})
}
function expireStoredSession(): void {
clearStoredSession({ notifyAuthExpired: true })
}
function markAuthErrorHandled(error: unknown): unknown {
if (error instanceof ApiClientError && error.status === 401) {
error.handled = true
}
return error
}
export function isAuthSessionExpiredError(error: unknown): boolean {
return error instanceof ApiClientError && error.status === 401
}
export function shouldRefreshAccessToken(
expiresAt: number | null,
minRemainingMs = accessRefreshSkewMs,
): boolean {
return !expiresAt || expiresAt * 1000 - Date.now() <= minRemainingMs
}
export async function refreshStoredAuthSession(
refreshToken = getStoredRefreshToken(),
): Promise<AuthTokens> {
if (!refreshToken) {
expireStoredSession()
throw authRequiredError()
}
const requestedRefreshToken = refreshToken
if (!storedRefreshPromise) {
storedRefreshPromise = publicClient
.post<RefreshResponse, { refresh_token: string }>('/api/auth/refresh', {
refresh_token: requestedRefreshToken,
})
.then((data) => {
const tokens = toAuthTokens(data)
setStoredTokens(tokens)
return tokens
})
.catch((error) => {
if (isAuthSessionExpiredError(error)) {
const current = readStoredSession()
if (
current.accessToken &&
current.refreshToken &&
current.refreshToken !== requestedRefreshToken
) {
const tokens = {
accessToken: current.accessToken,
refreshToken: current.refreshToken,
expiresAt: current.accessTokenExpiresAt ?? undefined,
}
setStoredTokens(tokens)
return tokens
}
expireStoredSession()
}
throw markAuthErrorHandled(error)
})
.finally(() => {
storedRefreshPromise = null
})
}
return storedRefreshPromise
}
async function getFreshStoredAccessToken(): Promise<string> {
const snapshot = readStoredSession()
if (snapshot.refreshToken && shouldRefreshAccessToken(snapshot.accessTokenExpiresAt)) {
return (await refreshStoredAuthSession(snapshot.refreshToken)).accessToken
}
if (snapshot.accessToken) {
return snapshot.accessToken
}
if (snapshot.refreshToken) {
return (await refreshStoredAuthSession(snapshot.refreshToken)).accessToken
}
expireStoredSession()
throw authRequiredError()
}
export const apiClient = createApiClient({
baseURL,
auth: {
getAccessToken: getStoredAccessToken,
getRefreshToken: getStoredRefreshToken,
setTokens: setStoredTokens,
clearTokens: expireStoredSession,
refresh: refreshStoredAuthSession,
},
})
const currentBrandScopedPathPatterns = [
/^\/api\/tenant\/workspace\/(?:overview|recent-articles)(?:\/|$)/,
/^\/api\/tenant\/articles(?:\/|$)/,
/^\/api\/tenant\/templates\/[^/]+\/(?:drafts|generate)(?:\/|$)/,
/^\/api\/tenant\/kol\/subscription-prompts\/[^/]+\/generate(?:\/|$)/,
/^\/api\/tenant\/schedules(?:\/|$)/,
/^\/api\/tenant\/instant-tasks(?:\/|$)/,
/^\/api\/tenant\/publish-jobs(?:\/|$)/,
/^\/api\/tenant\/publish-records(?:\/|$)/,
/^\/api\/tenant\/publish-tasks\/[^/]+\/retry(?:\/|$)/,
/^\/api\/tenant\/enterprise-sites\/[^/]+\/publish(?:\/|$)/,
/^\/api\/tenant\/media-supply\/orders(?:\/|$)/,
/^\/api\/tenant\/prompt-rules(?:\/|$)/,
]
function shouldAttachCurrentBrandHeader(url: unknown): boolean {
if (typeof url !== 'string') {
return false
}
const pathname = normalizeRequestPath(url)
return currentBrandScopedPathPatterns.some((pattern) => pattern.test(pathname))
}
function normalizeRequestPath(url: string): string {
if (/^https?:\/\//i.test(url)) {
try {
return new URL(url).pathname
} catch {
return url
}
}
return url.split('?')[0] || url
}
function buildCurrentBrandHeaders(path: string): Headers | undefined {
if (!shouldAttachCurrentBrandHeader(path)) {
return undefined
}
const brandId = readStoredCurrentBrandId()
if (!brandId) {
return undefined
}
const headers = new Headers()
headers.set('X-Brand-ID', String(brandId))
return headers
}
apiClient.raw.interceptors.request.use((config) => {
if (!shouldAttachCurrentBrandHeader(config.url)) {
return config
}
const brandId = readStoredCurrentBrandId()
if (!brandId) {
return config
}
config.headers = config.headers ?? {}
;(config.headers as Record<string, string>)['X-Brand-ID'] = String(brandId)
return config
})
const membershipBlockedErrors = new Set([
'user_disabled',
'trial_plan_expired',
'subscription_required',
'subscription_inactive',
])
apiClient.raw.interceptors.response.use(
(response) => response,
(error) => {
if (error instanceof ApiClientError && membershipBlockedErrors.has(error.message)) {
markStoredMembershipBlocked(error.message)
}
if (error instanceof ApiClientError && error.message === 'ai_points_insufficient') {
void import('./errors').then(({ showAiPointsInsufficientModal }) => {
showAiPointsInsufficientModal()
})
}
return Promise.reject(error)
},
)
export const authApi = {
passwordPublicKey() {
return publicClient.get<PasswordPublicKeyResponse>('/api/auth/password-key')
},
login(payload: LoginRequest) {
return publicClient.post<LoginResponse, LoginRequest>('/api/auth/login', payload)
},
refresh: refreshStoredAuthSession,
me() {
return apiClient.get<UserInfo>('/api/auth/me')
},
changePassword(payload: ChangePasswordRequest) {
return apiClient.post<{ ok: boolean }, ChangePasswordRequest>('/api/auth/password', payload)
},
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')
},
aiPointUsage(params?: { page?: number; page_size?: number; limit?: number; offset?: number }) {
return apiClient.get<AIPointUsageListResponse>('/api/tenant/workspace/ai-point-usage', {
params,
})
},
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`,
{},
)
},
selfSubscribePackage(id: number) {
return apiClient.post<KolPackageSummary, Record<string, never>>(
`/api/tenant/kol/manage/packages/${id}/self-subscription`,
{},
)
},
cancelSelfSubscription(id: number) {
return apiClient.remove<KolPackageSummary>(
`/api/tenant/kol/manage/packages/${id}/self-subscription`,
)
},
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)
},
generateImitation(payload: GenerateImitationRequest) {
return apiClient.post<GenerateImitationResponse, GenerateImitationRequest>(
'/api/tenant/articles/imitations/generate',
payload,
)
},
regenerate(id: number) {
return apiClient.post<RegenerateArticleResponse, null>(
`/api/tenant/articles/${id}/regenerate`,
null,
)
},
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}`)
},
retryItem(id: number) {
return apiClient.post<KnowledgeItem, null>(`/api/tenant/knowledge/items/retry/${id}`, null)
},
}
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 requestHeaders = new Headers(init.headers)
requestHeaders.set('Accept', 'text/event-stream')
requestHeaders.set('Authorization', `Bearer ${await getFreshStoredAccessToken()}`)
const currentBrandHeaders = buildCurrentBrandHeaders(path)
currentBrandHeaders?.forEach((value, key) => requestHeaders.set(key, value))
let response = await fetch(`${baseURL}${path}`, {
...init,
headers: requestHeaders,
signal,
})
if (response.status === 401) {
try {
const tokens = await refreshStoredAuthSession()
requestHeaders.set('Authorization', `Bearer ${tokens.accessToken}`)
response = await fetch(`${baseURL}${path}`, {
...init,
headers: requestHeaders,
signal,
})
} catch (error) {
if (isAuthSessionExpiredError(error)) {
expireStoredSession()
}
throw markAuthErrorHandled(error)
}
}
if (!response.ok) {
const error = await buildSSERequestError(response)
if (response.status === 401) {
expireStoredSession()
}
throw error
}
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,
handled: response.status === 401,
})
}
} 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}`,
handled: response.status === 401,
})
}
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,
params?: { keyword_id?: number | null; page?: number; page_size?: number; q?: string },
) {
return apiClient.get<QuestionListResponse>(`/api/tenant/brands/${brandId}/questions`, {
params,
})
},
previewQuestionCombination(brandId: number, payload: QuestionCombinationRequest) {
return apiClient.post<QuestionCandidateResult, QuestionCombinationRequest>(
`/api/tenant/brands/${brandId}/questions/combination-preview`,
payload,
)
},
fillQuestionCombination(brandId: number, payload: QuestionCombinationFillRequest) {
return apiClient.post<QuestionCombinationFillResult, QuestionCombinationFillRequest>(
`/api/tenant/brands/${brandId}/questions/combination-fill`,
payload,
{ timeout: aiDistillRequestTimeoutMs },
)
},
distillQuestions(brandId: number, payload: QuestionDistillRequest) {
return apiClient.post<QuestionCandidateResult, QuestionDistillRequest>(
`/api/tenant/brands/${brandId}/questions/ai-distill`,
payload,
{ timeout: aiDistillRequestTimeoutMs },
)
},
classifyQuestions(brandId: number, payload: ClassifyQuestionsRequest) {
return apiClient.post<ClassifiedQuestion[], ClassifyQuestionsRequest>(
`/api/tenant/brands/${brandId}/questions/classify-metadata`,
payload,
)
},
materializeQuestions(brandId: number, payload: MaterializeQuestionsRequest) {
return apiClient.post<MaterializeQuestionsResult, MaterializeQuestionsRequest>(
`/api/tenant/brands/${brandId}/questions/materialize`,
payload,
)
},
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 mediaSupplyApi = {
listResources(params: ListSupplierMediaResourcesParams = {}) {
return apiClient.get<ListCustomerSupplierMediaResourcesResponse>(
'/api/tenant/media-supply/resources',
{
params,
},
)
},
listResourcesByIds(ids: number[], modelId?: number) {
return apiClient.get<ListCustomerSupplierMediaResourcesResponse>(
'/api/tenant/media-supply/resources/by-ids',
{ params: { ids: ids.join(','), model_id: modelId } },
)
},
searchOptions(modelId?: number) {
return apiClient.get<MediaSupplySearchOptionsResponse>(
'/api/tenant/media-supply/resources/search-options',
{ params: { model_id: modelId } },
)
},
createOrder(payload: CreateMediaSupplyOrderRequest) {
return apiClient.post<CreateMediaSupplyOrderResponse, CreateMediaSupplyOrderRequest>(
'/api/tenant/media-supply/orders',
payload,
)
},
listOrders(params: ListMediaSupplyOrdersParams = {}) {
return apiClient.get<ListMediaSupplyOrdersResponse>('/api/tenant/media-supply/orders', {
params,
})
},
getOrder(id: number) {
return apiClient.get<MediaSupplyOrderDetail>(`/api/tenant/media-supply/orders/${id}`)
},
wallet() {
return apiClient.get<MediaSupplyWalletStatus>('/api/tenant/media-supply/wallet')
},
listWalletLedgers(params: ListMediaSupplyWalletLedgersParams = {}) {
return apiClient.get<ListMediaSupplyWalletLedgersResponse>(
'/api/tenant/media-supply/wallet/ledgers',
{ params },
)
},
sessionStatus() {
return apiClient.get<MediaSupplySessionStatus>('/api/tenant/media-supply/supplier-session')
},
}
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 desktopClientApi = {
downloadableReleases(channel = 'stable') {
return apiClient.get<DesktopClientDownloadableReleaseListResponse>(
'/api/tenant/desktop-client/releases/downloadable',
{ params: { channel } },
)
},
publicLatest(params: { platform: string; arch: string; channel?: string; version?: string }) {
return publicClient.get<DesktopClientReleaseCheckResponse>('/api/desktop/releases/latest', {
params: {
platform: params.platform,
arch: params.arch,
channel: params.channel ?? 'stable',
version: params.version ?? '0.0.0',
},
})
},
publicDownloadURL(params: {
platform: string
arch: string
channel?: string
version?: string
}) {
return publicClient.get<DesktopClientReleaseDownloadURLResponse>(
'/api/desktop/releases/download-url',
{
params: {
platform: params.platform,
arch: params.arch,
channel: params.channel ?? 'stable',
version: params.version ?? '0.0.0',
},
},
)
},
async publicFallbackDownloadableReleases(): Promise<DesktopClientDownloadableReleaseListResponse> {
const targets = [
{ platform: 'win32', arch: 'x64' },
{ platform: 'darwin', arch: 'arm64' },
]
const results = await Promise.allSettled(
targets.map((target) => this.publicLatest({ ...target, channel: 'stable' })),
)
return {
items: results.flatMap((result, index) => {
if (result.status !== 'fulfilled') {
return []
}
const release = result.value
const target = targets[index]
const item: DesktopClientDownloadableRelease = {
id: index + 1,
platform: release.platform ?? target.platform,
arch: release.arch ?? target.arch,
channel: release.channel ?? 'stable',
version: release.latest_version,
download_url: release.download_url ?? null,
oss_object_key: release.oss_object_key ?? null,
file_name: release.file_name ?? null,
file_size_bytes: release.file_size_bytes ?? null,
sha256: release.sha256 ?? null,
release_notes: release.release_notes ?? null,
published_at: release.published_at ?? null,
updated_at: release.published_at ?? new Date(0).toISOString(),
force_update: release.force_update,
min_supported_version: release.min_supported_version,
download_source: release.download_source,
}
return [item]
}),
}
},
}
export const publishJobsApi = {
create(payload: CreatePublishJobRequest) {
return apiClient.post<CreatePublishJobResponse, CreatePublishJobRequest>(
'/api/tenant/publish-jobs',
payload,
)
},
}
export const publishTasksApi = {
list(params: ListDesktopPublishTasksParams = {}) {
return apiClient.get<TenantPublishTaskListResponse>('/api/tenant/publish-tasks', {
params,
})
},
retry(taskId: string) {
return apiClient.post<CreatePublishJobResponse>(
`/api/tenant/publish-tasks/${encodeURIComponent(taskId)}/retry`,
)
},
}
export const publishRecordsApi = {
list(params: ListPublishRecordsParams = {}) {
return apiClient.get<PublishRecordListResponse>('/api/tenant/publish-records', {
params,
})
},
}
export const enterpriseSitesApi = {
list() {
return apiClient.get<EnterpriseSiteConnection[]>('/api/tenant/enterprise-sites')
},
create(payload: CreateEnterpriseSiteConnectionRequest) {
return apiClient.post<EnterpriseSiteConnection, CreateEnterpriseSiteConnectionRequest>(
'/api/tenant/enterprise-sites',
payload,
)
},
update(id: number, payload: UpdateEnterpriseSiteConnectionRequest) {
return apiClient.patch<EnterpriseSiteConnection, UpdateEnterpriseSiteConnectionRequest>(
`/api/tenant/enterprise-sites/${id}`,
payload,
)
},
remove(id: number) {
return apiClient.remove<null>(`/api/tenant/enterprise-sites/${id}`)
},
ping(id: number) {
return apiClient.post<EnterpriseSiteCapability>(`/api/tenant/enterprise-sites/${id}/ping`)
},
categories(id: number) {
return apiClient.get<EnterpriseSiteCategory[]>(`/api/tenant/enterprise-sites/${id}/categories`)
},
syncCategories(id: number) {
return apiClient.post<EnterpriseSiteCategory[]>(
`/api/tenant/enterprise-sites/${id}/categories/sync`,
)
},
publish(id: number, payload: PublishEnterpriseSiteArticleRequest) {
return apiClient.post<EnterpriseSitePublishResponse, PublishEnterpriseSiteArticleRequest>(
`/api/tenant/enterprise-sites/${id}/publish`,
payload,
)
},
}
export const complianceApi = {
runtimeStatus() {
return apiClient.get<ComplianceRuntimeStatus>('/api/tenant/compliance/runtime-status')
},
checkPlain(payload: ComplianceCheckPlainRequest) {
return apiClient.post<ComplianceCheckResult, ComplianceCheckPlainRequest>(
'/api/tenant/compliance/check',
payload,
)
},
checkArticle(articleId: number, payload: ComplianceCheckArticleRequest) {
return apiClient.post<ComplianceCheckResult, ComplianceCheckArticleRequest>(
`/api/tenant/articles/${articleId}/compliance/check`,
payload,
)
},
latestArticleCheck(articleId: number) {
return apiClient.get<ComplianceCheckResult>(
`/api/tenant/articles/${articleId}/compliance/latest`,
)
},
createAck(recordId: number, payload: CreateAckRequest) {
return apiClient.post<ComplianceAckRecord, CreateAckRequest>(
`/api/tenant/compliance/check-records/${recordId}/ack`,
payload,
)
},
submitManualReview(articleId: number, payload: { check_record_id: number; note: string }) {
return apiClient.post<ComplianceManualReview, typeof payload>(
`/api/tenant/articles/${articleId}/compliance/manual-reviews`,
payload,
)
},
latestManualReview(articleId: number, articleVersionId?: number | null) {
return apiClient.get<ComplianceManualReview>(
`/api/tenant/articles/${articleId}/compliance/manual-reviews/latest`,
{
params: articleVersionId ? { article_version_id: articleVersionId } : undefined,
},
)
},
cancelManualReview(articleId: number, reviewId: number, payload: { reason?: string }) {
return apiClient.post<ComplianceManualReview, typeof payload>(
`/api/tenant/articles/${articleId}/compliance/manual-reviews/${reviewId}/cancel`,
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
question_id?: number | null
days?: number
business_date?: string
ai_platform_id?: string
hot_questions_page?: number
hot_questions_page_size?: number
hot_questions_q?: string
}) {
return apiClient.get<MonitoringDashboardCompositeResponse>(
'/api/tenant/monitoring/dashboard/composite',
{
params,
},
)
},
citationSummary(params: {
days?: number
brand_id?: number
keyword_id?: number | null
question_id?: number | null
business_date?: string
ai_platform_id?: string
cited_articles_page?: number
cited_articles_page_size?: number
}) {
return apiClient.get<MonitoringCitationSummaryResponse>(
'/api/tenant/monitoring/citation-summary',
{
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; question_id?: number | null; platform_ids?: string[] },
) {
return apiClient.post<
MonitoringCollectNowResponse,
{ keyword_id?: number | null; question_id?: number | null; platform_ids?: string[] }
>(`/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
}