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>; 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 { const response = await request.get("/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(`/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 { const response = await request.get(`/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(`/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(`/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(`/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("/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>; }; 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; /** 报错文案映射规则:命中关键词就把技术报错替换成大白话,只影响前台展示。 */ 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 | 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; [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; 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; sizes?: string[]; capabilities?: Array<"txt2img" | "img2img" | string>; aspect_ratios?: string[]; aspect_ratio_sizes?: Record; costs?: Record; 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; 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; failure_values?: Array; }; }; 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; 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>; }; 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("/auth/login", { method: "POST", body: {}, headers: { Authorization: `Bearer ${normalizedAuthKey}`, }, redirectOnUnauthorized: false, }); } export async function refreshSession(authKey: string) { const normalizedAuthKey = String(authKey || "").trim(); return httpRequest("/auth/session", { method: "POST", body: {}, headers: { Authorization: `Bearer ${normalizedAuthKey}`, }, redirectOnUnauthorized: false, }); } export async function fetchAccounts() { return httpRequest("/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("/api/accounts", { method: "POST", body: { tokens, ...(accounts.length > 0 ? { accounts } : {}), }, }); } export async function startAccountEmailLoginImport(email: string) { return httpRequest("/api/accounts/email-login/start", { method: "POST", body: { email }, }); } export async function verifyAccountEmailLoginImport(sessionId: string, code: string) { return httpRequest("/api/accounts/email-login/verify", { method: "POST", body: { session_id: sessionId, code }, }); } export async function deleteAccounts(tokens: string[]) { return httpRequest("/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("/api/accounts/re-login", { method: "POST", body: { access_token: accessToken }, }); } export async function verifyAccountPasswordRelogin(reloginId: string, code: string) { return httpRequest("/api/accounts/re-login/verify", { method: "POST", body: { relogin_id: reloginId, code }, }); } export async function fetchModels() { return httpRequest("/v1/models"); } /** * 对话页模型下拉的数据源。 * * `/v1/models` 是 OpenAI 兼容的全量接口(含第三方生图模型),不能拿来当对话模型用, * 所以对话页改用它:后端已经按结构化规则过滤掉生图/工具类模型。 */ export async function fetchChatModels() { return httpRequest("/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("/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("/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("/api/accounts/oauth/start", { method: "POST", body: { email_hint: emailHint ?? "" }, }); } export async function finishOAuthLogin(sessionId: string, callback: string) { return httpRequest("/api/accounts/oauth/finish", { method: "POST", body: { session_id: sessionId, callback }, }); } export async function fetchAccountRefreshProgress(progressId: string) { return httpRequest(`/api/accounts/refresh/progress/${encodeURIComponent(progressId)}`); } export async function runUpstreamAccountActions( accessTokens: string[], options: UpstreamAccountActionOptions, ) { return httpRequest("/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({ 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("/api/accounts/update", { method: "POST", body: { access_token: accessToken, ...updates, }, }); } export async function generateImage(prompt: string, model?: ImageModel, size?: string) { return httpRequest( "/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( "/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("/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("/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(`/api/image-tasks${params.toString() ? `?${params.toString()}` : ""}`, { signal }); } export async function resumeImageTaskPoll(taskId: string, extraTimeoutSecs?: number) { return httpRequest(`/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(`/api/image-tasks/share-evidence?${params.toString()}`, { signal }); } export async function createImageShareEvidence(conversationId: string) { return httpRequest("/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(`/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("/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( `/api/product-visual/tasks${params.toString() ? `?${params.toString()}` : ""}`, { signal }, ); } export async function retryProductVisualItem(taskId: string, seq: number) { return httpRequest(`/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(`/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({ 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("/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("/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("/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("/api/presence/ping", { ...PRESENCE_REQUEST_OPTIONS, method: "POST", }); } export async function fetchPresence() { return httpRequest("/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 { 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(`/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("/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("/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("/api/third-party-images/adapt-document", { method: "POST", body: { document, filename, base_url: baseUrl }, }); } export async function onlineCallThirdPartyImageAdapter( groupId: string, connectionId: string, connection: Partial, adapterTemplate: ThirdPartyImageAdapterTemplate | null, requestBody: Record, 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(`/api/third-party-images/groups/${groupId}/connections/${connectionId}/online-call`, { method: "POST", body: formData, }); } return httpRequest(`/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, 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) { 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) { 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) { 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> }; 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; 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("/api/error-messages/summarize", { method: "POST", body: { text }, }); } export async function fetchCheckin() { return httpRequest("/api/checkin"); } export async function submitCheckin(mode: "checkin" | "random") { return httpRequest("/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("/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(`/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(`/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("/api/chat-room/online"); } /** 我的好友 id 列表 + 待处理的好友申请。 */ export async function fetchChatRoomFriends() { return httpRequest("/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, }); }