feat: harden loose-offline sync for user JWT, schema, and console ops

Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-05 09:47:35 +08:00
parent 76cdcd760e
commit b04b180d30
59 changed files with 4762 additions and 308 deletions

View File

@@ -537,6 +537,12 @@ export type AgentAccount = {
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;
@@ -563,7 +569,15 @@ export async function listAgents(session: Session) {
export async function createAgent(
session: Session,
body: { name: string; role_id: number; app_slugs: string[]; permissions?: string[] }
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",
@@ -587,6 +601,9 @@ export async function updateAgent(
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}`, {
@@ -831,10 +848,16 @@ export type SyncChannel = {
local: SyncEndpoint;
remote: SyncEndpoint;
pk_columns: Record<string, string>;
/** 绑定智能体:模块/库归属对照 */
agent_id?: number;
/** 绑定已发布模块 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;
@@ -842,6 +865,32 @@ export type SyncChannel = {
};
};
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;
@@ -1002,12 +1051,77 @@ export async function ingestSyncRows(
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;
}
@@ -1022,9 +1136,23 @@ export async function ensureSyncBinding(
});
const data = await readJson(res);
throwIfBad(res, data, "ensure binding failed");
return data;
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;
note?: string;
created_at?: string;
updated_at?: string;
};
export async function listSyncBindings(session: Session, localDatabaseId?: string) {
const q = localDatabaseId
? `?local_database_id=${encodeURIComponent(localDatabaseId)}`
@@ -1034,7 +1162,7 @@ export async function listSyncBindings(session: Session, localDatabaseId?: strin
});
const data = await readJson(res);
throwIfBad(res, data, "list bindings failed");
return data as { items: Array<Record<string, unknown>> };
return data as { items: SyncBinding[] };
}
export async function listPlatformLwwOverrides(