li / web /src /lib /api.ts
xiaolongmr
聊天室:修复置顶 422,未读角标改挂到用户行
ca6cef4
Raw History Blame Contribute Delete
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,
});
}