Download web/src/lib/api.ts from zlcy/li: direct link, hf CLI and curl.
- Browser
- Download file 104 kB
-
https://huggingface.co/spaces/zlcy/li/resolve/main/web/src/lib/api.ts
- Command line
-
hf download hf://spaces/zlcy/li/web/src/lib/api.ts
-
curl -L -o api.ts https://huggingface.co/spaces/zlcy/li/resolve/main/web/src/lib/api.ts
104 kB
| import { httpRequest, request } from "@/lib/request"; | |
| import webConfig from "@/constants/common-env"; | |
| import { getStoredAuthKey } from "@/store/auth"; | |
| export type AccountType = string; | |
| export type AccountStatus = "正常" | "限流" | "异常" | "禁用"; | |
| export type ImageModel = "gpt-image-2" | "codex-gpt-image-2" | string; | |
| export type AuthRole = "admin" | "user" | "normal"; | |
| export type InstalledSkill = { | |
| id: string; | |
| name: string; | |
| description: string; | |
| source: string; | |
| installed_at: string; | |
| file_count: number; | |
| content?: string; | |
| scripts_allowed?: boolean; | |
| allowed_scripts?: string[]; | |
| enabled?: boolean; | |
| favorited?: boolean; | |
| guide_version?: string; | |
| presentation_summary?: SkillPresentationSummary; | |
| }; | |
| export type SkillAsset = { | |
| path: string; | |
| name: string; | |
| description: string; | |
| type: string; | |
| kind: "image" | "video" | "audio" | "html" | "pdf" | "file"; | |
| size: number; | |
| role: string; | |
| }; | |
| export type SkillExample = { | |
| id: string; | |
| title: string; | |
| description: string; | |
| prompt: string; | |
| assets: string[]; | |
| attachment_hint: string; | |
| }; | |
| export type SkillInputSpec = { | |
| id: string; | |
| label: string; | |
| type: string; | |
| required: boolean; | |
| description: string; | |
| }; | |
| export type SkillOutputSpec = SkillInputSpec; | |
| export type SkillWorkflowStep = { | |
| id: string; | |
| title: string; | |
| description: string; | |
| capability: string; | |
| }; | |
| export type SkillOrigin = { | |
| type: "github" | "url" | "upload" | "local" | string; | |
| label: string; | |
| url: string; | |
| author: string; | |
| version: string; | |
| open_source: boolean; | |
| }; | |
| export type SkillPresentationSummary = { | |
| version: number; | |
| title: string; | |
| description: string; | |
| category: string; | |
| tags: string[]; | |
| capabilities: string[]; | |
| origin: SkillOrigin; | |
| cover: SkillAsset | null; | |
| asset_count: number; | |
| example_count: number; | |
| input_count: number; | |
| output_count: number; | |
| workflow_count: number; | |
| }; | |
| export type SkillPresentation = SkillPresentationSummary & { | |
| assets: SkillAsset[]; | |
| examples: SkillExample[]; | |
| inputs: SkillInputSpec[]; | |
| outputs: SkillOutputSpec[]; | |
| workflow: { summary: string; steps: SkillWorkflowStep[] }; | |
| manifest: { present: boolean; path: string; overridden?: boolean; override_path?: string }; | |
| }; | |
| export type SkillPresentationUpdate = { | |
| display?: { | |
| title?: string; | |
| description?: string; | |
| category?: string; | |
| cover?: string; | |
| tags?: string[]; | |
| }; | |
| assets?: Array<Pick<SkillAsset, "path" | "name" | "description" | "role">>; | |
| examples?: SkillExample[]; | |
| inputs?: SkillInputSpec[]; | |
| outputs?: SkillOutputSpec[]; | |
| capabilities?: string[]; | |
| tags?: string[]; | |
| }; | |
| export type SkillResource = { | |
| path: string; | |
| size: number; | |
| type: string; | |
| readable: boolean; | |
| }; | |
| export type SkillScript = SkillResource & { | |
| kind: "python" | "node"; | |
| allowed: boolean; | |
| }; | |
| export type SkillRuntime = { | |
| id: string; | |
| name: string; | |
| description: string; | |
| enabled?: boolean; | |
| content: string; | |
| resources: SkillResource[]; | |
| scripts: SkillScript[]; | |
| permissions: { | |
| allow_scripts: boolean; | |
| allowed_scripts: string[]; | |
| timeout_seconds: number; | |
| }; | |
| presentation: SkillPresentation; | |
| }; | |
| export type SkillScriptArtifact = { | |
| name: string; | |
| type: string; | |
| size: number; | |
| url: string; | |
| }; | |
| export type SkillScriptResult = { | |
| run_id: string; | |
| skill_id: string; | |
| script: string; | |
| status: "success" | "error"; | |
| exit_code: number; | |
| duration_ms: number; | |
| stdout: string; | |
| stderr: string; | |
| artifacts: SkillScriptArtifact[]; | |
| }; | |
| type SkillCatalogResponse = { items: InstalledSkill[]; favorite_ids: string[] }; | |
| let cachedSkillCatalog: SkillCatalogResponse | null = null; | |
| let cachedSkillCatalogEtag = ""; | |
| export async function fetchAvailableSkills(): Promise<SkillCatalogResponse> { | |
| const response = await request.get<SkillCatalogResponse>("/api/skills", { | |
| headers: cachedSkillCatalogEtag ? {"If-None-Match": cachedSkillCatalogEtag} : undefined, | |
| validateStatus: (status) => (status >= 200 && status < 300) || status === 304, | |
| }); | |
| const etag = String(response.headers.etag || ""); | |
| // The server folds the favorite list into the ETag, so a 304 always means the | |
| // cached copy is still correct for this account. | |
| if (response.status === 304 && cachedSkillCatalog) { | |
| if (etag) cachedSkillCatalogEtag = etag; | |
| return cachedSkillCatalog; | |
| } | |
| cachedSkillCatalog = response.data; | |
| cachedSkillCatalogEtag = etag; | |
| return response.data; | |
| } | |
| export async function updateSkillFavorite(skillId: string, favorite: boolean) { | |
| return httpRequest<{ skill_id: string; favorite: boolean; ids: string[] }>( | |
| `/api/skills/${encodeURIComponent(skillId)}/favorite`, | |
| { method: "PUT", body: { favorite } }, | |
| ); | |
| } | |
| export async function fetchSkillRuntime(skillId: string) { | |
| return httpRequest<SkillRuntime>(`/api/skills/${encodeURIComponent(skillId)}`); | |
| } | |
| export type SkillRuntimeFetchResult = | |
| | { notModified: true; etag: string } | |
| | { notModified: false; etag: string; runtime: SkillRuntime }; | |
| /** | |
| * Conditional variant used by the detail dialog. The endpoint derives its ETag | |
| * from the Skill directory, so an unchanged Skill answers with an empty 304 | |
| * instead of re-sending SKILL.md and the full presentation. | |
| */ | |
| export async function fetchSkillRuntimeConditional(skillId: string, etag = ""): Promise<SkillRuntimeFetchResult> { | |
| const response = await request.get<SkillRuntime>(`/api/skills/${encodeURIComponent(skillId)}`, { | |
| headers: etag ? {"If-None-Match": etag} : undefined, | |
| validateStatus: (status) => (status >= 200 && status < 300) || status === 304, | |
| }); | |
| const nextEtag = String(response.headers.etag || "") || etag; | |
| if (response.status === 304) return {notModified: true, etag: nextEtag}; | |
| return {notModified: false, etag: nextEtag, runtime: response.data}; | |
| } | |
| export async function fetchSkillUserGuide(skillId: string) { | |
| return httpRequest<{ guide: string; version: string; cached: boolean }>( | |
| `/api/skills/${encodeURIComponent(skillId)}/guide`, | |
| ); | |
| } | |
| export async function readSkillResource(skillId: string, path: string) { | |
| const query = new URLSearchParams({ path }); | |
| return httpRequest<{ skill_id: string; path: string; content: string; size: number }>( | |
| `/api/skills/${encodeURIComponent(skillId)}/resource?${query.toString()}`, | |
| ); | |
| } | |
| export async function fetchSkillAsset(skillId: string, path: string, signal?: AbortSignal) { | |
| const query = new URLSearchParams({ path }); | |
| const response = await request.get<Blob>(`/api/skills/${encodeURIComponent(skillId)}/asset?${query.toString()}`, { | |
| responseType: "blob", | |
| signal, | |
| }); | |
| return response.data; | |
| } | |
| export function skillAssetUrl(skillId: string, path: string, interactive = false) { | |
| const query = new URLSearchParams({ path }); | |
| if (interactive) query.set("interactive", "1"); | |
| const apiBase = webConfig.apiUrl.replace(/\/$/, ""); | |
| return `${apiBase}/api/skills/${encodeURIComponent(skillId)}/asset?${query.toString()}`; | |
| } | |
| export async function runSkillScript(skillId: string, script: string, args: string[], files: File[] = [], clientRunId = "") { | |
| const form = new FormData(); | |
| form.append("script", script); | |
| form.append("args", JSON.stringify(args)); | |
| if (clientRunId) form.append("client_run_id", clientRunId); | |
| files.forEach((file) => form.append("file", file)); | |
| return httpRequest<SkillScriptResult>(`/api/skills/${encodeURIComponent(skillId)}/run`, { | |
| method: "POST", | |
| body: form, | |
| }); | |
| } | |
| export async function updateSkillPermissions( | |
| skillId: string, | |
| value: { allow_scripts: boolean; allowed_scripts: string[]; timeout_seconds?: number }, | |
| ) { | |
| return httpRequest<{ skill: SkillRuntime; items: InstalledSkill[] }>( | |
| `/api/admin/skills/${encodeURIComponent(skillId)}/permissions`, | |
| { method: "PUT", body: value }, | |
| ); | |
| } | |
| export async function updateSkillVisibility(skillId: string, enabled: boolean) { | |
| return httpRequest<{ skill: SkillRuntime; items: InstalledSkill[] }>( | |
| `/api/admin/skills/${encodeURIComponent(skillId)}/visibility`, | |
| { method: "PUT", body: { enabled } }, | |
| ); | |
| } | |
| export async function fetchAdminSkillRuntime(skillId: string) { | |
| return httpRequest<SkillRuntime>(`/api/admin/skills/${encodeURIComponent(skillId)}`); | |
| } | |
| export async function reindexSkillPresentation(skillId: string) { | |
| return httpRequest<{ skill: SkillRuntime; items: InstalledSkill[] }>( | |
| `/api/admin/skills/${encodeURIComponent(skillId)}/reindex`, | |
| { method: "POST" }, | |
| ); | |
| } | |
| export async function updateSkillPresentation(skillId: string, value: SkillPresentationUpdate) { | |
| return httpRequest<{ skill: SkillRuntime; items: InstalledSkill[] }>( | |
| `/api/admin/skills/${encodeURIComponent(skillId)}/presentation`, | |
| { method: "PUT", body: value }, | |
| ); | |
| } | |
| export async function uploadSkillPresentationAssets(skillId: string, files: File[]) { | |
| const form = new FormData(); | |
| files.forEach((file) => form.append("files", file)); | |
| return httpRequest<{ assets: SkillAsset[] }>( | |
| `/api/admin/skills/${encodeURIComponent(skillId)}/presentation/assets`, | |
| { method: "POST", body: form }, | |
| ); | |
| } | |
| export async function generateSkillExamples(skillId: string) { | |
| return httpRequest<{ skill: SkillRuntime; items: InstalledSkill[] }>( | |
| `/api/admin/skills/${encodeURIComponent(skillId)}/presentation/generate-examples`, | |
| { method: "POST" }, | |
| ); | |
| } | |
| export async function fetchInstalledSkills() { | |
| return httpRequest<{ items: InstalledSkill[] }>("/api/admin/skills"); | |
| } | |
| export async function installSkillFromUrl(url: string, overwrite = false) { | |
| return httpRequest<{ installed: InstalledSkill[]; items: InstalledSkill[] }>("/api/admin/skills/install-url", { | |
| method: "POST", | |
| body: { url, overwrite }, | |
| }); | |
| } | |
| export async function installSkillFromInstruction(instruction: string, overwrite = false) { | |
| return httpRequest<{ installed: InstalledSkill[]; resolved_url: string; instruction_urls: string[]; already_installed?: boolean; items: InstalledSkill[] }>("/api/admin/skills/install-instruction", { | |
| method: "POST", | |
| body: { instruction, overwrite }, | |
| }); | |
| } | |
| export async function installSkillFromZip(file: File, overwrite = false) { | |
| const form = new FormData(); | |
| form.append("file", file); | |
| form.append("overwrite", String(overwrite)); | |
| return httpRequest<{ installed: InstalledSkill[]; items: InstalledSkill[] }>("/api/admin/skills/install-zip", { | |
| method: "POST", | |
| body: form, | |
| }); | |
| } | |
| export async function deleteInstalledSkill(skillId: string) { | |
| return httpRequest<{ ok: boolean; items: InstalledSkill[] }>(`/api/admin/skills/${encodeURIComponent(skillId)}`, { | |
| method: "DELETE", | |
| }); | |
| } | |
| /** Delete several Skills in one call; unknown ids come back in `missing`. */ | |
| export async function deleteInstalledSkills(ids: string[]) { | |
| return httpRequest<{ deleted: string[]; missing: string[]; items: InstalledSkill[] }>( | |
| "/api/admin/skills/batch-delete", | |
| { method: "POST", body: { ids } }, | |
| ); | |
| } | |
| /** Reinstall a Skill from the source recorded in its .install.json. */ | |
| export async function updateInstalledSkill(skillId: string) { | |
| return httpRequest<{ installed: InstalledSkill[]; items: InstalledSkill[] }>( | |
| `/api/admin/skills/${encodeURIComponent(skillId)}/update`, | |
| { method: "POST" }, | |
| ); | |
| } | |
| /** Import an export bundle. Uses the administrator path, which allows far larger archives. */ | |
| export async function importSkillsArchive(file: File, overwrite = false) { | |
| const form = new FormData(); | |
| form.append("file", file); | |
| form.append("overwrite", String(overwrite)); | |
| return httpRequest<{ installed: InstalledSkill[]; items: InstalledSkill[] }>("/api/admin/skills/import-zip", { | |
| method: "POST", | |
| body: form, | |
| }); | |
| } | |
| /** | |
| * Download an export bundle. The endpoint streams a ZIP, so the payload is read | |
| * as a blob and handed to the browser; the server decides the file name. | |
| */ | |
| export async function exportSkillsArchive(ids: string[] = []) { | |
| const response = await request.get<Blob>("/api/admin/skills/export", { | |
| params: ids.length ? { ids: ids.join(",") } : undefined, | |
| responseType: "blob", | |
| }); | |
| const disposition = String(response.headers["content-disposition"] || ""); | |
| const match = /filename="?([^";]+)"?/i.exec(disposition); | |
| return { blob: response.data as Blob, filename: match?.[1] || "skills-export.zip" }; | |
| } | |
| export type Account = { | |
| access_token: string; | |
| type: AccountType; | |
| export_type?: string | null; | |
| status: AccountStatus; | |
| quota: number; | |
| image_quota_unknown?: boolean; | |
| email?: string | null; | |
| expired?: string | null; | |
| id_token?: string | null; | |
| account_id?: string | null; | |
| last_refresh?: string | null; | |
| refresh_token?: string | null; | |
| user_id?: string | null; | |
| limits_progress?: Array<{ | |
| feature_name?: string; | |
| remaining?: number; | |
| reset_after?: string; | |
| }>; | |
| default_model_slug?: string | null; | |
| restore_at?: string | null; | |
| success: number; | |
| fail: number; | |
| last_used_at?: string | null; | |
| owner_id?: string; | |
| owner_role?: string; | |
| owner_name?: string; | |
| shared?: boolean; | |
| shared_cleanup_failures?: number; | |
| consecutive_refresh_401_failures?: number; | |
| last_refresh_error?: string | null; | |
| last_refresh_error_at?: string | null; | |
| image_inflight?: number; | |
| has_password?: boolean; | |
| }; | |
| export type Model = { | |
| id: string; | |
| object?: string; | |
| owned_by?: string; | |
| }; | |
| export type ChatModel = Model & { | |
| title?: string; | |
| is_work_mode_model?: boolean; | |
| reasoning_type?: string; | |
| enabled_tools?: string[]; | |
| /** 当前是否会出现在对话页的模型下拉里(自动规则叠加人工覆盖)。 */ | |
| chat_enabled?: boolean; | |
| /** 不带任何人工覆盖时,自动规则给出的结果。 */ | |
| chat_default_enabled?: boolean; | |
| /** `upstream` = 账号池上游模型;`third_party` = 第三方接入模型。 */ | |
| chat_source?: "upstream" | "third_party"; | |
| /** 是否生图模型(按能力字段判定,其次看模型名)。 */ | |
| chat_is_image?: boolean; | |
| /** 默认隐藏的原因:`image_model` 生图模型 / `tool_model` 工具类伪模型;空串表示默认可聊。 */ | |
| chat_default_reason?: "" | "image_model" | "tool_model"; | |
| /** 第三方接入模型所属分组名。 */ | |
| group_name?: string; | |
| capabilities?: string[]; | |
| }; | |
| /** | |
| * 「生图可用模型」列表项(来自 `/api/image/models`)。 | |
| * | |
| * 注意不要与上面的 `ImageModel`(生图模型 id 字符串)混淆。 | |
| */ | |
| export type ImageModelOption = Model & { | |
| /** 当前是否会出现在生图模型下拉里。 */ | |
| image_enabled?: boolean; | |
| /** 不带任何人工覆盖时的默认值(生图侧一律为 true:默认全部可用)。 */ | |
| image_default_enabled?: boolean; | |
| /** `builtin` = 账号池上游内置生图模型;`third_party` = 第三方接入生图模型。 */ | |
| image_source?: "builtin" | "third_party"; | |
| /** 该模型声明的生图能力(`txt2img` / `img2img`)。 */ | |
| image_capabilities?: string[]; | |
| /** 第三方接入模型所属分组(内置模型没有该字段)。 */ | |
| group_id?: string; | |
| group_name?: string; | |
| capabilities?: string[]; | |
| }; | |
| export type ModelListResponse = { | |
| object: "list" | string; | |
| data: Model[]; | |
| }; | |
| /** | |
| * 「系统可用模型」列表项(来自 `/api/system/models`)。 | |
| * | |
| * 与 `/v1/models` 同一份全量目录(上游账号池 + 第三方接入), | |
| * 额外带 `system_enabled` —— 只影响这张列表显示哪些模型,不影响对外 API。 | |
| */ | |
| export type SystemModel = Model & { | |
| /** 当前是否出现在管理页的「系统可用模型」列表里。 */ | |
| system_enabled?: boolean; | |
| /** `upstream` = 账号池上游模型;`third_party` = 第三方接入模型。 */ | |
| system_source?: "upstream" | "third_party"; | |
| title?: string; | |
| group_id?: string; | |
| group_name?: string; | |
| capabilities?: string[]; | |
| }; | |
| /** 模型下拉的人工覆盖项(对话与生图共用同一套结构)。 */ | |
| export type ModelOverrides = { | |
| include: string[]; | |
| exclude: string[]; | |
| }; | |
| export type ChatModelOverrides = ModelOverrides; | |
| export type ChatModelListResponse = { | |
| object: "list" | string; | |
| data: ChatModel[]; | |
| include: string[]; | |
| exclude: string[]; | |
| }; | |
| export type ImageModelListResponse = { | |
| object: "list" | string; | |
| data: ImageModelOption[]; | |
| include: string[]; | |
| exclude: string[]; | |
| }; | |
| export type SystemModelListResponse = { | |
| object: "list" | string; | |
| data: SystemModel[]; | |
| include: string[]; | |
| exclude: string[]; | |
| }; | |
| type AccountListResponse = { | |
| items: Account[]; | |
| }; | |
| type AccountMutationResponse = { | |
| items: Account[]; | |
| added?: number; | |
| skipped?: number; | |
| removed?: number; | |
| refreshed?: number; | |
| errors?: Array<{ access_token: string; error: string }>; | |
| }; | |
| export type AccountRefreshResponse = { | |
| items: Account[]; | |
| refreshed: number; | |
| errors: Array<{ access_token: string; error: string }>; | |
| }; | |
| export type UpstreamAccountActionOptions = { | |
| disable_memory?: boolean; | |
| hide_conversations?: boolean; | |
| delete_files?: boolean; | |
| file_page_limit?: number; | |
| }; | |
| export type UpstreamAccountActionName = | |
| | "disable_memory" | |
| | "hide_conversations" | |
| | "delete_files"; | |
| export type UpstreamAccountActionExecution = { | |
| success: boolean; | |
| result?: unknown; | |
| error?: string; | |
| }; | |
| export type UpstreamAccountActionResult = { | |
| account_id: string; | |
| token_preview?: string; | |
| success: boolean; | |
| status: "success" | "error" | string; | |
| message?: string; | |
| error?: string; | |
| duration_ms?: number; | |
| actions?: Partial<Record<UpstreamAccountActionName, UpstreamAccountActionExecution>>; | |
| }; | |
| export type UpstreamAccountActionResponse = { | |
| total: number; | |
| succeeded: number; | |
| failed: number; | |
| duration_ms?: number; | |
| errors: Array<{ | |
| account_id?: string; | |
| token_preview?: string; | |
| action?: UpstreamAccountActionName; | |
| error: string; | |
| }>; | |
| results: UpstreamAccountActionResult[]; | |
| }; | |
| export type AccountUpdateResponse = { | |
| item: Account; | |
| items: Account[]; | |
| }; | |
| export type AccountPasswordReloginPendingResponse = { | |
| status: "pending_code"; | |
| relogin_id: string; | |
| email: string; | |
| message: string; | |
| }; | |
| export type AccountPasswordReloginCompletedResponse = AccountUpdateResponse & { | |
| status: "completed"; | |
| }; | |
| export type AccountPasswordReloginResponse = | |
| | AccountPasswordReloginPendingResponse | |
| | AccountPasswordReloginCompletedResponse; | |
| export type AccountEmailLoginImportResponse = { | |
| status: "pending_code" | "completed"; | |
| session_id?: string; | |
| email: string; | |
| message: string; | |
| created_account?: boolean; | |
| generated_password?: string; | |
| result?: AccountMutationResponse; | |
| }; | |
| export type AccountImportPayload = { | |
| access_token: string; | |
| accessToken?: string; | |
| type?: string; | |
| export_type?: string; | |
| email?: string; | |
| expired?: string; | |
| id_token?: string; | |
| account_id?: string; | |
| last_refresh?: string; | |
| refresh_token?: string; | |
| password?: string; | |
| [key: string]: unknown; | |
| }; | |
| export type AccountExportFormat = "json" | "zip"; | |
| export type FreeAccountCleanupAction = "mark_abnormal" | "delete"; | |
| export type FreeAccountCleanupSettings = { | |
| enabled: boolean; | |
| interval_minutes: number | string; | |
| failure_threshold: number | string; | |
| register_precheck_enabled: boolean; | |
| action: FreeAccountCleanupAction; | |
| }; | |
| export type RedeemCodeCleanupSettings = { | |
| enabled: boolean; | |
| interval_minutes: number | string; | |
| delete_expired: boolean; | |
| delete_disabled: boolean; | |
| archive_redeemed: boolean; | |
| redeemed_older_than_days: number | string; | |
| limit: number | string; | |
| }; | |
| export type NavigationItem = { | |
| id: string; | |
| title: string; | |
| url: string; | |
| new_window: boolean; | |
| append_api_credentials: boolean; | |
| api_key_param: string; | |
| base_url_param: string; | |
| enabled: boolean; | |
| children: NavigationItem[]; | |
| }; | |
| export type AccountRefreshProgress = { | |
| progress_id: string; | |
| total: number; | |
| processed: number; | |
| refreshed: number; | |
| failed: number; | |
| done: boolean; | |
| error?: string | null; | |
| result?: AccountRefreshResponse; | |
| }; | |
| export type ProxyRuntimeEgressMode = "direct" | "single_proxy"; | |
| export type ProxyRuntimeClearanceMode = "none" | "manual" | "flaresolverr"; | |
| export type ProxyRuntimeClearanceSettings = { | |
| enabled: boolean; | |
| mode: ProxyRuntimeClearanceMode; | |
| cf_cookies: string; | |
| cf_clearance: string; | |
| user_agent: string; | |
| browser: string; | |
| flaresolverr_url: string; | |
| timeout_sec: number | string; | |
| refresh_interval: number | string; | |
| warm_up_on_start: boolean; | |
| has_cf_cookies?: boolean; | |
| has_cf_clearance?: boolean; | |
| }; | |
| export type ProxyRuntimeSettings = { | |
| enabled: boolean; | |
| egress_mode: ProxyRuntimeEgressMode; | |
| proxy_url: string; | |
| resource_proxy_url: string; | |
| skip_ssl_verify: boolean; | |
| reset_session_status_codes: number[]; | |
| clearance: ProxyRuntimeClearanceSettings; | |
| }; | |
| export type ProxyRuntimeStatus = { | |
| enabled: boolean; | |
| egress_mode: ProxyRuntimeEgressMode | string; | |
| proxy_source: string; | |
| has_proxy: boolean; | |
| clearance_enabled: boolean; | |
| clearance_mode: ProxyRuntimeClearanceMode | string; | |
| has_clearance_bundle: boolean; | |
| cached_clearance_hosts: string[]; | |
| }; | |
| export type SettingsConfig = { | |
| proxy: string; | |
| base_url?: string; | |
| image_proxy_base_url?: string; | |
| global_system_prompt?: string; | |
| prompt_optimizer_system_prompt?: string; | |
| prompt_optimizer_provider?: "internal" | "external"; | |
| prompt_optimizer_base_url?: string; | |
| prompt_optimizer_api_key?: string; | |
| prompt_optimizer_model?: string; | |
| reverse_prompt_system_prompt?: string; | |
| reverse_prompt_provider?: "internal" | "external"; | |
| reverse_prompt_base_url?: string; | |
| reverse_prompt_api_key?: string; | |
| reverse_prompt_model?: string; | |
| product_visual_planner_system_prompt?: string; | |
| product_visual_agent_provider?: "internal" | "external"; | |
| product_visual_agent_base_url?: string; | |
| product_visual_agent_api_key?: string; | |
| product_visual_agent_model?: string; | |
| sensitive_words?: string[]; | |
| local_review?: { | |
| enabled?: boolean; | |
| strong_short_terms?: string[]; | |
| /** 命中词紧跟否定词(无 / 不 / 避免 / 禁止…)时不算违规。默认开启。 */ | |
| negation_aware?: boolean; | |
| /** 组合规则左右两侧命中词的最大间距(按归一化后字符计)。0 = 不限制。默认 120。 */ | |
| combination_window?: number; | |
| /** 内置跨语言违规词(英/日/韩/俄/德/法/西/越/泰/阿拉伯…)恒先生效。默认开启。 */ | |
| multilingual_aware?: boolean; | |
| /** 送审前先还原混淆写法:繁→简、URL/HTML/\\u 转义、Base64、稀字剔除、倒序词。默认开启。 */ | |
| deobfuscate?: boolean; | |
| categories?: Array<{ | |
| name: string; | |
| mode?: "block" | "context"; | |
| terms: string[]; | |
| }>; | |
| combination_rules?: Array<{ | |
| id: string; | |
| left_categories?: string[]; | |
| right_categories?: string[]; | |
| left_terms?: string[]; | |
| right_terms?: string[]; | |
| }>; | |
| }; | |
| ai_review?: { | |
| enabled?: boolean; | |
| base_url?: string; | |
| api_key?: string; | |
| model?: string; | |
| prompt?: string; | |
| fail_open?: boolean; | |
| }; | |
| thread_pool_tokens?: number | string; | |
| chat_models?: ChatModelOverrides; | |
| image_models?: ModelOverrides; | |
| system_models?: ModelOverrides; | |
| refresh_account_interval_minute?: number | string; | |
| image_retention_days?: number | string; | |
| image_poll_timeout_secs?: number | string; | |
| image_poll_max_attempts?: number | string; | |
| image_settle_enabled?: boolean; | |
| image_settle_secs?: number | string; | |
| image_account_concurrency?: number | string; | |
| image_memory_button_enabled?: boolean; | |
| image_parallel_generation?: boolean; | |
| image_prompt_aspect_hint?: boolean; | |
| image_remove_conversation_after_result?: boolean; | |
| image_remove_conversation_always?: boolean; | |
| image_share_evidence_enabled?: boolean; | |
| image_share_retention_hours?: number | string; | |
| image_timeout_retry_secs?: number | string; | |
| default_upstream_model_name?: string; | |
| default_thinking_effort?: "auto" | "standard" | "extended" | "max"; | |
| normal_user_default_credits?: number | string; | |
| normal_user_daily_credits?: number | string; | |
| normal_user_pool_pricing?: PoolPricingConfig; | |
| normal_user_third_party_image_pricing?: PoolPricingConfig; | |
| checkin_daily_credits?: number | string; | |
| /** 签到赠送积分的可用天数;0(默认)= 永久有效,>0 则在发放 N 天后过期作废。 */ | |
| checkin_credit_valid_days?: number | string; | |
| checkin_content?: Partial<CheckinContent>; | |
| /** 报错文案映射规则:命中关键词就把技术报错替换成大白话,只影响前台展示。 */ | |
| error_message_rules?: ErrorMessageRule[]; | |
| /** 未命中规则时是否使用兜底文案(关 = 原样显示技术报错)。 */ | |
| error_message_fallback_enabled?: boolean; | |
| /** 「AI 归纳」使用的模型;留空则用池子默认文本模型。 */ | |
| error_message_ai_model?: string; | |
| internal_image_model_daily_limits?: ImageModelDailyLimit[]; | |
| wechat_login_enabled?: boolean; | |
| wechat_auto_register_enabled?: boolean; | |
| internal_user_login_session_duration_hours?: number | string; | |
| normal_user_login_session_duration_hours?: number | string; | |
| login_session_duration_hours?: number | string; | |
| site_title?: string; | |
| site_logo_url?: string; | |
| site_header_title?: string; | |
| site_external_link_label?: string; | |
| site_external_link_url?: string; | |
| site_external_link_icon_url?: string; | |
| site_external_link_icon_svg?: string; | |
| shop_purchase_url?: string; | |
| auto_remove_invalid_accounts?: boolean; | |
| auto_remove_rate_limited_accounts?: boolean; | |
| free_account_cleanup?: FreeAccountCleanupSettings; | |
| redeem_code_cleanup?: RedeemCodeCleanupSettings; | |
| cleanup_shared_conversations?: boolean; | |
| log_levels?: string[]; | |
| backup?: BackupSettings; | |
| backup_state?: BackupState; | |
| image_storage?: ImageStorageSettings; | |
| proxy_runtime?: ProxyRuntimeSettings; | |
| third_party_apps?: { | |
| infinite_canvas: { enabled: boolean; url: string }; | |
| }; | |
| announcements?: Announcement[]; | |
| normal_user_navigation?: NavigationItem[]; | |
| internal_user_navigation?: NavigationItem[]; | |
| admin_user_navigation?: NavigationItem[]; | |
| [key: string]: unknown; | |
| }; | |
| export type AnnouncementType = "notice" | "timeline"; | |
| export type AnnouncementTarget = "internal" | "normal" | "all"; | |
| export type Announcement = { | |
| id: string; | |
| type: AnnouncementType; | |
| target: AnnouncementTarget; | |
| title: string; | |
| content: string; | |
| enabled: boolean; | |
| created_at: string; | |
| updated_at: string; | |
| }; | |
| export type ImageStorageMode = "local" | "webdav" | "both"; | |
| export type ImageStorageSettings = { | |
| enabled: boolean; | |
| mode: ImageStorageMode; | |
| webdav_url: string; | |
| webdav_username: string; | |
| webdav_password: string; | |
| webdav_root_path: string; | |
| public_base_url: string; | |
| }; | |
| export type BackupInclude = { | |
| config: boolean; | |
| register: boolean; | |
| cpa: boolean; | |
| sub2api: boolean; | |
| logs: boolean; | |
| image_tasks: boolean; | |
| accounts_snapshot: boolean; | |
| auth_keys_snapshot: boolean; | |
| images: boolean; | |
| }; | |
| export type BackupSettings = { | |
| enabled: boolean; | |
| provider: "cloudflare_r2" | string; | |
| account_id: string; | |
| access_key_id: string; | |
| secret_access_key: string; | |
| bucket: string; | |
| prefix: string; | |
| interval_minutes: number | string; | |
| rotation_keep: number | string; | |
| encrypt: boolean; | |
| passphrase: string; | |
| include: BackupInclude; | |
| }; | |
| export type BackupState = { | |
| running: boolean; | |
| last_started_at?: string | null; | |
| last_finished_at?: string | null; | |
| last_status?: string; | |
| last_error?: string | null; | |
| last_object_key?: string | null; | |
| }; | |
| export type BackupItem = { | |
| key: string; | |
| name: string; | |
| size: number; | |
| updated_at?: string | null; | |
| encrypted: boolean; | |
| }; | |
| export type BackupDetail = { | |
| key: string; | |
| name: string; | |
| encrypted: boolean; | |
| created_at?: string | null; | |
| trigger?: string | null; | |
| app_version?: string | null; | |
| storage_backend?: Record<string, unknown> | null; | |
| files: Array<{ | |
| name: string; | |
| exists: boolean; | |
| content_type?: string; | |
| size: number; | |
| sha256?: string; | |
| }>; | |
| snapshots: Array<{ | |
| name: string; | |
| count: number; | |
| }>; | |
| }; | |
| export type ManagedImage = { | |
| rel: string; | |
| path?: string; | |
| name: string; | |
| date: string; | |
| size: number; | |
| url: string; | |
| thumbnail_url?: string; | |
| created_at: string; | |
| storage?: "local" | "webdav" | "both" | string; | |
| local?: boolean; | |
| webdav?: boolean; | |
| width?: number; | |
| height?: number; | |
| tags?: string[]; | |
| }; | |
| export type SystemLog = { | |
| id: string; | |
| time: string; | |
| type: "call" | "account" | string; | |
| summary?: string; | |
| detail?: Record<string, unknown>; | |
| [key: string]: unknown; | |
| }; | |
| export type ImageResponse = { | |
| created: number; | |
| data: Array<{ b64_json?: string; url?: string; revised_prompt?: string }>; | |
| }; | |
| export type ImageTask = { | |
| id: string; | |
| status: "queued" | "running" | "success" | "error"; | |
| mode: "generate" | "edit"; | |
| model?: ImageModel; | |
| size?: string; | |
| prompt?: string; | |
| quality?: string; | |
| resolution?: string; | |
| created_at: string; | |
| started_at?: string | null; | |
| updated_at: string; | |
| data?: Array<{ b64_json?: string; url?: string; revised_prompt?: string }>; | |
| error?: string; | |
| progress?: string; | |
| progress_profile?: "pool" | "api" | string; | |
| progress_events?: Array<{ step?: string; at?: string; ts?: number }>; | |
| elapsed_secs?: number; | |
| duration_ms?: number; | |
| conversation_id?: string; | |
| /** 失败/超时时上游 ChatGPT 的最后一句话(后端从会话文档里取出来带回)。 */ | |
| upstream_note?: string; | |
| /** 上游已给终局结论(只回文字、本轮没生图)时为 false:别让用户白点「继续等待」。 */ | |
| can_continue_waiting?: boolean; | |
| /** 失败卡片上是否值得给「查看官网对话」入口。后端显式下发 false 时才为 false: | |
| * 账号生图配额耗尽(「你已达到 Free 套餐的图像生成请求上限」)时官网这轮什么都没生成, | |
| * 点进去只有同一句提示 → 收掉入口。老记录没有这个字段 = 保持原样(给入口)。 */ | |
| can_view_official?: boolean; | |
| }; | |
| export type PoolPricingDateType = "official_holiday" | "weekend" | "saturday" | "sunday" | "specific_dates" | "date_range" | "official_workday"; | |
| export type PoolPricingRule = { | |
| id: string; | |
| name: string; | |
| target_type: "pool" | "third_party_group"; | |
| third_party_group_id: string; | |
| enabled: boolean; | |
| pay_percentage: number; | |
| date_type: PoolPricingDateType; | |
| holiday_names: string[]; | |
| weekend_basis: "natural" | "official"; | |
| dates: string[]; | |
| start_date: string; | |
| end_date: string; | |
| all_day: boolean; | |
| start_time: string; | |
| end_time: string; | |
| }; | |
| export type PoolPricingConfig = { | |
| enabled: boolean; | |
| timezone: "Asia/Shanghai" | string; | |
| base_unit_price: number; | |
| calendar_refresh_hours: number; | |
| rules: PoolPricingRule[]; | |
| }; | |
| export type PoolPricingCalendarStatus = { | |
| years: number[]; | |
| updated_at: string; | |
| source: string; | |
| warning: string; | |
| }; | |
| export type PoolPricingQuote = { | |
| applies: boolean; | |
| target_type: "pool" | "third_party_group"; | |
| third_party_group_id: string; | |
| base_unit_price: number; | |
| pay_percentage: number; | |
| unit_price: number; | |
| quantity: number; | |
| estimated_total: number; | |
| rule_id: string; | |
| rule_name: string; | |
| conflicting_rule_ids: string[]; | |
| conflicting_rule_names: string[]; | |
| evaluated_at: string; | |
| calendar: PoolPricingCalendarStatus; | |
| }; | |
| export type PoolPricingValidation = { | |
| config: PoolPricingConfig; | |
| errors: Array<{ rule_id: string; message: string }>; | |
| warnings: Array<{ rule_id: string; message: string }>; | |
| valid: boolean; | |
| }; | |
| type ImageTaskListResponse = { | |
| items: ImageTask[]; | |
| missing_ids: string[]; | |
| }; | |
| export type ThirdPartyImageModelOption = { | |
| id: string; | |
| group_id: string; | |
| group_name?: string | null; | |
| connection_id?: string | null; | |
| connection_name?: string | null; | |
| alias?: string | null; | |
| upstream_model?: string | null; | |
| resolution?: "1k" | "2k" | "4k" | string; | |
| allow_4k?: boolean; | |
| default_quality?: "auto" | "low" | "medium" | "high" | "xhigh" | "max" | string; | |
| billing_mode?: "general_multiplier" | "dedicated_wallet" | "both" | string; | |
| credit_cost?: number; | |
| default_cost?: number; | |
| activity_rule_id?: string; | |
| activity_rule_name?: string; | |
| sizes?: string[]; | |
| capabilities?: Array<"txt2img" | "img2img" | string>; | |
| aspect_ratios?: string[]; | |
| aspect_ratio_sizes?: Record<string, string>; | |
| unsupported_img2img_behavior?: "hidden" | "disabled" | string; | |
| }; | |
| export type ThirdPartyImageConnection = { | |
| id: string; | |
| name: string; | |
| base_url: string; | |
| enabled: boolean; | |
| priority: number; | |
| weight: number; | |
| max_concurrency?: number; | |
| response_format?: "auto" | "source_url" | "url" | "source_b64_json" | "b64_json" | string; | |
| call_mode?: "openai" | "custom" | string; | |
| size_mode?: "auto" | "pixels" | "ratio" | string; | |
| adapter_template?: ThirdPartyImageAdapterTemplate | null; | |
| adapter_summary?: string | null; | |
| api_key?: string; | |
| api_key_masked?: string; | |
| has_api_key?: boolean; | |
| last_checked_at?: string | null; | |
| last_error?: string | null; | |
| }; | |
| export type ThirdPartyImageModelConfig = { | |
| id: string; | |
| connection_id?: string; | |
| upstream_model: string; | |
| alias: string; | |
| enabled: boolean; | |
| resolution?: "1k" | "2k" | "4k" | string; | |
| allow_4k: boolean; | |
| default_quality: "auto" | "low" | "medium" | "high" | "xhigh" | "max" | string; | |
| quality_map?: Record<string, string>; | |
| sizes?: string[]; | |
| capabilities?: Array<"txt2img" | "img2img" | string>; | |
| aspect_ratios?: string[]; | |
| aspect_ratio_sizes?: Record<string, string>; | |
| costs?: Record<string, number>; | |
| default_cost?: number; | |
| }; | |
| export type ThirdPartyImageGroup = { | |
| id: string; | |
| name: string; | |
| remark?: string; | |
| enabled: boolean; | |
| billing_mode: "general_multiplier" | "dedicated_wallet" | "both" | string; | |
| active_dedicated_model_ids?: string[]; | |
| has_active_dedicated_wallets?: boolean; | |
| multiplier_enabled: boolean; | |
| wallet_recharge_enabled: boolean; | |
| default_cost: number; | |
| connections: ThirdPartyImageConnection[]; | |
| models: ThirdPartyImageModelConfig[]; | |
| }; | |
| export type ThirdPartyImageWallet = { | |
| id?: string; | |
| group_id?: string; | |
| model_id?: string; | |
| remaining?: number; | |
| reserved?: number; | |
| used?: number; | |
| total_recharged?: number; | |
| }; | |
| export type LoginResponse = { | |
| ok: boolean; | |
| version: string; | |
| role: AuthRole; | |
| subject_id: string; | |
| name: string; | |
| account_pool_enabled: boolean; | |
| image_memory_button_enabled?: boolean; | |
| image_share_evidence_enabled?: boolean; | |
| login_session_duration_hours?: number; | |
| key?: string; | |
| created?: boolean; | |
| }; | |
| export type BrandingConfig = { | |
| site_title?: string; | |
| site_logo_url?: string; | |
| site_header_title?: string; | |
| site_external_link_label?: string; | |
| site_external_link_url?: string; | |
| site_external_link_icon_url?: string; | |
| site_external_link_icon_svg?: string; | |
| }; | |
| export type UserKey = { | |
| id: string; | |
| name: string; | |
| role: "user" | "normal"; | |
| enabled: boolean; | |
| account_pool_enabled?: boolean; | |
| image_memory_button_enabled?: boolean; | |
| third_party_image_enabled?: boolean; | |
| third_party_image_group_ids?: string[]; | |
| third_party_image_model_ids?: string[]; | |
| third_party_image_wallets?: ThirdPartyImageWallet[]; | |
| image_model_daily_limits?: ImageModelDailyLimit[]; | |
| image_model_daily_default_usages?: ImageModelDailyLimit[]; | |
| created_at: string | null; | |
| last_used_at: string | null; | |
| credits?: number; | |
| paid_credits?: number; | |
| reserved_credits?: number; | |
| used_credits?: number; | |
| total_recharged?: number; | |
| permanent_credits?: number; | |
| expiring_credits?: number; | |
| expiring_reserved_credits?: number; | |
| package_credits?: number; | |
| package_grants?: PackageGrant[]; | |
| credit_grants?: Array<{ | |
| id?: string; | |
| remaining?: number; | |
| reserved?: number; | |
| expires_at?: string; | |
| source?: string; | |
| }>; | |
| daily_credits?: number; | |
| daily_credits_remaining?: number; | |
| daily_credits_used?: number; | |
| daily_bonus_credits?: number; | |
| daily_bonus_model_ids?: string[]; | |
| daily_reserved_credits?: number; | |
| daily_credits_date?: string; | |
| has_api_key_display?: boolean; | |
| wechat_openid?: string; | |
| wechat_nickname?: string; | |
| wechat_avatar?: string; | |
| avatar_url?: string; | |
| wechat_bound_at?: string | null; | |
| }; | |
| export type ImageModelDailyLimit = { | |
| model_id: string; | |
| daily_limit: number; | |
| limit_scope?: "per_user" | "total" | string; | |
| used?: number; | |
| reserved?: number; | |
| remaining?: number; | |
| date?: string; | |
| updated_at?: string; | |
| }; | |
| export type MyProfile = { | |
| id: string; | |
| name: string; | |
| role: AuthRole | string; | |
| created_at?: string | null; | |
| account_pool_enabled?: boolean; | |
| third_party_image_enabled?: boolean; | |
| third_party_image_group_ids?: string[]; | |
| third_party_image_model_ids?: string[]; | |
| third_party_image_wallets?: ThirdPartyImageWallet[]; | |
| wechat_openid?: string; | |
| wechat_avatar?: string; | |
| avatar_url?: string; | |
| avatar_updates_remaining?: number; | |
| wechat_bound_at?: string | null; | |
| }; | |
| export type ThirdPartyImageSettings = { | |
| unsupported_img2img_behavior: "hidden" | "disabled" | string; | |
| }; | |
| export type ThirdPartyImageAdapterOperation = { | |
| endpoint: string; | |
| content_type: "auto" | "json" | "multipart"; | |
| body: Record<string, unknown>; | |
| image_field?: string; | |
| poll?: { | |
| endpoint: string; | |
| id_path: string; | |
| interval_seconds: number; | |
| max_attempts: number; | |
| } | null; | |
| response: { | |
| image_paths: string[]; | |
| error_paths?: string[]; | |
| success_path?: string; | |
| success_values?: Array<string | number | boolean | null>; | |
| failure_values?: Array<string | number | boolean | null>; | |
| }; | |
| }; | |
| export type ThirdPartyImageAdapterTemplate = { | |
| version: 2; | |
| method: "POST"; | |
| auth_type: "bearer" | "x-api-key" | "body" | "none"; | |
| auth_name?: string; | |
| generate: ThirdPartyImageAdapterOperation; | |
| edit: ThirdPartyImageAdapterOperation | null; | |
| models?: { | |
| endpoint: string; | |
| response_path?: string; | |
| id_path?: string; | |
| } | null; | |
| }; | |
| export type ThirdPartyImageAdapterResult = { | |
| summary: string; | |
| models: string[]; | |
| template: ThirdPartyImageAdapterTemplate; | |
| model: string; | |
| document_redacted: boolean; | |
| auth_hint?: "none" | "required" | "unknown" | string; | |
| }; | |
| export type ThirdPartyImageOnlineCallResult = { | |
| ok: boolean; | |
| status: number; | |
| duration_ms: number; | |
| endpoint: string; | |
| request_body?: Record<string, unknown>; | |
| response?: unknown; | |
| image_urls?: string[]; | |
| error?: string | null; | |
| }; | |
| export type CreditSummary = { | |
| role: AuthRole | string; | |
| metered: boolean; | |
| credits: number | null; | |
| paid_credits: number | null; | |
| reserved_credits: number; | |
| used_credits: number | null; | |
| total_recharged: number | null; | |
| permanent_credits: number | null; | |
| expiring_credits: number; | |
| expiring_reserved_credits: number; | |
| credit_grants: Array<{ | |
| id?: string; | |
| remaining?: number; | |
| reserved?: number; | |
| expires_at?: string; | |
| source?: string; | |
| }>; | |
| package_credits: number; | |
| package_grants: PackageGrant[]; | |
| daily_credits: number; | |
| daily_credits_remaining: number; | |
| daily_credits_used: number | null; | |
| daily_bonus_credits: number; | |
| daily_bonus_model_ids?: string[]; | |
| daily_reserved_credits: number; | |
| daily_credits_date: string; | |
| /** 该用户还没用掉的签到积分(永久形态账本 + 未过期的签到券)。 */ | |
| checkin_credits?: number; | |
| /** 未使用签到积分里最近一笔的到期时间(ISO);空 = 未设有效期,签到积分为永久。 */ | |
| checkin_credits_expires_at?: string; | |
| /** | |
| * 未使用签到积分的逐笔到期明细(按到期时间升序)。 | |
| * 签到积分逐笔到期,同一时刻到期的会合并成一条;永久形态(未设可用天数)时为空数组。 | |
| */ | |
| checkin_credit_batches?: { credits: number; expires_at: string }[]; | |
| third_party_image_enabled?: boolean; | |
| third_party_image_wallets?: ThirdPartyImageWallet[]; | |
| pool_unit_price?: number; | |
| pool_pay_percentage?: number; | |
| pool_rule_name?: string; | |
| }; | |
| export type PackagePlan = { | |
| name?: string | null; | |
| duration_days?: number | null; | |
| daily_limit?: number | null; | |
| weekly_limit?: number | null; | |
| monthly_limit?: number | null; | |
| total_limit?: number | null; | |
| }; | |
| export type PackageGrant = PackagePlan & { | |
| id?: string | null; | |
| remaining?: number | null; | |
| created_at?: string | null; | |
| starts_at?: string | null; | |
| expires_at?: string | null; | |
| source?: string | null; | |
| code_id?: string | null; | |
| used_total?: number | null; | |
| reserved_total?: number | null; | |
| used_daily?: number | null; | |
| reserved_daily?: number | null; | |
| used_weekly?: number | null; | |
| reserved_weekly?: number | null; | |
| used_monthly?: number | null; | |
| reserved_monthly?: number | null; | |
| }; | |
| export type RedeemCode = { | |
| id: string; | |
| code?: string | null; | |
| prefix: string; | |
| credits: number; | |
| status: "active" | "redeemed" | "disabled" | string; | |
| code_type?: "normal" | "daily_gift" | "package" | string | null; | |
| batch?: string | null; | |
| batch_id?: string | null; | |
| batch_single_use?: boolean; | |
| note?: string | null; | |
| created_at?: string | null; | |
| starts_at?: string | null; | |
| expires_at?: string | null; | |
| daily_gift_date?: string | null; | |
| daily_gift_model_ids?: string[] | null; | |
| package_plan?: PackagePlan | null; | |
| third_party_image?: { | |
| group_id?: string; | |
| group_name?: string; | |
| model_id?: string; | |
| model_ids?: string[]; | |
| models?: Array<{ | |
| id?: string; | |
| name?: string; | |
| alias?: string; | |
| upstream_model?: string; | |
| }>; | |
| auto_enable?: boolean; | |
| } | null; | |
| credit_expires_days?: number | null; | |
| credit_expires_at?: string | null; | |
| is_not_started?: boolean; | |
| is_expired?: boolean; | |
| redeemed_by?: string | null; | |
| redeemed_at?: string | null; | |
| redeemed_user?: { | |
| id?: string | null; | |
| name?: string | null; | |
| wechat_openid?: string | null; | |
| wechat_nickname?: string | null; | |
| } | null; | |
| cleanup_reason?: string; | |
| }; | |
| export type RedeemCodeCleanupSummary = { | |
| matched: number; | |
| deleted: number; | |
| archived: number; | |
| archive_path?: string | null; | |
| expired: number; | |
| disabled: number; | |
| redeemed: number; | |
| limit: number; | |
| }; | |
| export type RedeemCodeCleanupResponse = { | |
| dry_run: boolean; | |
| summary: RedeemCodeCleanupSummary; | |
| archive?: { | |
| archived: number; | |
| path?: string | null; | |
| }; | |
| items: RedeemCode[]; | |
| all_items?: RedeemCode[]; | |
| }; | |
| export type ShopLedgerEntry = { | |
| id: string; | |
| time: string; | |
| type: string; | |
| source?: string; | |
| user_id?: string; | |
| code_id?: string; | |
| code_type?: string; | |
| delta?: number; | |
| credits?: number; | |
| batch?: string; | |
| note?: string; | |
| }; | |
| export type RegistrationMethod = "platform_oauth" | "chatgpt_session"; | |
| export type RegisterConfig = { | |
| enabled: boolean; | |
| mail: { | |
| request_timeout: number; | |
| wait_timeout: number; | |
| wait_interval: number; | |
| providers: Array<Record<string, unknown>>; | |
| }; | |
| proxy: string; | |
| total: number; | |
| threads: number; | |
| retry_attempts: number; | |
| registration_method: RegistrationMethod; | |
| mode: "total" | "quota" | "available"; | |
| target_quota: number; | |
| target_available: number; | |
| check_interval: number; | |
| stats: { | |
| job_id?: string; | |
| success: number; | |
| fail: number; | |
| done: number; | |
| running: number; | |
| threads: number; | |
| elapsed_seconds?: number; | |
| avg_seconds?: number; | |
| success_rate?: number; | |
| current_quota?: number; | |
| current_available?: number; | |
| started_at?: string; | |
| updated_at?: string; | |
| finished_at?: string; | |
| }; | |
| logs?: Array<{ | |
| time: string; | |
| text: string; | |
| level: string; | |
| }>; | |
| }; | |
| export async function login(authKey: string) { | |
| const normalizedAuthKey = String(authKey || "").trim(); | |
| return httpRequest<LoginResponse>("/auth/login", { | |
| method: "POST", | |
| body: {}, | |
| headers: { | |
| Authorization: `Bearer ${normalizedAuthKey}`, | |
| }, | |
| redirectOnUnauthorized: false, | |
| }); | |
| } | |
| export async function refreshSession(authKey: string) { | |
| const normalizedAuthKey = String(authKey || "").trim(); | |
| return httpRequest<LoginResponse>("/auth/session", { | |
| method: "POST", | |
| body: {}, | |
| headers: { | |
| Authorization: `Bearer ${normalizedAuthKey}`, | |
| }, | |
| redirectOnUnauthorized: false, | |
| }); | |
| } | |
| export async function fetchAccounts() { | |
| return httpRequest<AccountListResponse>("/api/accounts"); | |
| } | |
| export async function fetchQuotaSummary(strategy?: string, allowShared = true) { | |
| const params = new URLSearchParams(); | |
| if (strategy) { | |
| params.set("strategy", strategy); | |
| } | |
| params.set("allow_shared", allowShared ? "true" : "false"); | |
| return httpRequest<{ quota: number; label?: string; unknown?: boolean; count?: number }>( | |
| `/api/accounts/quota?${params.toString()}`, | |
| ); | |
| } | |
| export async function createAccounts(tokens: string[], accounts: AccountImportPayload[] = []) { | |
| return httpRequest<AccountMutationResponse>("/api/accounts", { | |
| method: "POST", | |
| body: { | |
| tokens, | |
| ...(accounts.length > 0 ? { accounts } : {}), | |
| }, | |
| }); | |
| } | |
| export async function startAccountEmailLoginImport(email: string) { | |
| return httpRequest<AccountEmailLoginImportResponse>("/api/accounts/email-login/start", { | |
| method: "POST", | |
| body: { email }, | |
| }); | |
| } | |
| export async function verifyAccountEmailLoginImport(sessionId: string, code: string) { | |
| return httpRequest<AccountEmailLoginImportResponse>("/api/accounts/email-login/verify", { | |
| method: "POST", | |
| body: { session_id: sessionId, code }, | |
| }); | |
| } | |
| export async function deleteAccounts(tokens: string[]) { | |
| return httpRequest<AccountMutationResponse>("/api/accounts", { | |
| method: "DELETE", | |
| body: { tokens }, | |
| }); | |
| } | |
| export async function refreshAccounts(accessTokens: string[]) { | |
| return httpRequest<{ progress_id: string; total: number }>("/api/accounts/refresh", { | |
| method: "POST", | |
| body: { access_tokens: accessTokens }, | |
| }); | |
| } | |
| export async function reLoginAccount(accessToken: string) { | |
| return httpRequest<AccountPasswordReloginResponse>("/api/accounts/re-login", { | |
| method: "POST", | |
| body: { access_token: accessToken }, | |
| }); | |
| } | |
| export async function verifyAccountPasswordRelogin(reloginId: string, code: string) { | |
| return httpRequest<AccountPasswordReloginCompletedResponse>("/api/accounts/re-login/verify", { | |
| method: "POST", | |
| body: { relogin_id: reloginId, code }, | |
| }); | |
| } | |
| export async function fetchModels() { | |
| return httpRequest<ModelListResponse>("/v1/models"); | |
| } | |
| /** | |
| * 对话页模型下拉的数据源。 | |
| * | |
| * `/v1/models` 是 OpenAI 兼容的全量接口(含第三方生图模型),不能拿来当对话模型用, | |
| * 所以对话页改用它:后端已经按结构化规则过滤掉生图/工具类模型。 | |
| */ | |
| export async function fetchChatModels() { | |
| return httpRequest<ChatModelListResponse>("/api/chat/models"); | |
| } | |
| /** 保存「对话可用模型」的人工覆盖项(include 强制显示 / exclude 强制隐藏)。 */ | |
| export async function updateChatModelOverrides(overrides: ChatModelOverrides) { | |
| return httpRequest<{ config: SettingsConfig }>("/api/settings", { | |
| method: "POST", | |
| body: { chat_models: overrides }, | |
| }); | |
| } | |
| /** | |
| * 生图页模型下拉 / 管理页「生图可用模型」的数据源。 | |
| * | |
| * 只返回能生图的模型(账号池上游内置 + 第三方接入),默认全部启用。 | |
| */ | |
| export async function fetchImageModels() { | |
| return httpRequest<ImageModelListResponse>("/api/image/models"); | |
| } | |
| /** | |
| * 保存「生图可用模型」的人工覆盖项。 | |
| * | |
| * 只影响模型是否出现在生图下拉里,不改动第三方生图配置本身 | |
| * (计费、路由、额度与对外 API 均不受影响)。 | |
| */ | |
| export async function updateImageModelOverrides(overrides: ModelOverrides) { | |
| return httpRequest<{ config: SettingsConfig }>("/api/settings", { | |
| method: "POST", | |
| body: { image_models: overrides }, | |
| }); | |
| } | |
| /** | |
| * 管理页「系统可用模型」列表的数据源。 | |
| * | |
| * 与 `/v1/models` 同一份全量目录,只是额外带上了 `system_enabled` 标记。 | |
| */ | |
| export async function fetchSystemModels() { | |
| return httpRequest<SystemModelListResponse>("/api/system/models"); | |
| } | |
| /** | |
| * 保存「系统可用模型」的人工覆盖项。 | |
| * | |
| * 只影响这张列表显示哪些模型,不改动上游账号池、第三方配置、计费与路由, | |
| * 也不影响对外 `/v1/models`(后者必须保持全量)。 | |
| */ | |
| export async function updateSystemModelOverrides(overrides: ModelOverrides) { | |
| return httpRequest<{ config: SettingsConfig }>("/api/settings", { | |
| method: "POST", | |
| body: { system_models: overrides }, | |
| }); | |
| } | |
| export type OAuthLoginStartResponse = { | |
| session_id: string; | |
| authorize_url: string; | |
| expires_in: string; | |
| redirect_uri_prefix: string; | |
| }; | |
| export async function startOAuthLogin(emailHint?: string) { | |
| return httpRequest<OAuthLoginStartResponse>("/api/accounts/oauth/start", { | |
| method: "POST", | |
| body: { email_hint: emailHint ?? "" }, | |
| }); | |
| } | |
| export async function finishOAuthLogin(sessionId: string, callback: string) { | |
| return httpRequest<AccountMutationResponse>("/api/accounts/oauth/finish", { | |
| method: "POST", | |
| body: { session_id: sessionId, callback }, | |
| }); | |
| } | |
| export async function fetchAccountRefreshProgress(progressId: string) { | |
| return httpRequest<AccountRefreshProgress>(`/api/accounts/refresh/progress/${encodeURIComponent(progressId)}`); | |
| } | |
| export async function runUpstreamAccountActions( | |
| accessTokens: string[], | |
| options: UpstreamAccountActionOptions, | |
| ) { | |
| return httpRequest<UpstreamAccountActionResponse>("/api/accounts/upstream-actions", { | |
| method: "POST", | |
| body: { access_tokens: accessTokens, ...options }, | |
| }); | |
| } | |
| function getFilenameFromDisposition(value: unknown, fallback: string) { | |
| const disposition = typeof value === "string" ? value : ""; | |
| const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i); | |
| if (utf8Match?.[1]) { | |
| return decodeURIComponent(utf8Match[1].replace(/"/g, "")); | |
| } | |
| const match = disposition.match(/filename="?([^";]+)"?/i); | |
| return match?.[1] || fallback; | |
| } | |
| export async function exportAccounts(format: AccountExportFormat, accessTokens: string[] = []) { | |
| const response = await request.request<Blob>({ | |
| url: "/api/accounts/export", | |
| method: "POST", | |
| data: { | |
| format, | |
| access_tokens: accessTokens, | |
| }, | |
| responseType: "blob", | |
| }); | |
| return { | |
| blob: response.data, | |
| filename: getFilenameFromDisposition(response.headers["content-disposition"], `codex-accounts.${format}`), | |
| }; | |
| } | |
| export async function updateAccount( | |
| accessToken: string, | |
| updates: { | |
| type?: AccountType; | |
| status?: AccountStatus; | |
| quota?: number; | |
| shared?: boolean; | |
| }, | |
| ) { | |
| return httpRequest<AccountUpdateResponse>("/api/accounts/update", { | |
| method: "POST", | |
| body: { | |
| access_token: accessToken, | |
| ...updates, | |
| }, | |
| }); | |
| } | |
| export async function generateImage(prompt: string, model?: ImageModel, size?: string) { | |
| return httpRequest<ImageResponse>( | |
| "/v1/images/generations", | |
| { | |
| method: "POST", | |
| body: { | |
| prompt, | |
| ...(model ? { model } : {}), | |
| ...(size ? { size } : {}), | |
| n: 1, | |
| response_format: "b64_json", | |
| }, | |
| }, | |
| ); | |
| } | |
| export async function editImage(files: File | File[], prompt: string, model?: ImageModel, size?: string) { | |
| const formData = new FormData(); | |
| const uploadFiles = Array.isArray(files) ? files : [files]; | |
| uploadFiles.forEach((file) => { | |
| formData.append("image", file); | |
| }); | |
| formData.append("prompt", prompt); | |
| if (model) { | |
| formData.append("model", model); | |
| } | |
| if (size) { | |
| formData.append("size", size); | |
| } | |
| formData.append("n", "1"); | |
| return httpRequest<ImageResponse>( | |
| "/v1/images/edits", | |
| { | |
| method: "POST", | |
| body: formData, | |
| }, | |
| ); | |
| } | |
| export async function createImageGenerationTask( | |
| clientTaskId: string, | |
| prompt: string, | |
| model?: ImageModel, | |
| size?: string, | |
| memory?: boolean, | |
| memoryConversationId?: string, | |
| accountPoolStrategy?: string, | |
| quality?: string, | |
| resolution?: string, | |
| thirdPartyImageGroupId?: string, | |
| ) { | |
| return httpRequest<ImageTask>("/api/image-tasks/generations", { | |
| method: "POST", | |
| body: { | |
| client_task_id: clientTaskId, | |
| prompt, | |
| ...(model ? { model } : {}), | |
| ...(size ? { size } : {}), | |
| ...(memory ? { memory: true } : {}), | |
| ...(memory && memoryConversationId ? { memory_conversation_id: memoryConversationId } : {}), | |
| ...(accountPoolStrategy ? { account_pool_strategy: accountPoolStrategy } : {}), | |
| ...(quality ? { quality } : {}), | |
| ...(resolution ? { resolution } : {}), | |
| ...(thirdPartyImageGroupId ? { third_party_image_group_id: thirdPartyImageGroupId } : {}), | |
| }, | |
| }); | |
| } | |
| export async function createImageEditTask( | |
| clientTaskId: string, | |
| files: File | File[], | |
| prompt: string, | |
| model?: ImageModel, | |
| size?: string, | |
| memory?: boolean, | |
| memoryConversationId?: string, | |
| memoryReset?: boolean, | |
| accountPoolStrategy?: string, | |
| quality?: string, | |
| resolution?: string, | |
| thirdPartyImageGroupId?: string, | |
| mask?: File, | |
| ) { | |
| const formData = new FormData(); | |
| const uploadFiles = Array.isArray(files) ? files : [files]; | |
| uploadFiles.forEach((file) => { | |
| formData.append("image", file); | |
| }); | |
| if (mask) { | |
| formData.append("mask", mask); | |
| } | |
| formData.append("client_task_id", clientTaskId); | |
| formData.append("prompt", prompt); | |
| if (model) { | |
| formData.append("model", model); | |
| } | |
| if (size) { | |
| formData.append("size", size); | |
| } | |
| if (memory) { | |
| formData.append("memory", "true"); | |
| } | |
| if (memory && memoryConversationId) { | |
| formData.append("memory_conversation_id", memoryConversationId); | |
| } | |
| if (memory && memoryReset) { | |
| formData.append("memory_reset", "true"); | |
| } | |
| if (accountPoolStrategy) { | |
| formData.append("account_pool_strategy", accountPoolStrategy); | |
| } | |
| if (quality) { | |
| formData.append("quality", quality); | |
| } | |
| if (resolution) { | |
| formData.append("resolution", resolution); | |
| } | |
| if (thirdPartyImageGroupId) { | |
| formData.append("third_party_image_group_id", thirdPartyImageGroupId); | |
| } | |
| return httpRequest<ImageTask>("/api/image-tasks/edits", { | |
| method: "POST", | |
| body: formData, | |
| }); | |
| } | |
| export async function fetchImageTasks(ids: string[], signal?: AbortSignal) { | |
| const params = new URLSearchParams(); | |
| if (ids.length > 0) { | |
| params.set("ids", ids.join(",")); | |
| } | |
| return httpRequest<ImageTaskListResponse>(`/api/image-tasks${params.toString() ? `?${params.toString()}` : ""}`, { signal }); | |
| } | |
| export async function resumeImageTaskPoll(taskId: string, extraTimeoutSecs?: number) { | |
| return httpRequest<ImageTask>(`/api/image-tasks/${encodeURIComponent(taskId)}/resume-poll`, { | |
| method: "POST", | |
| body: extraTimeoutSecs ? { extra_timeout_secs: extraTimeoutSecs } : {}, | |
| }); | |
| } | |
| /** 「官网来源取证」状态:是否已生成分享链接、链接本身、剩余有效秒数。 */ | |
| export type ImageShareEvidence = { | |
| tracked: boolean; | |
| shared: boolean; | |
| /** 官方链接正在生成中(出图后自动固化尚未完成,或失败后等下一轮重试)。 */ | |
| generating: boolean; | |
| share_url: string; | |
| expires_at: string; | |
| remaining_seconds: number; | |
| status: string; | |
| /** 已过保留期并被程序回收(官网对话已删除,无法再生成链接)。 */ | |
| released: boolean; | |
| released_at: string; | |
| /** 回收原因:"expired"(到期自动)/ "revoked"(用户手动撤销)。 */ | |
| release_reason?: string; | |
| /** 已到保留期但回收线程还没轮到(每 5 分钟一轮),属于「正在清理」的短暂状态。 */ | |
| expired: boolean; | |
| /** 后台当前设置的「链接保留时长」(小时),用于把「已过期」说清楚。 */ | |
| retention_hours: number; | |
| }; | |
| export async function fetchImageShareEvidence(conversationId: string, signal?: AbortSignal) { | |
| const params = new URLSearchParams({ conversation_id: conversationId }); | |
| return httpRequest<ImageShareEvidence>(`/api/image-tasks/share-evidence?${params.toString()}`, { signal }); | |
| } | |
| export async function createImageShareEvidence(conversationId: string) { | |
| return httpRequest<ImageShareEvidence>("/api/image-tasks/share-evidence", { | |
| method: "POST", | |
| body: { conversation_id: conversationId }, | |
| }); | |
| } | |
| export async function revokeImageShareEvidence(conversationId: string) { | |
| const params = new URLSearchParams({ conversation_id: conversationId }); | |
| return httpRequest<ImageShareEvidence>(`/api/image-tasks/share-evidence?${params.toString()}`, { | |
| method: "DELETE", | |
| }); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // 商品视觉(商品素材包 / 连续商品详情页) | |
| // --------------------------------------------------------------------------- | |
| export type ProductVisualScenario = "asset_pack" | "detail_page"; | |
| export type ProductVisualItem = { | |
| seq: number; | |
| title: string; | |
| prompt: string; | |
| size: string; | |
| status: "pending" | "queued" | "running" | "success" | "error"; | |
| image_task_id: string; | |
| image_url: string; | |
| error: string; | |
| updated_at: string; | |
| retry_count?: number; | |
| versions?: Array<{ | |
| version: string; | |
| image_url: string; | |
| prompt: string; | |
| status: string; | |
| created_at: string; | |
| }>; | |
| }; | |
| export type ProductVisualTask = { | |
| id: string; | |
| scenario: ProductVisualScenario; | |
| scenario_label: string; | |
| platform: string; | |
| platform_label: string; | |
| output_count: number; | |
| source_prompt: string; | |
| source_image_count?: number; | |
| model: string; | |
| size: string; | |
| status: "planning" | "generating" | "success" | "partial_failed" | "failed"; | |
| items: ProductVisualItem[]; | |
| success_count: number; | |
| created_at: string; | |
| updated_at: string; | |
| plan_error: string; | |
| plan_stage?: "" | "analyzing" | "planning" | "reviewing" | "done"; | |
| plan_analysis?: { | |
| category?: string; | |
| audience?: string; | |
| decision_points?: string[]; | |
| differentiation?: string; | |
| concerns?: string[]; | |
| visual_evidence?: string[]; | |
| uncertainties?: string[]; | |
| } | null; | |
| plan_visual_style?: { | |
| product_appearance?: string; | |
| background_style?: string; | |
| color_palette?: string; | |
| lighting?: string; | |
| composition_language?: string; | |
| typography_style?: string; | |
| } | null; | |
| agent_mode?: "multimodal_agent" | "template_fallback" | string; | |
| agent_trace?: Array<{ | |
| stage: "observe" | "plan" | "review" | "fallback" | string; | |
| status: "success" | "failed" | "warning" | string; | |
| summary: string; | |
| updated_at: string; | |
| }>; | |
| agent_review?: { | |
| approved: boolean; | |
| summary: string; | |
| issues: string[]; | |
| revised: boolean; | |
| } | null; | |
| mode?: "concurrent" | "sequential"; | |
| }; | |
| export type ProductVisualTaskListResponse = { | |
| items: ProductVisualTask[]; | |
| missing_ids: string[]; | |
| }; | |
| export async function createProductVisualTask( | |
| clientTaskId: string, | |
| files: File | File[], | |
| prompt: string, | |
| scenario: ProductVisualScenario, | |
| platform: string, | |
| outputCount: number, | |
| model?: string, | |
| mode: "concurrent" | "sequential" = "concurrent", | |
| ) { | |
| const formData = new FormData(); | |
| const uploadFiles = Array.isArray(files) ? files : [files]; | |
| uploadFiles.forEach((file) => { | |
| formData.append("image", file); | |
| }); | |
| formData.append("client_task_id", clientTaskId); | |
| formData.append("prompt", prompt); | |
| formData.append("scenario", scenario); | |
| formData.append("platform", platform); | |
| formData.append("output_count", String(outputCount)); | |
| formData.append("mode", mode); | |
| if (model) { | |
| formData.append("model", model); | |
| } | |
| return httpRequest<ProductVisualTask>("/api/product-visual/tasks", { | |
| method: "POST", | |
| body: formData, | |
| }); | |
| } | |
| export async function fetchProductVisualTasks(ids: string[], signal?: AbortSignal) { | |
| const params = new URLSearchParams(); | |
| if (ids.length > 0) { | |
| params.set("ids", ids.join(",")); | |
| } | |
| return httpRequest<ProductVisualTaskListResponse>( | |
| `/api/product-visual/tasks${params.toString() ? `?${params.toString()}` : ""}`, | |
| { signal }, | |
| ); | |
| } | |
| export async function retryProductVisualItem(taskId: string, seq: number) { | |
| return httpRequest<ProductVisualTask>(`/api/product-visual/tasks/${encodeURIComponent(taskId)}/items/${seq}/retry`, { | |
| method: "POST", | |
| }); | |
| } | |
| export async function iterateProductVisualItem( | |
| taskId: string, | |
| seq: number, | |
| revision: string, | |
| maskDataUrl?: string, | |
| ) { | |
| return httpRequest<ProductVisualTask>(`/api/product-visual/tasks/${encodeURIComponent(taskId)}/items/${seq}/iterate`, { | |
| method: "POST", | |
| body: maskDataUrl ? { revision, mask: maskDataUrl } : { revision }, | |
| }); | |
| } | |
| export async function fetchProductVisualSourceImage(taskId: string, index: number) { | |
| const response = await request.request<Blob>({ | |
| url: `/api/product-visual/tasks/${encodeURIComponent(taskId)}/source-images/${index}`, | |
| method: "GET", | |
| responseType: "blob", | |
| }); | |
| return response.data; | |
| } | |
| export async function fetchSettingsConfig() { | |
| return httpRequest<{ config: SettingsConfig }>("/api/settings"); | |
| } | |
| export async function fetchNavigation() { | |
| return httpRequest<{ items: NavigationItem[] }>("/api/navigation"); | |
| } | |
| export async function registerNormalUser(name: string, inviteCode: string) { | |
| return httpRequest<LoginResponse & { invite_code?: string }>("/auth/register", { | |
| method: "POST", | |
| body: { name, invite_code: inviteCode }, | |
| redirectOnUnauthorized: false, | |
| }); | |
| } | |
| export async function optimizePrompt(prompt: string) { | |
| return httpRequest<{ prompt: string; model: string }>("/api/prompt/optimize", { | |
| method: "POST", | |
| body: { prompt }, | |
| }); | |
| } | |
| export async function reverseImagePrompt(image: string) { | |
| return httpRequest<{ prompt: string; model: string }>("/api/prompt/reverse", { | |
| method: "POST", | |
| body: { image }, | |
| }); | |
| } | |
| export type ImageAnnotationBounds = { | |
| x: number; | |
| y: number; | |
| width: number; | |
| height: number; | |
| }; | |
| export type ImageRegionRecognition = { | |
| label: string; | |
| object_type: string; | |
| visible_text: string; | |
| description: string; | |
| confidence: number; | |
| model: string; | |
| bounds: ImageAnnotationBounds; | |
| image_width: number; | |
| image_height: number; | |
| }; | |
| export async function identifyImageAnnotationRegion(image: string, bounds: ImageAnnotationBounds) { | |
| return httpRequest<ImageRegionRecognition>("/api/image-annotations/identify", { | |
| method: "POST", | |
| body: { image, bounds }, | |
| }); | |
| } | |
| export async function fetchAnnouncements() { | |
| return httpRequest<{ items: Announcement[] }>("/api/announcements"); | |
| } | |
| export async function updateSettingsConfig(settings: SettingsConfig) { | |
| return httpRequest<{ config: SettingsConfig }>("/api/settings", { | |
| method: "POST", | |
| body: settings, | |
| }); | |
| } | |
| export type ContentReviewPreview = { | |
| allowed: boolean; | |
| rule_id: string; | |
| matched_terms: string[]; | |
| }; | |
| export async function previewContentReview( | |
| text: string, | |
| localReview: SettingsConfig["local_review"], | |
| sensitiveWords: string[] = [], | |
| ) { | |
| return httpRequest<ContentReviewPreview>("/api/settings/content-review/preview", { | |
| method: "POST", | |
| body: { | |
| text, | |
| local_review: localReview, | |
| sensitive_words: sensitiveWords, | |
| }, | |
| }); | |
| } | |
| export async function testBackupConnection() { | |
| return httpRequest<{ result: { ok: boolean; status: number } }>("/api/backup/test", { | |
| method: "POST", | |
| body: {}, | |
| }); | |
| } | |
| export async function testImageStorageConnection() { | |
| return httpRequest<{ result: { ok: boolean; status: number; error?: string | null } }>("/api/image-storage/test", { | |
| method: "POST", | |
| body: {}, | |
| }); | |
| } | |
| export async function syncImageStorage() { | |
| return httpRequest<{ result: { uploaded: number; skipped: number; failed: number } }>("/api/image-storage/sync", { | |
| method: "POST", | |
| body: {}, | |
| }); | |
| } | |
| export async function fetchBackups() { | |
| return httpRequest<{ items: BackupItem[]; state: BackupState; settings: BackupSettings }>("/api/backups"); | |
| } | |
| export async function runBackupNow() { | |
| return httpRequest<{ result: { key: string; size: number; encrypted: boolean } }>("/api/backups/run", { | |
| method: "POST", | |
| body: {}, | |
| }); | |
| } | |
| export async function deleteBackup(key: string) { | |
| return httpRequest<{ ok: boolean }>("/api/backups/delete", { | |
| method: "POST", | |
| body: { key }, | |
| }); | |
| } | |
| export async function fetchBackupDetail(key: string) { | |
| const params = new URLSearchParams(); | |
| params.set("key", key); | |
| return httpRequest<{ item: BackupDetail }>(`/api/backups/detail?${params.toString()}`); | |
| } | |
| export function getBackupDownloadUrl(key: string) { | |
| const params = new URLSearchParams(); | |
| params.set("key", key); | |
| return `/api/backups/download?${params.toString()}`; | |
| } | |
| export async function fetchManagedImages(filters: { | |
| start_date?: string; | |
| end_date?: string; | |
| limit?: number; | |
| offset?: number; | |
| tags?: string[]; | |
| } = {}) { | |
| const params = new URLSearchParams(); | |
| if (filters.start_date) params.set("start_date", filters.start_date); | |
| if (filters.end_date) params.set("end_date", filters.end_date); | |
| params.set("limit", String(filters.limit || 12)); | |
| if (filters.offset) params.set("offset", String(filters.offset)); | |
| if (filters.tags?.length) params.set("tags", filters.tags.join(",")); | |
| return httpRequest<{ | |
| items: ManagedImage[]; | |
| groups: Array<{ date: string; items: ManagedImage[] }>; | |
| total: number; | |
| offset: number; | |
| limit: number; | |
| matching_paths: string[]; | |
| }>( | |
| `/api/images${params.toString() ? `?${params.toString()}` : ""}`, | |
| ); | |
| } | |
| export async function deleteManagedImages(body: { paths?: string[]; start_date?: string; end_date?: string; all_matching?: boolean }) { | |
| return httpRequest<{ removed: number }>("/api/images/delete", { method: "POST", body }); | |
| } | |
| export async function downloadImages(paths: string[]) { | |
| const response = await request.post("/api/images/download", { paths }, { responseType: "blob" }); | |
| const blob = response.data as Blob; | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = "images.zip"; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| export async function downloadSingleImage(path: string) { | |
| const response = await request.get(`/api/images/download/${path}`, { responseType: "blob" }); | |
| const blob = response.data as Blob; | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = path.split("/").pop() || "image.png"; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| export async function fetchImageTags() { | |
| return httpRequest<{ tags: string[] }>("/api/images/tags"); | |
| } | |
| export async function setImageTags(path: string, tags: string[]) { | |
| return httpRequest<{ ok: boolean; tags: string[] }>("/api/images/tags", { | |
| method: "POST", | |
| body: { path, tags }, | |
| }); | |
| } | |
| export async function deleteImageTag(tag: string) { | |
| return httpRequest<{ ok: boolean; removed_from: number }>(`/api/images/tags/${encodeURIComponent(tag)}`, { | |
| method: "DELETE", | |
| }); | |
| } | |
| export type PresenceSnapshot = { | |
| total: number; | |
| normal?: number; | |
| internal?: number; | |
| admin?: number; | |
| window_secs?: number; | |
| }; | |
| // 在线人数属装饰性信息:心跳失败时绝不能触发「清会话跳登录」的全局拦截。 | |
| const PRESENCE_REQUEST_OPTIONS = { redirectOnUnauthorized: false } as const; | |
| export async function pingPresence() { | |
| return httpRequest<PresenceSnapshot>("/api/presence/ping", { | |
| ...PRESENCE_REQUEST_OPTIONS, | |
| method: "POST", | |
| }); | |
| } | |
| export async function fetchPresence() { | |
| return httpRequest<PresenceSnapshot>("/api/presence/online", PRESENCE_REQUEST_OPTIONS); | |
| } | |
| /** | |
| * 页面关闭时主动下线,让人数立即回落。 | |
| * | |
| * ⚠️ 必须**同步发起**:所以要由调用方把密钥提前备好传进来。若在这里先 await 读存储, | |
| * 页面卸载会让这个微任务来不及执行,请求根本发不出去(keepalive 的全部意义就是 | |
| * 让请求在文档消失后仍能完成,前提是它已经被发起)。 | |
| * 失败无需处理——服务端窗口到期后同样会把这个身份清掉。 | |
| */ | |
| export function leavePresence(authKey: string) { | |
| const normalizedKey = String(authKey || "").trim(); | |
| if (!normalizedKey) { | |
| return; | |
| } | |
| try { | |
| void fetch(`${webConfig.apiUrl.replace(/\/$/, "")}/api/presence/leave`, { | |
| method: "POST", | |
| keepalive: true, | |
| headers: { Authorization: `Bearer ${normalizedKey}` }, | |
| }); | |
| } catch { | |
| // 静默:这是一次最佳努力上报。 | |
| } | |
| } | |
| /** 读取本地保存的密钥,供 pagehide 前的同步下线请求复用。 */ | |
| export async function loadPresenceAuthKey(): Promise<string> { | |
| try { | |
| return await getStoredAuthKey(); | |
| } catch { | |
| return ""; | |
| } | |
| } | |
| export type CreditRangeKey = "today" | "7d" | "30d" | "all"; | |
| export type CreditUsageTotals = { | |
| /** 真实扣减的积分。内部用户与管理员恒为 0 —— 他们不消耗积分。 */ | |
| credits: number; | |
| /** 按普通用户同时段单价折算的参考值,不是实际扣费。 */ | |
| equiv_credits: number; | |
| calls: number; | |
| success_calls: number; | |
| failed_calls: number; | |
| images: number; | |
| /** 有调用记录的身份数(含内部/管理员)。 */ | |
| caller_users: number; | |
| /** 真正产生积分消耗的身份数,只可能来自普通用户。 */ | |
| payer_users: number; | |
| }; | |
| export type CreditUsageBucket = { | |
| calls: number; | |
| images: number; | |
| credits: number; | |
| equiv_credits: number; | |
| }; | |
| export type CreditUsageDay = CreditUsageBucket & { day: string }; | |
| export type CreditUsageModel = CreditUsageBucket & { model: string }; | |
| export type CreditUsageRoleRow = CreditUsageBucket & { role: string; label: string }; | |
| export type CreditUsageUser = CreditUsageBucket & { | |
| identity_id: string; | |
| name: string; | |
| role: string; | |
| role_label: string; | |
| }; | |
| export type CreditUsageSummary = { | |
| range: string; | |
| start_day: string; | |
| end_day: string; | |
| role: string; | |
| model: string; | |
| previous: { start_day: string; end_day: string; credits: number; calls: number }; | |
| /** 上一期为 0 时为 null,前端显示「—」而不是 +∞%。 */ | |
| change_percent: number | null; | |
| totals: CreditUsageTotals; | |
| by_day: CreditUsageDay[]; | |
| by_model: CreditUsageModel[]; | |
| by_role: CreditUsageRoleRow[]; | |
| top_users: CreditUsageUser[]; | |
| models: string[]; | |
| }; | |
| export async function fetchCreditUsage( | |
| params: { range?: CreditRangeKey; role?: string; model?: string; top?: number } = {}, | |
| options: { signal?: AbortSignal } = {}, | |
| ) { | |
| const search = new URLSearchParams(); | |
| search.set("range", params.range ?? "today"); | |
| if (params.role) { | |
| search.set("role", params.role); | |
| } | |
| if (params.model) { | |
| search.set("model", params.model); | |
| } | |
| if (params.top) { | |
| search.set("top", String(params.top)); | |
| } | |
| return httpRequest<CreditUsageSummary>(`/api/dashboard/credits?${search.toString()}`, { | |
| signal: options.signal, | |
| }); | |
| } | |
| export async function fetchSystemLogs(filters: { type?: string; start_date?: string; end_date?: string; limit?: number } = {}) { | |
| const params = new URLSearchParams(); | |
| if (filters.type) params.set("type", filters.type); | |
| if (filters.start_date) params.set("start_date", filters.start_date); | |
| if (filters.end_date) params.set("end_date", filters.end_date); | |
| params.set("limit", String(filters.limit || 200)); | |
| return httpRequest<{ items: SystemLog[] }>(`/api/logs${params.toString() ? `?${params.toString()}` : ""}`); | |
| } | |
| export async function deleteSystemLogs(ids: string[]) { | |
| return httpRequest<{ removed: number }>("/api/logs/delete", { | |
| method: "POST", | |
| body: { ids }, | |
| }); | |
| } | |
| export async function fetchUserKeys() { | |
| return httpRequest<{ items: UserKey[] }>("/api/auth/users"); | |
| } | |
| export async function wechatLogin(code: string, inviteCode = "") { | |
| return httpRequest<LoginResponse & { | |
| key: string; | |
| credits?: CreditSummary; | |
| wechat?: { openid?: string; nickname?: string; avatar?: string }; | |
| }>("/api/auth/wechat/login", { | |
| method: "POST", | |
| body: { code, invite_code: inviteCode }, | |
| redirectOnUnauthorized: false, | |
| }); | |
| } | |
| export async function fetchWechatLoginStatus() { | |
| return httpRequest<{ enabled: boolean; auto_register_enabled?: boolean; login_session_duration_hours?: number }>("/api/auth/wechat/status", { | |
| redirectOnUnauthorized: false, | |
| }); | |
| } | |
| export async function fetchBranding() { | |
| return httpRequest<{ branding: BrandingConfig }>("/api/branding", { | |
| redirectOnUnauthorized: false, | |
| }); | |
| } | |
| export async function fetchNormalUserKeys() { | |
| return httpRequest<{ items: UserKey[] }>("/api/auth/normal-users"); | |
| } | |
| export type UserKeyBatchAction = "enable" | "disable" | "delete"; | |
| export async function batchMutateUserKeys( | |
| kind: "internal" | "normal", | |
| ids: string[], | |
| action: UserKeyBatchAction, | |
| ) { | |
| const path = kind === "normal" ? "/api/auth/normal-users/batch" : "/api/auth/users/batch"; | |
| return httpRequest<{ | |
| result: { | |
| action: UserKeyBatchAction; | |
| requested: number; | |
| matched: number; | |
| changed: number; | |
| }; | |
| items: UserKey[]; | |
| }>(path, { | |
| method: "POST", | |
| body: { ids, action }, | |
| }); | |
| } | |
| export async function batchUpdateUserImagePermissions( | |
| kind: "internal" | "normal", | |
| items: Array<{ | |
| id: string; | |
| third_party_image_enabled?: boolean; | |
| third_party_image_group_ids?: string[]; | |
| third_party_image_model_ids?: string[]; | |
| }>, | |
| ) { | |
| const userKind = kind === "normal" ? "normal-users" : "users"; | |
| return httpRequest<{ | |
| result: { requested: number; matched: number; changed: number }; | |
| items: UserKey[]; | |
| }>(`/api/auth/${userKind}/batch-image-permissions`, { | |
| method: "POST", | |
| body: { items }, | |
| }); | |
| } | |
| export async function createUserKey(name: string) { | |
| return httpRequest<{ item: UserKey; key: string; items: UserKey[] }>("/api/auth/users", { | |
| method: "POST", | |
| body: { name }, | |
| }); | |
| } | |
| export async function createNormalUserKey(name: string) { | |
| return httpRequest<{ item: UserKey; key: string; items: UserKey[] }>("/api/auth/normal-users", { | |
| method: "POST", | |
| body: { name }, | |
| }); | |
| } | |
| export async function updateUserKey(keyId: string, updates: { | |
| enabled?: boolean; | |
| account_pool_enabled?: boolean; | |
| image_memory_button_enabled?: boolean; | |
| third_party_image_enabled?: boolean; | |
| third_party_image_group_ids?: string[]; | |
| third_party_image_model_ids?: string[]; | |
| image_model_daily_limits?: Array<{ model_id: string; daily_limit: number }>; | |
| name?: string; | |
| key?: string; | |
| }) { | |
| return httpRequest<{ item: UserKey; items: UserKey[] }>(`/api/auth/users/${keyId}`, { | |
| method: "POST", | |
| body: updates, | |
| }); | |
| } | |
| export async function updateNormalUserKey(keyId: string, updates: { | |
| enabled?: boolean; | |
| account_pool_enabled?: boolean; | |
| image_memory_button_enabled?: boolean; | |
| third_party_image_enabled?: boolean; | |
| third_party_image_group_ids?: string[]; | |
| third_party_image_model_ids?: string[]; | |
| image_model_daily_limits?: Array<{ model_id: string; daily_limit: number }>; | |
| name?: string; | |
| key?: string; | |
| }) { | |
| return httpRequest<{ item: UserKey; items: UserKey[] }>(`/api/auth/normal-users/${keyId}`, { | |
| method: "POST", | |
| body: updates, | |
| }); | |
| } | |
| export async function deleteUserKey(keyId: string) { | |
| return httpRequest<{ items: UserKey[] }>(`/api/auth/users/${keyId}`, { | |
| method: "DELETE", | |
| }); | |
| } | |
| export async function deleteNormalUserKey(keyId: string) { | |
| return httpRequest<{ items: UserKey[] }>(`/api/auth/normal-users/${keyId}`, { | |
| method: "DELETE", | |
| }); | |
| } | |
| export async function fetchMyCredits() { | |
| return httpRequest<{ credits: CreditSummary }>("/api/me/credits"); | |
| } | |
| export async function fetchMyRedeemCodes() { | |
| return httpRequest<{ items: RedeemCode[] }>("/api/me/redeem-codes"); | |
| } | |
| export async function fetchMyProfile() { | |
| return httpRequest<{ profile: MyProfile }>("/api/me/profile"); | |
| } | |
| export async function updateMyProfile(name: string) { | |
| return httpRequest<{ profile: MyProfile }>("/api/me/profile", { | |
| method: "POST", | |
| body: { name }, | |
| }); | |
| } | |
| export async function uploadMyAvatar(file: File) { | |
| const body = new FormData(); | |
| body.append("file", file); | |
| return httpRequest<{ profile: MyProfile }>("/api/me/avatar", { | |
| method: "POST", | |
| body, | |
| }); | |
| } | |
| export async function fetchMyApiKey() { | |
| return httpRequest<{ api_key: string }>("/api/me/api-key"); | |
| } | |
| export async function redeemCode(code: string) { | |
| return httpRequest<{ item: RedeemCode; credits: CreditSummary }>("/api/redeem", { | |
| method: "POST", | |
| body: { code }, | |
| }); | |
| } | |
| export async function previewRedeemCode(code: string) { | |
| return httpRequest<{ item: RedeemCode; redeemable: boolean; message: string }>("/api/redeem/preview", { | |
| method: "POST", | |
| body: { code }, | |
| }); | |
| } | |
| export async function fetchShopCodes() { | |
| return httpRequest<{ items: RedeemCode[] }>("/api/shop/codes"); | |
| } | |
| export async function fetchShopPublicConfig() { | |
| return httpRequest<{ purchase_url: string }>("/api/shop/public"); | |
| } | |
| export async function createRedeemCodes(body: { | |
| count: number; | |
| credits: number; | |
| prefix?: string; | |
| batch?: string; | |
| note?: string; | |
| validity_days?: number | null; | |
| credit_expires_days?: number | null; | |
| batch_single_use?: boolean; | |
| code_type?: string; | |
| daily_gift_date?: string; | |
| daily_gift_model_ids?: string[]; | |
| package_name?: string; | |
| package_duration_days?: number; | |
| package_daily_limit?: number | null; | |
| package_weekly_limit?: number | null; | |
| package_monthly_limit?: number | null; | |
| package_total_limit?: number | null; | |
| third_party_image_group_id?: string; | |
| third_party_image_model_id?: string; | |
| third_party_image_auto_enable?: boolean; | |
| }) { | |
| return httpRequest<{ codes: string[]; items: RedeemCode[]; all_items: RedeemCode[] }>("/api/shop/codes", { | |
| method: "POST", | |
| body, | |
| }); | |
| } | |
| export async function disableRedeemCode(codeId: string) { | |
| return httpRequest<{ item: RedeemCode; items: RedeemCode[] }>("/api/shop/codes/disable", { | |
| method: "POST", | |
| body: { code_id: codeId }, | |
| }); | |
| } | |
| export async function enableRedeemCode(codeId: string) { | |
| return httpRequest<{ item: RedeemCode; items: RedeemCode[] }>("/api/shop/codes/enable", { | |
| method: "POST", | |
| body: { code_id: codeId }, | |
| }); | |
| } | |
| export async function deleteRedeemCode(codeId: string) { | |
| return httpRequest<{ item: RedeemCode; items: RedeemCode[] }>("/api/shop/codes/delete", { | |
| method: "POST", | |
| body: { code_id: codeId }, | |
| }); | |
| } | |
| export async function deleteRedeemCodes(codeIds: string[]) { | |
| return httpRequest<{ | |
| deleted: number; | |
| deleted_ids: string[]; | |
| deleted_items: RedeemCode[]; | |
| }>("/api/shop/codes/delete-batch", { | |
| method: "POST", | |
| body: { code_ids: codeIds }, | |
| }); | |
| } | |
| export async function cleanupRedeemCodes(body: { | |
| delete_expired: boolean; | |
| delete_disabled: boolean; | |
| archive_redeemed: boolean; | |
| redeemed_older_than_days: number; | |
| limit: number; | |
| dry_run: boolean; | |
| }) { | |
| return httpRequest<RedeemCodeCleanupResponse>("/api/shop/codes/cleanup", { | |
| method: "POST", | |
| body, | |
| }); | |
| } | |
| export async function fetchShopLedger() { | |
| return httpRequest<{ items: ShopLedgerEntry[] }>("/api/shop/ledger"); | |
| } | |
| export async function adjustNormalUserCredits(keyId: string, delta: number, note = "") { | |
| return httpRequest<{ item: UserKey; items: UserKey[] }>(`/api/shop/users/${keyId}/credits`, { | |
| method: "POST", | |
| body: { delta, note }, | |
| }); | |
| } | |
| export async function adjustNormalUserImageWallet( | |
| keyId: string, | |
| groupId: string, | |
| modelId: string, | |
| delta: number, | |
| note = "", | |
| ) { | |
| return httpRequest<{ item: UserKey; items: UserKey[] }>(`/api/shop/users/${keyId}/third-party-image-wallet`, { | |
| method: "POST", | |
| body: { group_id: groupId, model_id: modelId, delta, note }, | |
| }); | |
| } | |
| let thirdPartyImageGroupsCache: | |
| | { expiresAt: number; data: { groups: ThirdPartyImageGroup[]; settings: ThirdPartyImageSettings } } | |
| | null = null; | |
| let thirdPartyImageGroupsRequest: Promise<{ groups: ThirdPartyImageGroup[]; settings: ThirdPartyImageSettings }> | null = null; | |
| export async function fetchThirdPartyImageGroups(options: { force?: boolean } = {}) { | |
| if (!options.force && thirdPartyImageGroupsCache && thirdPartyImageGroupsCache.expiresAt > Date.now()) { | |
| return thirdPartyImageGroupsCache.data; | |
| } | |
| if (!options.force && thirdPartyImageGroupsRequest) { | |
| return thirdPartyImageGroupsRequest; | |
| } | |
| thirdPartyImageGroupsRequest = httpRequest<{ groups: ThirdPartyImageGroup[]; settings: ThirdPartyImageSettings }>( | |
| "/api/third-party-images/groups", | |
| ).then((data) => { | |
| thirdPartyImageGroupsCache = { data, expiresAt: Date.now() + 30_000 }; | |
| return data; | |
| }).finally(() => { | |
| thirdPartyImageGroupsRequest = null; | |
| }); | |
| return thirdPartyImageGroupsRequest; | |
| } | |
| export async function saveThirdPartyImageGroups(groups: ThirdPartyImageGroup[], settings?: ThirdPartyImageSettings) { | |
| const data = await httpRequest<{ groups: ThirdPartyImageGroup[]; settings: ThirdPartyImageSettings }>("/api/third-party-images/groups", { | |
| method: "POST", | |
| body: { groups, settings }, | |
| }); | |
| thirdPartyImageGroupsCache = { data, expiresAt: Date.now() + 30_000 }; | |
| return data; | |
| } | |
| export async function adaptThirdPartyImageDocument(document: string, filename: string, baseUrl: string) { | |
| return httpRequest<ThirdPartyImageAdapterResult>("/api/third-party-images/adapt-document", { | |
| method: "POST", | |
| body: { document, filename, base_url: baseUrl }, | |
| }); | |
| } | |
| export async function onlineCallThirdPartyImageAdapter( | |
| groupId: string, | |
| connectionId: string, | |
| connection: Partial<ThirdPartyImageConnection>, | |
| adapterTemplate: ThirdPartyImageAdapterTemplate | null, | |
| requestBody: Record<string, unknown>, | |
| referenceImages: File[] = [], | |
| operation: "generate" | "edit" = "generate", | |
| callMode: "custom" | "openai" = "custom", | |
| ) { | |
| if (referenceImages.length > 0) { | |
| const formData = new FormData(); | |
| formData.append("connection", JSON.stringify(connection)); | |
| formData.append("adapter_template", JSON.stringify(adapterTemplate)); | |
| formData.append("request_body", JSON.stringify(requestBody)); | |
| formData.append("operation", operation); | |
| formData.append("call_mode", callMode); | |
| referenceImages.forEach((file) => formData.append("images", file)); | |
| return httpRequest<ThirdPartyImageOnlineCallResult>(`/api/third-party-images/groups/${groupId}/connections/${connectionId}/online-call`, { | |
| method: "POST", | |
| body: formData, | |
| }); | |
| } | |
| return httpRequest<ThirdPartyImageOnlineCallResult>(`/api/third-party-images/groups/${groupId}/connections/${connectionId}/online-call`, { | |
| method: "POST", | |
| body: { connection, adapter_template: adapterTemplate, request_body: requestBody, operation, call_mode: callMode }, | |
| }); | |
| } | |
| export async function fetchThirdPartyImageAdapterModels( | |
| groupId: string, | |
| connectionId: string, | |
| connection: Partial<ThirdPartyImageConnection>, | |
| adapterTemplate: ThirdPartyImageAdapterTemplate, | |
| ) { | |
| return httpRequest<{ models: string[] }>(`/api/third-party-images/groups/${groupId}/connections/${connectionId}/adapter-models`, { | |
| method: "POST", | |
| body: { connection, adapter_template: adapterTemplate }, | |
| }); | |
| } | |
| export async function syncThirdPartyImageModels(groupId: string) { | |
| return httpRequest<{ models: string[]; groups: ThirdPartyImageGroup[] }>(`/api/third-party-images/groups/${groupId}/sync-models`, { | |
| method: "POST", | |
| body: {}, | |
| }); | |
| } | |
| export async function syncThirdPartyImageConnectionModels(groupId: string, connectionId: string, connection?: Partial<ThirdPartyImageConnection>) { | |
| return httpRequest<{ models: string[]; groups: ThirdPartyImageGroup[] }>(`/api/third-party-images/groups/${groupId}/connections/${connectionId}/sync-models`, { | |
| method: "POST", | |
| body: { connection_id: connectionId, ...(connection ? { connection } : {}) }, | |
| }); | |
| } | |
| export async function testThirdPartyImageConnection(groupId: string, connectionId = "", connection?: Partial<ThirdPartyImageConnection>) { | |
| if (connectionId) { | |
| const data = await syncThirdPartyImageConnectionModels(groupId, connectionId, connection); | |
| return { | |
| result: { ok: true, status: 200, error: null, models: data.models }, | |
| groups: data.groups, | |
| }; | |
| } | |
| return httpRequest<{ result: { ok: boolean; status: number; error?: string | null; models?: string[] }; groups?: ThirdPartyImageGroup[] }>(`/api/third-party-images/groups/${groupId}/test`, { | |
| method: "POST", | |
| body: { connection_id: connectionId, ...(connection ? { connection } : {}) }, | |
| }); | |
| } | |
| export async function fetchThirdPartyImageOptions() { | |
| return httpRequest<{ models: ThirdPartyImageModelOption[] }>("/api/third-party-images/options"); | |
| } | |
| export async function fetchRegisterConfig() { | |
| return httpRequest<{ register: RegisterConfig }>("/api/register"); | |
| } | |
| export async function updateRegisterConfig(updates: Partial<RegisterConfig>) { | |
| return httpRequest<{ register: RegisterConfig }>("/api/register", { | |
| method: "POST", | |
| body: updates, | |
| }); | |
| } | |
| export async function startRegister() { | |
| return httpRequest<{ register: RegisterConfig }>("/api/register/start", { method: "POST" }); | |
| } | |
| export async function stopRegister() { | |
| return httpRequest<{ register: RegisterConfig }>("/api/register/stop", { method: "POST" }); | |
| } | |
| export async function resetRegister() { | |
| return httpRequest<{ register: RegisterConfig }>("/api/register/reset", { method: "POST" }); | |
| } | |
| // ── CPA (CLIProxyAPI) ────────────────────────────────────────────── | |
| export type CPAPool = { | |
| id: string; | |
| name: string; | |
| base_url: string; | |
| import_job?: CPAImportJob | null; | |
| }; | |
| export type CPARemoteFile = { | |
| name: string; | |
| email: string; | |
| }; | |
| export type CPAImportJob = { | |
| job_id: string; | |
| status: "pending" | "running" | "completed" | "failed"; | |
| created_at: string; | |
| updated_at: string; | |
| total: number; | |
| completed: number; | |
| added: number; | |
| skipped: number; | |
| refreshed: number; | |
| failed: number; | |
| errors: Array<{ name: string; error: string }>; | |
| }; | |
| export async function fetchCPAPools() { | |
| return httpRequest<{ pools: CPAPool[] }>("/api/cpa/pools"); | |
| } | |
| export async function createCPAPool(pool: { name: string; base_url: string; secret_key: string }) { | |
| return httpRequest<{ pool: CPAPool; pools: CPAPool[] }>("/api/cpa/pools", { | |
| method: "POST", | |
| body: pool, | |
| }); | |
| } | |
| export async function updateCPAPool( | |
| poolId: string, | |
| updates: { name?: string; base_url?: string; secret_key?: string }, | |
| ) { | |
| return httpRequest<{ pool: CPAPool; pools: CPAPool[] }>(`/api/cpa/pools/${poolId}`, { | |
| method: "POST", | |
| body: updates, | |
| }); | |
| } | |
| export async function deleteCPAPool(poolId: string) { | |
| return httpRequest<{ pools: CPAPool[] }>(`/api/cpa/pools/${poolId}`, { | |
| method: "DELETE", | |
| }); | |
| } | |
| export async function fetchCPAPoolFiles(poolId: string) { | |
| return httpRequest<{ pool_id: string; files: CPARemoteFile[] }>(`/api/cpa/pools/${poolId}/files`); | |
| } | |
| export async function startCPAImport(poolId: string, names: string[]) { | |
| return httpRequest<{ import_job: CPAImportJob | null }>(`/api/cpa/pools/${poolId}/import`, { | |
| method: "POST", | |
| body: { names }, | |
| }); | |
| } | |
| export async function fetchCPAPoolImportJob(poolId: string) { | |
| return httpRequest<{ import_job: CPAImportJob | null }>(`/api/cpa/pools/${poolId}/import`); | |
| } | |
| // ── Sub2API ──────────────────────────────────────────────────────── | |
| export type Sub2APIServer = { | |
| id: string; | |
| name: string; | |
| base_url: string; | |
| email: string; | |
| has_api_key: boolean; | |
| group_id: string; | |
| import_job?: CPAImportJob | null; | |
| }; | |
| export type Sub2APIRemoteAccount = { | |
| id: string; | |
| name: string; | |
| email: string; | |
| plan_type: string; | |
| status: string; | |
| expires_at: string; | |
| has_refresh_token: boolean; | |
| }; | |
| export type Sub2APIRemoteGroup = { | |
| id: string; | |
| name: string; | |
| description: string; | |
| platform: string; | |
| status: string; | |
| account_count: number; | |
| active_account_count: number; | |
| }; | |
| export async function fetchSub2APIServers() { | |
| return httpRequest<{ servers: Sub2APIServer[] }>("/api/sub2api/servers"); | |
| } | |
| export async function createSub2APIServer(server: { | |
| name: string; | |
| base_url: string; | |
| email: string; | |
| password: string; | |
| api_key: string; | |
| group_id: string; | |
| }) { | |
| return httpRequest<{ server: Sub2APIServer; servers: Sub2APIServer[] }>("/api/sub2api/servers", { | |
| method: "POST", | |
| body: server, | |
| }); | |
| } | |
| export async function updateSub2APIServer( | |
| serverId: string, | |
| updates: { | |
| name?: string; | |
| base_url?: string; | |
| email?: string; | |
| password?: string; | |
| api_key?: string; | |
| group_id?: string; | |
| }, | |
| ) { | |
| return httpRequest<{ server: Sub2APIServer; servers: Sub2APIServer[] }>(`/api/sub2api/servers/${serverId}`, { | |
| method: "POST", | |
| body: updates, | |
| }); | |
| } | |
| export async function fetchSub2APIServerGroups(serverId: string) { | |
| return httpRequest<{ server_id: string; groups: Sub2APIRemoteGroup[] }>( | |
| `/api/sub2api/servers/${serverId}/groups`, | |
| ); | |
| } | |
| export async function deleteSub2APIServer(serverId: string) { | |
| return httpRequest<{ servers: Sub2APIServer[] }>(`/api/sub2api/servers/${serverId}`, { | |
| method: "DELETE", | |
| }); | |
| } | |
| export async function fetchSub2APIServerAccounts(serverId: string) { | |
| return httpRequest<{ server_id: string; accounts: Sub2APIRemoteAccount[] }>( | |
| `/api/sub2api/servers/${serverId}/accounts`, | |
| ); | |
| } | |
| export async function startSub2APIImport(serverId: string, accountIds: string[]) { | |
| return httpRequest<{ import_job: CPAImportJob | null }>(`/api/sub2api/servers/${serverId}/import`, { | |
| method: "POST", | |
| body: { account_ids: accountIds }, | |
| }); | |
| } | |
| export async function fetchSub2APIImportJob(serverId: string) { | |
| return httpRequest<{ import_job: CPAImportJob | null }>(`/api/sub2api/servers/${serverId}/import`); | |
| } | |
| // ── Upstream proxy ──────────────────────────────────────────────── | |
| export type ProxySettings = { | |
| enabled: boolean; | |
| url: string; | |
| }; | |
| export type ProxyTestResult = { | |
| ok: boolean; | |
| status: number; | |
| latency_ms: number; | |
| error: string | null; | |
| proxy_source?: string; | |
| }; | |
| export type ClearanceTestResult = { | |
| ok: boolean; | |
| status: number | string; | |
| latency_ms: number; | |
| has_cookies?: boolean; | |
| error: string | null; | |
| user_agent?: string; | |
| runtime: ProxyRuntimeStatus; | |
| }; | |
| export async function fetchProxy() { | |
| return httpRequest<{ proxy: ProxySettings }>("/api/proxy"); | |
| } | |
| export async function updateProxy(updates: { enabled?: boolean; url?: string }) { | |
| return httpRequest<{ proxy: ProxySettings }>("/api/proxy", { | |
| method: "POST", | |
| body: updates, | |
| }); | |
| } | |
| export async function testProxy(url?: string) { | |
| return httpRequest<{ result: ProxyTestResult }>("/api/proxy/test", { | |
| method: "POST", | |
| body: { url: url ?? "" }, | |
| }); | |
| } | |
| export async function previewPoolPricing( | |
| config: PoolPricingConfig, | |
| at: string, | |
| quantity = 1, | |
| targetType: "pool" | "third_party_group" = "pool", | |
| thirdPartyGroupId = "", | |
| baseUnitPrice = 1, | |
| ) { | |
| return httpRequest<{ | |
| validation: PoolPricingValidation; | |
| quote: PoolPricingQuote | null; | |
| calendar: PoolPricingCalendarStatus; | |
| }>("/api/settings/pool-pricing/preview", { | |
| method: "POST", | |
| body: { | |
| config, | |
| at, | |
| quantity, | |
| target_type: targetType, | |
| third_party_group_id: thirdPartyGroupId, | |
| base_unit_price: baseUnitPrice, | |
| }, | |
| }); | |
| } | |
| export async function refreshPoolPricingCalendar() { | |
| return httpRequest<{ calendar: PoolPricingCalendarStatus }>("/api/settings/pool-pricing/calendar/refresh", { | |
| method: "POST", | |
| body: {}, | |
| }); | |
| } | |
| export async function fetchProxyRuntime() { | |
| return httpRequest<{ runtime: ProxyRuntimeSettings; status: ProxyRuntimeStatus }>("/api/proxy/runtime"); | |
| } | |
| export async function updateProxyRuntime(runtime: ProxyRuntimeSettings) { | |
| return httpRequest<{ runtime: ProxyRuntimeSettings; status: ProxyRuntimeStatus }>("/api/proxy/runtime", { | |
| method: "POST", | |
| body: runtime, | |
| }); | |
| } | |
| export async function testProxyClearance(targetUrl?: string) { | |
| return httpRequest<{ result: ClearanceTestResult }>("/api/proxy/clearance/test", { | |
| method: "POST", | |
| body: { target_url: targetUrl ?? "https://chatgpt.com" }, | |
| }); | |
| } | |
| /** 签到页展示文案(后台可改,改完无需重新部署)。空字符串表示不显示该段。 */ | |
| export type CheckinContent = { | |
| notice: string; | |
| dialog_text: string; | |
| dialog_footer: string; | |
| qrcode_url: string; | |
| }; | |
| /** 与后端 DEFAULT_CHECKIN_CONTENT 保持一致:接口未返回字段时用它兜底。 */ | |
| export const DEFAULT_CHECKIN_CONTENT: CheckinContent = { | |
| notice: "奖励积分一部分来源公众号广告费,大家有时间可以帮忙点广告,每天闲了点几下,赚腾讯给钱给大家增加签到奖励的积分。", | |
| dialog_text: "打扰啦,辛苦有空逛逛公众号,留言点广告腾讯会给广告费", | |
| dialog_footer: "仅手机点广告评论才有收益,当前赠送积分已超广告收入\n基金亏麻了,天才交易员陨落,暂不能自费给大家送太多积分", | |
| qrcode_url: "https://wx.z-l.top/qrcode.png", | |
| }; | |
| export type CheckinRecord = { date: string; reward: number; mode: "checkin" | "random"; expires_at?: string }; | |
| export type CheckinRankingItem = { name: string; avatar_url?: string; wechat_avatar?: string; reward: number; days: number }; | |
| export type CheckinInvite = { code?: string; successful: number; rewards: number; records: Array<Record<string, string | number>> }; | |
| export type CheckinSummary = { | |
| today: string; | |
| checked_in: boolean; | |
| records: CheckinRecord[]; | |
| daily: CheckinRankingItem[]; | |
| total: CheckinRankingItem[]; | |
| invite: CheckinInvite; | |
| total_rewards?: number; | |
| /** 签到积分可用天数;0(默认)= 永久有效。 */ | |
| credit_valid_days?: number; | |
| ads?: Array<{ title?: string; image_url?: string; link_url?: string; enabled?: boolean; layout?: "banner" | "small_square" | "square" | string }>; | |
| content?: Partial<CheckinContent>; | |
| credits: { credits?: number; daily_credits_remaining?: number; [key: string]: unknown }; | |
| }; | |
| /** 报错文案映射规则:命中任意关键词,就把原始技术报错替换成 display(大白话)。 */ | |
| export type ErrorMessageRule = { | |
| id: string; | |
| /** 关键词列表;报错原文(小写)包含任意一个即命中。 */ | |
| keywords: string[]; | |
| /** 命中后展示给用户的文案(大白话)。 */ | |
| display: string; | |
| enabled: boolean; | |
| note?: string; | |
| }; | |
| /** 一条「未配置报错」:命中不了任何规则、被自动收集下来等待补规则。 */ | |
| export type UnconfiguredError = { | |
| fingerprint: string; | |
| /** 报错原文样本(可能被截断)。 */ | |
| sample: string; | |
| count: number; | |
| /** 来源链路,如 api / image_task / editable_file_task。 */ | |
| channel?: string; | |
| first_seen?: string; | |
| last_seen?: string; | |
| /** 后端从小样本里抽出的候选关键词,可直接用于新建规则。 */ | |
| keywords?: string[]; | |
| }; | |
| export async function fetchUnconfiguredErrors() { | |
| return httpRequest<{ items: UnconfiguredError[] }>("/api/error-messages/unconfigured"); | |
| } | |
| /** 清空未配置报错;传 fingerprints 只清指定的几条,不传则全清。 */ | |
| export async function clearUnconfiguredErrors(fingerprints?: string[]) { | |
| return httpRequest<{ removed: number; items: UnconfiguredError[] }>("/api/error-messages/unconfigured/clear", { | |
| method: "POST", | |
| body: { fingerprints: fingerprints ?? [] }, | |
| }); | |
| } | |
| /** 「AI 归纳」的结果:关键词 + 大白话文案。source 标明是模型给还是本地兜底抽取。 */ | |
| export type ErrorMessageSuggestion = { | |
| keywords: string[]; | |
| display: string; | |
| source: "ai" | "local"; | |
| /** 实际使用的模型名(source=ai 时有值)。 */ | |
| model?: string; | |
| /** 降级到本地抽取的原因(source=local 时有值)。 */ | |
| reason?: string; | |
| }; | |
| /** 粘贴一条报错原文,让模型归纳出可用的规则(关键词 + 大白话)。 */ | |
| export async function summarizeErrorMessage(text: string) { | |
| return httpRequest<ErrorMessageSuggestion>("/api/error-messages/summarize", { | |
| method: "POST", | |
| body: { text }, | |
| }); | |
| } | |
| export async function fetchCheckin() { | |
| return httpRequest<CheckinSummary>("/api/checkin"); | |
| } | |
| export async function submitCheckin(mode: "checkin" | "random") { | |
| return httpRequest<CheckinSummary & { result: CheckinRecord }>("/api/checkin", { | |
| method: "POST", | |
| body: { mode }, | |
| }); | |
| } | |
| export type AdminCheckinSummary = { | |
| normal_users: number; | |
| today_checked_in: number; | |
| total_rewards: number; | |
| /** | |
| * 签到累计发放中尚未被消耗的积分合计(精确账本,仅统计该功能上线后的发放)。 | |
| * 含两种形态:没设可用天数的永久部分(从基础积分池扣费时冲减)+ 设了可用天数的限时券未用余额。 | |
| */ | |
| unused_credits: number; | |
| ads?: CheckinSummary["ads"]; | |
| }; | |
| export async function fetchAdminCheckin() { | |
| return httpRequest<AdminCheckinSummary>("/api/admin/checkin"); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // 聊天室(群聊) | |
| // --------------------------------------------------------------------------- | |
| export type ChatRoomReplyRef = { | |
| id: number; | |
| name: string; | |
| excerpt: string; | |
| }; | |
| export type ChatRoomMessage = { | |
| id: number; | |
| created_at: number; | |
| identity_id: string; | |
| role: AuthRole; | |
| name: string; | |
| avatar_url: string; | |
| content: string; | |
| /** "public" = 公共大厅;"dm:a|b" = 私聊会话(由服务端计算,前端只读)。 */ | |
| conversation?: string; | |
| /** 图片消息的地址,纯文本消息为空串。 */ | |
| image_url?: string; | |
| /** 引用回复的目标消息快照(昵称 + 摘要),未引用时为 null。 */ | |
| reply_to?: ChatRoomReplyRef | null; | |
| /** 是否已被撤回 / 删除。为 true 时 content 与 image_url 已被服务端抹空。 */ | |
| deleted?: boolean; | |
| /** 删除时间戳(秒)。未删除为 0。 */ | |
| deleted_at?: number; | |
| /** 执行删除者的身份 id —— 前端据此区分「我撤回的」与「管理员删的」。 */ | |
| deleted_by?: string; | |
| /** 是否被管理员置顶(公告)。撤回的消息会同时清掉置顶。 */ | |
| pinned?: boolean; | |
| /** 置顶时间戳(秒)。未置顶为 0;多个置顶按它倒序。 */ | |
| pinned_at?: number; | |
| }; | |
| export type ChatRoomMessages = { | |
| messages: ChatRoomMessage[]; | |
| has_more: boolean; | |
| conversation?: string; | |
| is_direct?: boolean; | |
| online?: PresenceSnapshot; | |
| /** 该会话的置顶消息(仅公共大厅有)。 */ | |
| pinned?: ChatRoomMessage[]; | |
| }; | |
| /** 私聊收件箱里的一项:与我聊过天的某个人。 */ | |
| export type ChatRoomDirectConversation = { | |
| conversation: string; | |
| peer_id: string; | |
| peer_name: string; | |
| peer_avatar_url: string; | |
| /** 该会话最后一条消息。 */ | |
| last_message: ChatRoomMessage; | |
| /** 对方发来的、我未读的条数参考值。 */ | |
| total: number; | |
| /** 对方当前是否在线。 */ | |
| peer_online?: boolean; | |
| }; | |
| /** 我与某人的好友关系:friends=已是好友,outgoing=我已申请,incoming=对方申请我,none=无关系。 */ | |
| export type FriendRelation = "friends" | "outgoing" | "incoming" | "none"; | |
| export type ChatRoomOnlineUser = { | |
| id: string; | |
| role: AuthRole; | |
| name: string; | |
| avatar_url: string; | |
| /** 服务端判定「我能否与该用户私聊」——目前仅管理员为 true。 */ | |
| can_direct?: boolean; | |
| }; | |
| export type ChatRoomOnline = { | |
| users: ChatRoomOnlineUser[]; | |
| total: number; | |
| /** 我是否有私聊权限。 */ | |
| can_direct?: boolean; | |
| }; | |
| export type FriendRequestItem = { | |
| id: number; | |
| from_id: string; | |
| to_id: string; | |
| status: string; | |
| note: string; | |
| created_at: number; | |
| updated_at: number; | |
| from_name?: string; | |
| }; | |
| export type ChatRoomFriends = { | |
| friends: string[]; | |
| incoming: FriendRequestItem[]; | |
| outgoing: FriendRequestItem[]; | |
| }; | |
| export type ChatRoomSummary = { | |
| total: number; | |
| speakers: number; | |
| conversation?: string; | |
| is_direct?: boolean; | |
| online?: PresenceSnapshot; | |
| me?: { id?: string; name?: string; role?: AuthRole; avatar_url?: string }; | |
| }; | |
| /** | |
| * 拉取聊天室消息。 | |
| * `afterId=0` 取最新一页(首次进入);`afterId>0` 只取增量(轮询)。 | |
| * `peerId` 非空时读取与对方的私聊会话。 | |
| */ | |
| export async function fetchChatRoomMessages(afterId = 0, limit = 50, peerId = "") { | |
| const params = new URLSearchParams({ after_id: String(afterId), limit: String(limit) }); | |
| if (peerId) params.set("peer_id", peerId); | |
| return httpRequest<ChatRoomMessages>(`/api/chat-room/messages?${params.toString()}`); | |
| } | |
| export async function fetchChatRoomSummary(peerId = "") { | |
| const params = new URLSearchParams(); | |
| if (peerId) params.set("peer_id", peerId); | |
| const query = params.toString(); | |
| return httpRequest<ChatRoomSummary>(`/api/chat-room/summary${query ? `?${query}` : ""}`); | |
| } | |
| /** 未读角标:公共大厅里比本地已读位置更新的、别人发的消息数。 */ | |
| export async function fetchChatRoomUnread(afterId = 0) { | |
| const params = new URLSearchParams({ after_id: String(afterId) }); | |
| return httpRequest<{ unread: number; latest_id: number }>( | |
| `/api/chat-room/unread?${params.toString()}`, | |
| ); | |
| } | |
| /** 在线用户列表(左侧栏)。不包含自己,并带 relation 标注好友关系。 */ | |
| export async function fetchChatRoomOnline() { | |
| return httpRequest<ChatRoomOnline>("/api/chat-room/online"); | |
| } | |
| /** 我的好友 id 列表 + 待处理的好友申请。 */ | |
| export async function fetchChatRoomFriends() { | |
| return httpRequest<ChatRoomFriends>("/api/chat-room/friends"); | |
| } | |
| /** 发起好友申请。 */ | |
| export async function requestChatRoomFriend(peerId: string, note = "") { | |
| return httpRequest<{ status: string; auto?: boolean }>("/api/chat-room/friends/request", { | |
| method: "POST", | |
| body: { peer_id: peerId, note }, | |
| }); | |
| } | |
| /** 同意 / 拒绝收到的申请。 */ | |
| export async function respondChatRoomFriend(peerId: string, accept: boolean) { | |
| return httpRequest<{ status: string }>("/api/chat-room/friends/respond", { | |
| method: "POST", | |
| body: { peer_id: peerId, accept }, | |
| }); | |
| } | |
| /** 解除好友关系。 */ | |
| export async function removeChatRoomFriend(peerId: string) { | |
| return httpRequest<{ removed: boolean }>("/api/chat-room/friends/remove", { | |
| method: "POST", | |
| body: { peer_id: peerId }, | |
| }); | |
| } | |
| export type SendChatRoomMessageInput = { | |
| content: string; | |
| peerId?: string; | |
| replyToId?: number; | |
| imageUrl?: string; | |
| }; | |
| export async function sendChatRoomMessage(input: SendChatRoomMessageInput | string) { | |
| const payload: SendChatRoomMessageInput = | |
| typeof input === "string" ? { content: input } : input; | |
| return httpRequest<{ message: ChatRoomMessage }>("/api/chat-room/messages", { | |
| method: "POST", | |
| body: { | |
| content: payload.content, | |
| peer_id: payload.peerId || "", | |
| reply_to_id: payload.replyToId || 0, | |
| image_url: payload.imageUrl || "", | |
| }, | |
| }); | |
| } | |
| /** 撤回 / 删除一条聊天消息。 | |
| * | |
| * ⚠️ 谁有权删由**服务端**判定:作者本人 2 分钟内可撤回,管理员可删任意消息。 | |
| * 前端按同样规则决定是否渲染按钮,但不要拿它当权限依据。 | |
| */ | |
| export async function deleteChatRoomMessage(messageId: number) { | |
| return httpRequest<{ message: ChatRoomMessage }>( | |
| `/api/chat-room/messages/${messageId}`, | |
| { method: "DELETE" }, | |
| ); | |
| } | |
| /** 置顶 / 取消置顶一条消息(仅管理员可用,服务端强制)。 */ | |
| export async function pinChatRoomMessage(messageId: number, pinned = true) { | |
| return httpRequest<{ message: ChatRoomMessage }>( | |
| `/api/chat-room/messages/${messageId}/pin`, | |
| { | |
| method: "POST", | |
| // ⚠️ 传**原始对象**,不要 JSON.stringify —— httpRequest 直接把 body 交给 axios 的 data, | |
| // 传字符串时 axios 不会自动补 Content-Type: application/json, | |
| // FastAPI 就把它当非 JSON body → 读不到 body → 422 Field required。 | |
| body: { pinned }, | |
| }, | |
| ); | |
| } | |
| /** 我的私聊收件箱:所有与我聊过天的会话。 | |
| * | |
| * `mark` 是本地已读位置(`peer_id:last_id,...`)—— ⚠️ 已读状态**存在浏览器本地**, | |
| * 服务端不替用户记,只按这个位置数「还没读的有几条」。 | |
| */ | |
| export async function fetchChatRoomDirectConversations(mark = "") { | |
| const query = mark ? `?mark=${encodeURIComponent(mark)}` : ""; | |
| return httpRequest<{ items: ChatRoomDirectConversation[] }>( | |
| `/api/chat-room/direct-conversations${query}`, | |
| ); | |
| } | |
| /** 上传聊天图片,返回可直接放进消息发送的地址。 */ | |
| export async function uploadChatRoomImage(file: File) { | |
| const form = new FormData(); | |
| form.append("file", file); | |
| return httpRequest<{ url: string }>("/api/chat-room/uploads", { | |
| method: "POST", | |
| body: form, | |
| }); | |
| } | |