Files
ai_site/web/src/api.ts
whm 680d2b8cde feat: add sync checkpoint restore for last successful sync
Capture rolling online DB snapshots after push/drain/reconcile and expose SyncPage 数据恢复 plus checkpoint/restore APIs; mark Z12h and restore done in coop docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 11:49:00 +08:00

1708 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getActivePreview } from "./preview";
import { normalizeRole } from "./agentPerms";
const PLATFORM = "";
export const AI = "/ai";
const SESSION_KEY = "ajz_session";
export type Session = {
accessToken: string;
agentKey: string;
tenantId: number;
userId: number;
username?: string;
phone?: string;
usernameLoginDisabled?: boolean;
displayName?: string;
role?: string;
orgUnitId?: number;
status?: string; // pending | active
expiresAt?: number;
/** 超管打开某公司管理视图时的公司名(身份仍是超管) */
tenantName?: string;
};
export function isPendingMembership(s: Session | null | undefined): boolean {
if (!s) return false;
const role = normalizeRole(s.role || "");
// 平台超管无租户,绝不能当成「待加入」
if (role === "超级管理员") return false;
if (s.username === "ljk_admin") return false;
if (role === "待加入" || s.status === "pending") return true;
// status=active 且无租户:仍可能是待入驻注册用户
if (!s.tenantId) return true;
return false;
}
function roleFromAccessToken(token: string | undefined): string {
if (!token || token.split(".").length < 2) return "";
try {
const b64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
const pad = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
const bin = atob(pad);
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
const json = JSON.parse(new TextDecoder().decode(bytes));
return typeof json?.role === "string" ? json.role : "";
} catch {
return "";
}
}
/** emit=false仅清存储如 loadSession 发现坏 token不踢路由——避免毁掉 /#/preview */
export function clearSession(opts?: { emit?: boolean }) {
try {
localStorage.removeItem(SESSION_KEY);
} catch {
/* ignore */
}
if (opts?.emit === false) return;
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("ajz:session-expired"));
}
}
export function loadSession(): Session | null {
try {
const raw = localStorage.getItem(SESSION_KEY);
if (!raw) return null;
const s = JSON.parse(raw) as Session;
if (!s?.accessToken || typeof s.accessToken !== "string" || s.accessToken.split(".").length !== 3) {
clearSession({ emit: false });
return null;
}
if (s.expiresAt && s.expiresAt * 1000 < Date.now() - 60_000) {
clearSession({ emit: false });
return null;
}
if (!s.role) {
s.role = roleFromAccessToken(s.accessToken);
}
return s;
} catch {
clearSession({ emit: false });
return null;
}
}
export function saveSession(s: Session) {
localStorage.setItem(SESSION_KEY, JSON.stringify(s));
}
function toSession(data: any): Session {
const accessToken = data.access_token;
const s: Session = {
accessToken,
agentKey: data.agent_key,
tenantId: data.tenant_id || 0,
userId: data.user_id,
username: data.username,
phone: data.phone || undefined,
usernameLoginDisabled: !!data.username_login_disabled,
displayName: data.display_name,
role: data.role || roleFromAccessToken(accessToken),
orgUnitId: data.org_unit_id || 0,
status: data.status,
expiresAt: data.expires_at,
tenantName: data.tenant_name || undefined,
};
if (!s.accessToken) {
throw new Error("登录响应缺少 access_token");
}
return s;
}
function networkError(err: unknown): Error {
const msg = String((err as any)?.message || err);
if (/failed to fetch|networkerror|load failed|network request failed/i.test(msg)) {
return new Error("无法连接网关或前端代理,请确认服务已启动后刷新页面");
}
return err instanceof Error ? err : new Error(msg);
}
function authErrorMessage(data: any, status: number): string {
const msg = String(data?.message || data?.msg || data?.error || "");
// 登录/注册接口本身的 401 只表示账号密码错误,不能清掉会话事件循环
if (status === 401 || /invalid token|malformed token|expired|missing bearer/i.test(msg)) {
if (/invalid username or password|账号已停用|login failed/i.test(msg)) {
return msg || "用户名或密码错误";
}
clearSession();
return msg || "登录已失效,请重新登录";
}
return msg;
}
async function apiFetch(input: string, init?: RequestInit): Promise<Response> {
try {
return await fetch(input, init);
} catch (e) {
throw networkError(e);
}
}
async function readJson(res: Response): Promise<any> {
const text = await res.text();
if (!text) return {};
try {
return JSON.parse(text);
} catch {
throw new Error(res.ok ? "响应不是 JSON" : `请求失败 HTTP ${res.status}`);
}
}
export async function login(username: string, password: string): Promise<Session> {
return loginWith({ username: username.trim(), password });
}
export async function loginWith(body: {
username?: string;
phone?: string;
password?: string;
sms_code?: string;
}): Promise<Session> {
const res = await apiFetch(`${PLATFORM}/api/v1/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await readJson(res);
if (!res.ok) {
throw new Error(data.message || (res.status === 401 ? "登录失败" : "login failed"));
}
return toSession(data);
}
export async function sendLoginSMS(phone: string) {
const res = await apiFetch(`${PLATFORM}/api/v1/auth/sms/send`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone: phone.trim(), purpose: "login" }),
});
const data = await readJson(res);
throwIfBad(res, data, "send sms failed");
return data as {
ok: boolean;
expires_in: number;
retry_after?: number;
message?: string;
debug_code?: string;
};
}
export async function register(username: string, password: string, displayName: string): Promise<Session> {
const res = await apiFetch(`${PLATFORM}/api/v1/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password, display_name: displayName }),
});
const data = await readJson(res);
if (!res.ok) throw new Error(data.message || "register failed");
return toSession(data);
}
export async function listLlmProviders() {
const res = await apiFetch(`${AI}/api/v1/llm/providers`);
const data = await readJson(res);
if (!res.ok) throw new Error(JSON.stringify(data));
return data as {
default: string;
providers: {
id: string;
label: string;
configured: boolean;
default_model: string;
models: { id: string; label: string }[];
supports_vision: boolean;
}[];
};
}
export async function generateDraft(
prompt: string,
excel?: File | null,
images?: File[],
storageMode: "schema_per_app" | "database_per_app" = "schema_per_app",
llmProvider = "deepseek",
llmModel = "",
layoutFiles?: File[]
) {
const fd = new FormData();
fd.append("prompt", prompt);
fd.append("storage_mode", storageMode);
fd.append("llm_provider", llmProvider);
if (llmModel) fd.append("llm_model", llmModel);
if (excel) fd.append("excel", excel);
for (const img of images || []) {
fd.append("images", img);
}
for (const f of layoutFiles || []) {
fd.append("layout_files", f);
}
const res = await apiFetch(`${AI}/api/v1/apps/generate`, { method: "POST", body: fd });
const data = await readJson(res);
if (!res.ok) throw new Error(JSON.stringify(data));
return data as {
draft: any;
warnings: string[];
confidence: number;
require_confirm: boolean;
llm_provider?: string;
llm_model?: string;
fidelity?: {
skipped?: boolean;
reason?: string;
target?: number;
max_rounds?: number;
final_score?: number;
passed?: boolean;
rounds?: { round: number; score?: number; pass?: boolean; fails?: string[]; passes?: string[] }[];
};
generate_log?: {
run_id?: string;
started_at?: string;
log_file?: string;
lines?: { ts?: string; level?: string; message?: string; [k: string]: unknown }[];
};
};
}
function throwIfBad(res: Response, data: any, fallback: string): void {
if (res.ok) return;
const msg = authErrorMessage(data, res.status) || fallback || `请求失败 (HTTP ${res.status})`;
throw new Error(msg);
}
export async function listApps(session: Session) {
const res = await apiFetch(`${PLATFORM}/api/v1/apps`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "列出模块失败");
return data as {
scope?: string;
items: {
app_id: string;
slug: string;
name: string;
status: string;
status_label?: string;
building?: boolean;
schema_name?: string;
page_count: number;
entity_count: number;
updated_at: string;
created_at: string;
}[];
};
}
/** Z9e为本租户已发布模块补齐 import/export一键开启导入 */
export async function ensureAppsImport(session: Session, dryRun = false) {
const q = dryRun ? "?dry_run=1" : "";
const res = await apiFetch(`${PLATFORM}/api/v1/admin/apps/ensure-import${q}`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "一键开启导入失败");
return data as {
scanned: number;
updated: number;
skipped: number;
dry_run: boolean;
updated_slugs?: string[];
message?: string;
};
}
export async function saveDraft(session: Session, slug: string, blueprint: any) {
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/draft`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify({ blueprint }),
});
const data = await readJson(res);
throwIfBad(res, data, "登记在建模块失败");
return data;
}
export async function publish(
session: Session,
slug: string,
blueprint: any,
mode: "auto" | "add_pages" | "create" | "replace" | "merge" = "auto",
) {
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/publish`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify({ blueprint, mode: mode === "merge" ? "add_pages" : mode }),
});
const data = await readJson(res);
throwIfBad(res, data, "发布失败");
return data;
}
export async function getBlueprint(session: Session | null | undefined, slug: string) {
if (session?.accessToken) {
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/blueprint`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "blueprint failed");
return data;
}
const res = await apiFetch(`${PLATFORM}/api/v1/public/apps/${slug}/blueprint`);
const data = await readJson(res);
throwIfBad(res, data, "blueprint failed");
return data;
}
export async function listRows(
session: Session | null | undefined,
slug: string,
resource: string,
filters?: Record<string, string>,
page = 1,
pageSize = 500
) {
const q = new URLSearchParams();
q.set("page", String(page));
q.set("page_size", String(pageSize));
if (filters) {
for (const [k, v] of Object.entries(filters)) {
if (v) q.set(`filter.${k}`, v);
}
}
const qs = q.toString();
const path = session?.accessToken
? `${PLATFORM}/api/v1/apps/${slug}/${resource}?${qs}`
: `${PLATFORM}/api/v1/public/apps/${slug}/${resource}?${qs}`;
// 草稿预览:优先用预览包内嵌行数据(未发布也可出图)
const prev = getActivePreview();
if (prev && (!resource || resource === prev.resource || !prev.resource)) {
let items = prev.rows || [];
if (filters) {
items = items.filter((row) =>
Object.entries(filters).every(([k, v]) => !v || String(row[k] ?? "") === String(v))
);
}
const start = Math.max(0, (page - 1) * pageSize);
return {
items: items.slice(start, start + pageSize),
total: items.length,
};
}
const headers: Record<string, string> = {};
if (session?.accessToken) headers.Authorization = `Bearer ${session.accessToken}`;
const res = await apiFetch(path, { headers });
const data = await readJson(res);
throwIfBad(res, data, "list failed");
return data as { items: Record<string, unknown>[]; total: number };
}
export async function createRow(
session: Session | null | undefined,
slug: string,
resource: string,
body: Record<string, unknown>
) {
if (!session?.accessToken) throw new Error("预览模式为只读,写操作请先登录控制台");
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/${resource}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "create failed");
return data as Record<string, unknown>;
}
export async function updateRow(
session: Session | null | undefined,
slug: string,
resource: string,
id: string,
body: Record<string, unknown>
) {
if (!session?.accessToken) throw new Error("预览模式为只读,写操作请先登录控制台");
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/${resource}/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session.accessToken}`,
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "update failed");
return data as Record<string, unknown>;
}
export async function deleteRow(session: Session | null | undefined, slug: string, resource: string, id: string) {
if (!session?.accessToken) throw new Error("预览模式为只读,写操作请先登录控制台");
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/${resource}/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
if (!res.ok && res.status !== 204) {
const data = await readJson(res).catch(() => ({}));
throwIfBad(res, data, "delete failed");
}
}
export async function aggregate(
session: Session | null | undefined,
slug: string,
resource: string,
groupBy?: string,
sum?: string
) {
const q = new URLSearchParams();
if (groupBy) q.set("group_by", groupBy);
if (sum) q.set("sum", sum);
const headers: Record<string, string> = {};
if (session?.accessToken) headers.Authorization = `Bearer ${session.accessToken}`;
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/${resource}/aggregate?${q}`, {
headers,
});
const data = await readJson(res);
throwIfBad(res, data, "aggregate failed");
return data as {
total: number;
group_by?: string;
sum_field?: string;
sum?: number;
buckets?: { key: string; count: number; sum?: number }[];
};
}
export async function getCapsule(session: Session, slug: string) {
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/agent-capsule`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "capsule failed");
return data as { capsule: string; format: string; hint: string };
}
export async function importRows(session: Session | null | undefined, slug: string, resource: string, file: File) {
if (!session?.accessToken) throw new Error("预览模式为只读,写操作请先登录控制台");
const fd = new FormData();
fd.append("file", file);
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/${resource}/import`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
body: fd,
});
const data = await readJson(res);
throwIfBad(res, data, "import failed");
return data;
}
/** @deprecated use importRows */
export const importCSV = importRows;
export async function exportRows(
session: Session | null | undefined,
slug: string,
resource: string,
format: "xlsx" | "csv" = "xlsx"
) {
if (!session?.accessToken) throw new Error("预览模式为只读,写操作请先登录控制台");
const res = await apiFetch(
`${PLATFORM}/api/v1/apps/${slug}/${resource}/export?format=${format}`,
{ headers: { Authorization: `Bearer ${session.accessToken}` } }
);
if (!res.ok) {
const data = await readJson(res).catch(() => ({}));
throwIfBad(res, data, "export failed");
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${resource}.${format === "csv" ? "csv" : "xlsx"}`;
a.click();
URL.revokeObjectURL(url);
}
export async function listAudit(session: Session, page = 1) {
const res = await apiFetch(`/api/v1/audit/logs?page=${page}&page_size=50`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "audit failed");
return data as { items: any[]; total: number };
}
export type AgentAccount = {
agent_id: number;
tenant_id: number;
name: string;
client_id: string;
host_key?: string;
role_id?: number;
role_code?: string;
role_name?: string;
status: string;
permissions: string[];
app_slugs: string[];
/** Z8b绑定同步通道 */
channel_id?: string;
/** Z8b绑定线上库 id */
online_db_id?: string;
/** Z8b模块落库名database_per_app */
database_name?: string;
created_by: number;
created_at: string;
last_token_at?: string;
};
export type Role = {
role_id: number;
tenant_id: number;
code: string;
name: string;
description: string;
permissions: string[];
created_at: string;
};
export async function listAgents(session: Session) {
const res = await apiFetch(`/api/v1/admin/agents`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list agents failed");
return data as { items: AgentAccount[] };
}
export async function createAgent(
session: Session,
body: {
name: string;
role_id: number;
app_slugs: string[];
permissions?: string[];
channel_id?: string;
online_db_id?: string;
database_name?: string;
}
) {
const res = await apiFetch(`/api/v1/admin/agents`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "create agent failed");
return data as { account: AgentAccount; client_secret: string };
}
export async function updateAgent(
session: Session,
id: number,
body: {
name?: string;
status?: string;
role_id?: number;
permissions?: string[];
app_slugs?: string[];
channel_id?: string;
online_db_id?: string;
database_name?: string;
}
) {
const res = await apiFetch(`/api/v1/admin/agents/${id}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "update agent failed");
return data as AgentAccount;
}
export async function rotateAgentSecret(session: Session, id: number) {
const res = await apiFetch(`/api/v1/admin/agents/${id}/rotate-secret`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "rotate secret failed");
return data as { client_id: string; client_secret: string };
}
export async function deleteAgent(session: Session, id: number) {
const res = await apiFetch(`/api/v1/admin/agents/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "delete agent failed");
}
export async function listRoles(session: Session) {
const res = await apiFetch(`/api/v1/admin/roles`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list roles failed");
return data as { items: Role[] };
}
export async function createRole(
session: Session,
body: { code: string; name: string; description?: string; permissions: string[] }
) {
const res = await apiFetch(`/api/v1/admin/roles`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "create role failed");
return data as Role;
}
export async function updateRole(
session: Session,
id: number,
body: { name?: string; description?: string; permissions?: string[] }
) {
const res = await apiFetch(`/api/v1/admin/roles/${id}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "update role failed");
return data as Role;
}
export async function deleteRole(session: Session, id: number) {
const res = await apiFetch(`/api/v1/admin/roles/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "delete role failed");
}
export type TenantInvite = {
invite_id: number;
tenant_id: number;
code: string;
role: string;
org_unit_id?: number;
max_uses: number;
used_count: number;
status: string;
expires_at?: string;
created_at: string;
};
export type OrgUnit = {
org_unit_id: number;
tenant_id: number;
parent_id?: number;
name: string;
code?: string;
depth: number;
path: string;
created_at: string;
};
export async function acceptInvite(session: Session, code: string) {
const res = await apiFetch(`/api/v1/auth/invites/accept`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ code }),
});
const data = await readJson(res);
throwIfBad(res, data, "accept invite failed");
return toSession(data);
}
export async function createTenant(session: Session, name: string) {
const res = await apiFetch(`/api/v1/tenants`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name }),
});
const data = await readJson(res);
throwIfBad(res, data, "create tenant failed");
return toSession(data);
}
export async function listInvites(session: Session) {
const res = await apiFetch(`/api/v1/admin/invites`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list invites failed");
return data as { items: TenantInvite[] };
}
export async function createInvite(
session: Session,
body: { role?: string; org_unit_id?: number; max_uses?: number; expires_in_hours?: number }
) {
const res = await apiFetch(`/api/v1/admin/invites`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "create invite failed");
return data as TenantInvite;
}
export async function revokeInvite(session: Session, id: number) {
const res = await apiFetch(`/api/v1/admin/invites/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "revoke invite failed");
}
export async function listOrgUnits(session: Session) {
const res = await apiFetch(`/api/v1/admin/org-units`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list org units failed");
return data as { items: OrgUnit[]; max_depth: number };
}
export async function createOrgUnit(
session: Session,
body: { parent_id?: number; name: string; code?: string }
) {
const res = await apiFetch(`/api/v1/admin/org-units`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "create org unit failed");
return data as OrgUnit;
}
export async function updateOrgUnit(
session: Session,
id: number,
body: { name?: string; code?: string }
) {
const res = await apiFetch(`/api/v1/admin/org-units/${id}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "update org unit failed");
return data as OrgUnit;
}
export async function deleteOrgUnit(session: Session, id: number) {
const res = await apiFetch(`/api/v1/admin/org-units/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "delete org unit failed");
}
export type SyncEndpoint = {
driver: string;
dsn: string;
tables: string[];
};
export type SyncChannel = {
id: string;
tenant_id?: number;
name: string;
enabled: boolean;
direction: string;
conflict_policy: string;
poll_interval_ms: number;
local: SyncEndpoint;
remote: SyncEndpoint;
pk_columns: Record<string, string>;
/** 绑定智能体:模块/库归属对照 */
agent_id?: number;
/** 公司默认同步通道Z12 */
is_system_default?: boolean;
/** 绑定已发布模块 slug */
app_slug?: string;
last_error?: string;
last_sync_at?: string;
stats?: {
pushed_ok: number;
pushed_applied?: number;
pushed_skipped?: number;
pulled_ok: number;
conflicts: number;
retries: number;
last_batch: number;
};
};
export type SyncTableInspect = {
name: string;
row_count: number;
columns: string[];
column_count: number;
};
export type SyncInspectResult = {
ok: boolean;
side: string;
driver: string;
dsn_hint?: string;
tables: SyncTableInspect[];
message?: string;
};
export type SyncPreviewResult = {
ok: boolean;
table: string;
columns: string[];
rows: Record<string, unknown>[];
total: number;
limit: number;
message?: string;
};
export type SyncConflict = {
id: string;
channel_id: string;
table: string;
row_pk: string;
op: string;
source: string;
payload: string;
message: string;
resolved: boolean;
};
export async function listSyncChannels(session: Session) {
const res = await apiFetch(`/api/v1/admin/sync/channels`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list sync channels failed");
return data as { items: SyncChannel[] };
}
export async function createSyncChannel(session: Session, body: Partial<SyncChannel>) {
const res = await apiFetch(`/api/v1/admin/sync/channels`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "create sync channel failed");
return data as SyncChannel;
}
export async function updateSyncChannel(session: Session, id: string, body: Partial<SyncChannel>) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "update sync channel failed");
return data as SyncChannel;
}
export async function deleteSyncChannel(session: Session, id: string) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "delete sync channel failed");
return data as {
ok?: boolean;
deleted_id?: string;
was_system_default?: boolean;
recreated_default?: boolean;
channel_id?: string;
bindings_fixed?: number;
agents_fixed?: number;
heal_error?: string;
};
}
export async function testSyncEndpoints(
session: Session,
body: { local?: SyncEndpoint; remote?: SyncEndpoint; side?: string }
) {
const res = await apiFetch(`/api/v1/admin/sync/test`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "test sync failed");
return data as Record<string, unknown>;
}
export async function startSyncChannel(session: Session, id: string) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/start`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "start sync failed");
return data;
}
export async function stopSyncChannel(session: Session, id: string) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/stop`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "stop sync failed");
return data;
}
/** @deprecated 公司侧已 403请用 listPlatformLwwOverrides */
export async function listSyncConflicts(session: Session, unresolved = true) {
const q = unresolved ? "?unresolved=1" : "?unresolved=0";
const res = await apiFetch(`/api/v1/admin/sync/conflicts${q}`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list conflicts failed (公司侧已禁用,请用超管 LWW 审计)");
return data as { items: SyncConflict[] };
}
/** @deprecated 公司侧已 403 */
export async function resolveSyncConflict(session: Session, id: string, resolution: string) {
const res = await apiFetch(`/api/v1/admin/sync/conflicts/${encodeURIComponent(id)}/resolve`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ resolution }),
});
const data = await readJson(res);
throwIfBad(res, data, "resolve conflict failed (公司侧已禁用)");
return data;
}
export async function reconcileSyncChannel(session: Session, id: string) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/reconcile`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "reconcile failed");
return data as {
channel_id: string;
reports: Array<{
table: string;
only_local: string[];
only_remote: string[];
patched_push: number;
patched_pull: number;
}>;
message: string;
};
}
export type SyncCheckpointSlot = {
synced_at?: string;
source?: string;
table_count?: number;
row_count?: number;
};
export type SyncCheckpointMeta = {
channel_id?: string;
latest?: SyncCheckpointSlot | null;
previous?: SyncCheckpointSlot | null;
};
export async function getSyncCheckpointMeta(session: Session, id: string) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/checkpoint`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "get checkpoint failed");
return data as SyncCheckpointMeta;
}
export async function restoreSyncChannel(
session: Session,
id: string,
body: { which?: "latest" | "previous"; confirm: boolean }
) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/restore`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "restore failed");
return data as {
ok?: boolean;
which?: string;
synced_at?: string;
source?: string;
tables?: number;
upserted?: number;
deleted?: number;
restored_at?: string;
};
}
/** 外部源 C → 写入本地 B再经 outbox 同步到线上 A */
export async function ingestSyncRows(
session: Session,
id: string,
body: { table: string; source?: string; rows: Record<string, unknown>[] }
) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/ingest`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "ingest failed");
return data as { ok: boolean; ingested: number; hint?: string };
}
/** 控制台验同步:列出通道线上/本机库的表、行数、字段 */
export async function inspectSyncChannel(
session: Session,
id: string,
opts?: { side?: "remote" | "local"; include_sync_meta?: boolean }
) {
const side = opts?.side || "remote";
const q = new URLSearchParams({ side });
if (opts?.include_sync_meta) q.set("include_sync_meta", "1");
const res = await apiFetch(
`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/inspect?${q}`,
{ headers: { Authorization: `Bearer ${session.accessToken}` } }
);
const data = await readJson(res);
throwIfBad(res, data, "inspect failed");
return data as SyncInspectResult;
}
/** 预览单表内容 */
export async function previewSyncTable(
session: Session,
id: string,
body: { table: string; side?: "remote" | "local"; limit?: number }
) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/preview`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "preview failed");
return data as SyncPreviewResult;
}
/** 控制台删业务表(仅当前 side不同步 DDL 到另一侧) */
export async function dropSyncTable(
session: Session,
id: string,
body: { table: string; side?: "remote" | "local" }
) {
const res = await apiFetch(`/api/v1/admin/sync/channels/${encodeURIComponent(id)}/drop-table`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "drop table failed");
return data as {
ok: boolean;
side: string;
table: string;
dropped: boolean;
meta_cleared?: number;
message?: string;
};
}
export async function ensureSyncBinding(
session: Session,
body: {
local_database_id: string;
online_db_id: string;
channel_id?: string;
database_name?: string;
display_name?: string;
note?: string;
user_id?: number;
shared?: boolean;
}
) {
const res = await apiFetch(`/api/v1/admin/sync/bindings`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "ensure binding failed");
return data as SyncBinding;
}
export type SyncBinding = {
id: string;
tenant_id: number;
user_id?: number;
local_database_id: string;
online_db_id: string;
channel_id?: string;
database_name?: string;
display_name?: string;
shared?: boolean;
note?: string;
created_at?: string;
updated_at?: string;
};
export type BindCode = {
code: string;
tenant_id: number;
channel_id?: string;
online_db_id?: string;
database_name?: string;
max_uses: number;
used_count: number;
revoked?: boolean;
expires_at?: string;
created_by?: number;
created_at?: string;
note?: string;
};
export async function listBindCodes(session: Session) {
const res = await apiFetch(`/api/v1/admin/bind-codes`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list bind codes failed");
return data as { items: BindCode[] };
}
export async function createBindCode(
session: Session,
body?: {
channel_id?: string;
online_db_id?: string;
database_name?: string;
max_uses?: number;
expires_hours?: number;
note?: string;
}
) {
const res = await apiFetch(`/api/v1/admin/bind-codes`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body || {}),
});
const data = await readJson(res);
throwIfBad(res, data, "create bind code failed");
return data as BindCode;
}
export async function revokeBindCode(session: Session, code: string) {
const res = await apiFetch(`/api/v1/admin/bind-codes/${encodeURIComponent(code)}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "revoke bind code failed");
return data as { ok: boolean };
}
export async function listSyncBindings(session: Session, localDatabaseId?: string) {
const q = localDatabaseId
? `?local_database_id=${encodeURIComponent(localDatabaseId)}`
: "";
const res = await apiFetch(`/api/v1/admin/sync/bindings${q}`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list bindings failed");
return data as { items: SyncBinding[] };
}
export async function listPlatformLwwOverrides(
session: Session,
opts?: { tenant_id?: number; channel_id?: string; limit?: number }
) {
const q = new URLSearchParams();
if (opts?.tenant_id) q.set("tenant_id", String(opts.tenant_id));
if (opts?.channel_id) q.set("channel_id", opts.channel_id);
if (opts?.limit) q.set("limit", String(opts.limit));
const qs = q.toString();
const res = await apiFetch(`/api/v1/platform/dbsync/lww-overrides${qs ? `?${qs}` : ""}`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list lww overrides failed");
return data as {
items: Array<{
id: string;
tenant_id: number;
channel_id: string;
table: string;
row_pk: string;
op: string;
entry: string;
policy: string;
outcome: string;
loser_payload?: string;
winner_payload?: string;
target_ver: number;
source_ver: number;
created_at: string;
}>;
hint?: string;
};
}
export async function rollbackPlatformLwwOverride(session: Session, id: string) {
const res = await apiFetch(
`/api/v1/platform/dbsync/lww-overrides/${encodeURIComponent(id)}/rollback`,
{
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
}
);
const data = await readJson(res);
throwIfBad(res, data, "lww rollback failed");
return data as { ok: boolean; record?: Record<string, unknown>; hint?: string };
}
export async function listPlatformTenants(session: Session) {
const res = await apiFetch(`/api/v1/platform/tenants`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list tenants failed");
return data as {
items: Array<{
tenant_id: number;
name: string;
slug: string;
created_at: string;
user_count: number;
app_count: number;
}>;
};
}
export async function listPlatformPermModules(session: Session) {
const res = await apiFetch(`/api/v1/platform/perm-modules`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list perm modules failed");
return data as {
modules: { title: string; items: { perm: string; desc: string }[] }[];
catalog: string[];
platform: string[];
};
}
export async function getPlatformTenantPerms(session: Session, tenantId: number) {
const res = await apiFetch(`/api/v1/platform/tenants/${tenantId}/permissions`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "get tenant perms failed");
return data as { tenant_id: number; permissions: string[]; total: number };
}
export async function setPlatformTenantPerms(session: Session, tenantId: number, permissions: string[]) {
const res = await apiFetch(`/api/v1/platform/tenants/${tenantId}/permissions`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ permissions }),
});
const data = await readJson(res);
throwIfBad(res, data, "set tenant perms failed");
return data as { tenant_id: number; permissions: string[]; total: number };
}
export async function createPlatformTenant(
session: Session,
name: string,
withAdminInvite = false,
slug = "",
adminPhone = ""
) {
const res = await apiFetch(`/api/v1/platform/tenants`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name,
slug: slug || undefined,
with_admin_invite: withAdminInvite,
admin_phone: adminPhone.trim() || undefined,
}),
});
const data = await readJson(res);
throwIfBad(res, data, "create tenant failed");
return data as {
tenant: { tenant_id: number; name: string; slug: string };
admin_account?: {
user_id: number;
username: string;
password: string;
phone?: string;
display_name: string;
role: string;
};
admin_invite?: { code: string; role: string; expires_at?: string };
};
}
export async function updatePlatformTenant(
session: Session,
tenantId: number,
name: string,
slug = ""
) {
const res = await apiFetch(`/api/v1/platform/tenants/${tenantId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name, slug: slug || undefined }),
});
const data = await readJson(res);
throwIfBad(res, data, "update tenant failed");
return data as { tenant_id: number; name: string; slug: string };
}
export async function issuePlatformAdminInvite(session: Session, tenantId: number) {
const res = await apiFetch(`/api/v1/platform/tenants/${tenantId}/admin-invite`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "admin invite failed");
return data as { code: string; role: string; expires_at?: string };
}
export async function issuePlatformAdminAccount(session: Session, tenantId: number, phone: string) {
const res = await apiFetch(`/api/v1/platform/tenants/${tenantId}/admin-account`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ phone: phone.trim() }),
});
const data = await readJson(res);
throwIfBad(res, data, "admin account failed");
return data as {
admin_account: {
user_id: number;
username: string;
password: string;
phone?: string;
display_name: string;
role: string;
};
};
}
export type PlatformAdminInfo = {
user_id: number;
username: string;
phone: string;
username_login_disabled?: boolean;
display_name: string;
role: string;
status: string;
};
export async function listPlatformTenantAdmins(session: Session, tenantId: number) {
const res = await apiFetch(`/api/v1/platform/tenants/${tenantId}/admins`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list admins failed");
return data as { items: PlatformAdminInfo[] };
}
export async function updatePlatformTenantAdmin(
session: Session,
tenantId: number,
userId: number,
body: { reset_password?: boolean; password?: string; phone?: string; disable_username_login?: boolean }
) {
const res = await apiFetch(`/api/v1/platform/tenants/${tenantId}/admins/${userId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "update admin failed");
return data as {
admin_account: {
user_id: number;
username: string;
password?: string;
phone?: string;
display_name: string;
role: string;
};
};
}
export async function enterPlatformTenant(session: Session, tenantId: number): Promise<Session> {
const res = await apiFetch(`/api/v1/platform/tenants/${tenantId}/enter`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "enter tenant failed");
return toSession(data);
}
export async function exitPlatformTenant(session: Session): Promise<Session> {
const res = await apiFetch(`/api/v1/platform/exit`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "exit tenant failed");
return toSession(data);
}
export async function getCompanyEntitlements(session: Session) {
const res = await apiFetch(`/api/v1/admin/entitlements`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "get entitlements failed");
return data as {
permissions: string[];
modules: { title: string; items: { perm: string; desc: string }[] }[];
};
}
export type Member = {
user_id: number;
username: string;
phone?: string;
display_name: string;
role: string;
org_unit_id: number;
status: string;
created_at?: string;
};
export async function listMembers(session: Session) {
const res = await apiFetch(`/api/v1/admin/members`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "list members failed");
return data as { items: Member[] };
}
export async function createMember(
session: Session,
body: {
password?: string;
display_name?: string;
role?: string;
org_unit_id?: number;
}
) {
const res = await apiFetch(`/api/v1/admin/members`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "create member failed");
return data as Member & { password: string };
}
export async function changePassword(session: Session, oldPassword: string, newPassword: string) {
const res = await apiFetch(`/api/v1/auth/password`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }),
});
const data = await readJson(res);
throwIfBad(res, data, "change password failed");
return data as { ok: boolean };
}
export async function fetchMe(session: Session) {
const res = await apiFetch(`/api/v1/auth/me`, {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const data = await readJson(res);
throwIfBad(res, data, "fetch me failed");
return data as {
user_id: number;
username: string;
phone: string;
display_name: string;
role: string;
tenant_id: number;
status: string;
};
}
export async function bindPhone(
session: Session,
phone: string,
disableUsernameLogin?: boolean
) {
const body: { phone: string; disable_username_login?: boolean } = { phone };
if (disableUsernameLogin !== undefined) {
body.disable_username_login = disableUsernameLogin;
}
const res = await apiFetch(`/api/v1/auth/phone`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "bind phone failed");
return data as {
user_id: number;
username: string;
phone: string;
username_login_disabled: boolean;
display_name: string;
};
}
export async function updateMember(
session: Session,
id: number,
body: { role?: string; org_unit_id?: number; status?: string; phone?: string }
) {
const res = await apiFetch(`/api/v1/admin/members/${id}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await readJson(res);
throwIfBad(res, data, "update member failed");
return data as Member;
}
export async function uploadFile(session: Session, file: File) {
const fd = new FormData();
fd.append("file", file);
const res = await apiFetch(`/api/v1/storage`, {
method: "POST",
headers: { Authorization: `Bearer ${session.accessToken}` },
body: fd,
});
const data = await readJson(res);
throwIfBad(res, data, "upload failed");
return data as { key: string; url: string; filename: string };
}